[libadwaita/ebassi/tagged-entry: 14/20] Add AdwTaggedEntry
- From: Emmanuele Bassi <ebassi src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [libadwaita/ebassi/tagged-entry: 14/20] Add AdwTaggedEntry
- Date: Mon, 14 Mar 2022 18:46:42 +0000 (UTC)
commit 3f8095dd0f804073ed33da10ae2a8c5455b30403
Author: Emmanuele Bassi <ebassi gnome org>
Date: Fri Feb 4 14:09:08 2022 +0000
Add AdwTaggedEntry
A "tagged entry" is an entry widget that contains additional UI
elements called "tags" alongside the text.
Tags can be added view three possible mechanisms:
1. explicit external UI, like buttons
2. automatically, by looking at the contents of the text entry
3. from a list of possible candidates in a list model
Current existing users of a similar UI are:
- GdTaggedEntry, as used by Nautilus, Epiphany, and Photos
- NSTokenField, as used by AppKit and UIKit
- Input chips, as used by Android's Material
src/adw-tag-match-private.h | 27 +
src/adw-tag-match.c | 160 ++++++
src/adw-tag.c | 185 ++++++-
src/adw-tagged-entry.c | 1155 +++++++++++++++++++++++++++++++++++++++++++
src/adw-tagged-entry.h | 93 ++++
src/adw-tagged-entry.ui | 56 +++
src/adwaita.gresources.xml | 2 +
src/adwaita.h | 1 +
src/meson.build | 5 +-
9 files changed, 1669 insertions(+), 15 deletions(-)
---
diff --git a/src/adw-tag-match-private.h b/src/adw-tag-match-private.h
new file mode 100644
index 00000000..51545f6b
--- /dev/null
+++ b/src/adw-tag-match-private.h
@@ -0,0 +1,27 @@
+/* adw-tag-match-private.h
+ *
+ * SPDX-FileCopyrightText: 2022 Emmanuele Bassi
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include "adw-tag-private.h"
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_TAG_MATCH (adw_tag_match_get_type())
+G_DECLARE_FINAL_TYPE (AdwTagMatch, adw_tag_match, ADW, TAG_MATCH, GObject)
+
+AdwTagMatch *adw_tag_match_new (gpointer item,
+ const char *str);
+
+gpointer adw_tag_match_get_item (AdwTagMatch *self);
+
+const char *adw_tag_match_get_string (AdwTagMatch *self);
+
+void adw_tag_match_set_tag (AdwTagMatch *self,
+ AdwTag *tag);
+AdwTag *adw_tag_match_get_tag (AdwTagMatch *self);
+
+G_END_DECLS
diff --git a/src/adw-tag-match.c b/src/adw-tag-match.c
new file mode 100644
index 00000000..f2ad9cea
--- /dev/null
+++ b/src/adw-tag-match.c
@@ -0,0 +1,160 @@
+/* adw-tag-match.c:
+ *
+ * SPDX-FileCopyrightText: 2022 Emmanuele Bassi
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "adw-tag-match-private.h"
+
+struct _AdwTagMatch
+{
+ GObject parent_instance;
+
+ /* type: GObject, owned */
+ gpointer item;
+
+ char *str;
+
+ AdwTag *tag;
+};
+
+enum
+{
+ PROP_ITEM = 1,
+ PROP_STRING,
+
+ N_PROPS
+};
+
+static GParamSpec *obj_props[N_PROPS];
+
+G_DEFINE_FINAL_TYPE (AdwTagMatch, adw_tag_match, G_TYPE_OBJECT)
+
+static void
+adw_tag_match_set_property (GObject *gobject,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ AdwTagMatch *self = ADW_TAG_MATCH (gobject);
+
+ switch (prop_id) {
+ case PROP_ITEM:
+ g_clear_object (&self->item);
+ self->item = g_value_dup_object (value);
+ break;
+
+ case PROP_STRING:
+ g_clear_pointer (&self->str, g_free);
+ self->str = g_value_dup_string (value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+ }
+}
+
+static void
+adw_tag_match_get_property (GObject *gobject,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ AdwTagMatch *self = ADW_TAG_MATCH (gobject);
+
+ switch (prop_id) {
+ case PROP_ITEM:
+ g_value_set_object (value, self->item);
+ break;
+
+ case PROP_STRING:
+ g_value_set_string (value, self->str);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+ }
+}
+
+static void
+adw_tag_match_dispose (GObject *gobject)
+{
+ AdwTagMatch *self = ADW_TAG_MATCH (gobject);
+
+ g_clear_object (&self->tag);
+ g_clear_object (&self->item);
+ g_clear_pointer (&self->str, g_free);
+
+ G_OBJECT_CLASS (adw_tag_match_parent_class)->dispose (gobject);
+}
+
+static void
+adw_tag_match_class_init (AdwTagMatchClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->set_property = adw_tag_match_set_property;
+ gobject_class->get_property = adw_tag_match_get_property;
+ gobject_class->dispose = adw_tag_match_dispose;
+
+ obj_props[PROP_ITEM] =
+ g_param_spec_object ("item", NULL, NULL,
+ G_TYPE_OBJECT,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS);
+ obj_props[PROP_STRING] =
+ g_param_spec_string ("string", NULL, NULL,
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (gobject_class, N_PROPS, obj_props);
+}
+
+static void
+adw_tag_match_init (AdwTagMatch *self)
+{
+}
+
+AdwTagMatch *
+adw_tag_match_new (gpointer item,
+ const char *str)
+{
+ return g_object_new (ADW_TYPE_TAG_MATCH,
+ "item", item,
+ "string", str,
+ NULL);
+}
+
+gpointer
+adw_tag_match_get_item (AdwTagMatch *self)
+{
+ g_return_val_if_fail (ADW_IS_TAG_MATCH (self), NULL);
+
+ return self->item;
+}
+
+const char *
+adw_tag_match_get_string (AdwTagMatch *self)
+{
+ g_return_val_if_fail (ADW_IS_TAG_MATCH (self), NULL);
+
+ return self->str;
+}
+
+void
+adw_tag_match_set_tag (AdwTagMatch *self,
+ AdwTag *tag)
+{
+ g_set_object (&self->tag, tag);
+}
+
+AdwTag *
+adw_tag_match_get_tag (AdwTagMatch *self)
+{
+ return self->tag;
+}
diff --git a/src/adw-tag.c b/src/adw-tag.c
index 7a3d245b..3fa59390 100644
--- a/src/adw-tag.c
+++ b/src/adw-tag.c
@@ -1,3 +1,10 @@
+/* adw-tag.c
+ *
+ * SPDX-FileCopyrightText: 2022 Emmanuele Bassi
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
#include "config.h"
#include "adw-tag-private.h"
@@ -149,10 +156,10 @@ adw_tag_class_init (AdwTagClass *klass)
/**
* AdwTag:gicon:
*
- * The icon of the tag.
+ * The icon of the tag as a `GIcon`.
*
- * Setting this property will also set the [property@Tag:has-icon] as a
- * side effect.
+ * Setting this property will also set the [property@Tag:has-icon] property
+ * as a side effect.
*
* Since: 1.2
*/
@@ -162,6 +169,16 @@ adw_tag_class_init (AdwTagClass *klass)
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS |
G_PARAM_EXPLICIT_NOTIFY);
+ /**
+ * AdwTag:paintable:
+ *
+ * The icon of the tag, as a `GdkPaintable`.
+ *
+ * Setting this property will also set the [property@Tag:has-icon] property
+ * as a side effect.
+ *
+ * Since: 1.2
+ */
tag_props[PROP_TAG_PAINTABLE] =
g_param_spec_object ("paintable", NULL, NULL,
GDK_TYPE_PAINTABLE,
@@ -228,7 +245,6 @@ adw_tag_class_init (AdwTagClass *klass)
G_VARIANT_TYPE_ANY,
NULL,
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
-
/**
* AdwTag:has-icon:
*
@@ -252,6 +268,15 @@ adw_tag_init (AdwTag *self)
self->show_close = TRUE;
}
+/**
+ * adw_tag_new:
+ *
+ * Creates a new tag object.
+ *
+ * Returns: (transfer full): the newly created tag
+ *
+ * Since: 1.2
+ */
AdwTag *
adw_tag_new (void)
{
@@ -265,6 +290,8 @@ adw_tag_new (void)
* Retrieves the user readable label of the tag.
*
* Returns: (transfer none): the label of the tag
+ *
+ * Since: 1.2
*/
const char *
adw_tag_get_label (AdwTag *self)
@@ -280,6 +307,8 @@ adw_tag_get_label (AdwTag *self)
* @label: (not nullable): the label of the tag
*
* Sets the user readable label of the tag.
+ *
+ * Since: 1.2
*/
void
adw_tag_set_label (AdwTag *self,
@@ -304,6 +333,8 @@ adw_tag_set_label (AdwTag *self,
* Checks whether the tag should show a close button or not.
*
* Returns: true if the tag has a visible close button
+ *
+ * Since: 1.2
*/
gboolean
adw_tag_get_show_close (AdwTag *self)
@@ -318,6 +349,8 @@ adw_tag_get_show_close (AdwTag *self)
* @self: the tag we want to update
*
* Sets whether the tag should show a close button or not.
+ *
+ * Since: 1.2
*/
void
adw_tag_set_show_close (AdwTag *self,
@@ -334,14 +367,6 @@ adw_tag_set_show_close (AdwTag *self,
}
}
-GIcon *
-adw_tag_get_gicon (AdwTag *self)
-{
- g_return_val_if_fail (ADW_IS_TAG (self), NULL);
-
- return self->gicon;
-}
-
static void
update_icon_type (AdwTag *self)
{
@@ -353,6 +378,33 @@ update_icon_type (AdwTag *self)
self->icon_type = ADW_TAG_ICON_NONE;
}
+/**
+ * adw_tag_get_gicon:
+ * @self: the tag
+ *
+ * Retrieves the icon of the tag set using [method@Tag.set_gicon].
+ *
+ * Returns: (transfer none): the icon of the tag
+ *
+ * Since: 1.2
+ */
+GIcon *
+adw_tag_get_gicon (AdwTag *self)
+{
+ g_return_val_if_fail (ADW_IS_TAG (self), NULL);
+
+ return self->gicon;
+}
+
+/**
+ * adw_tag_set_gicon:
+ * @self: the tag
+ * @icon: (nullable): the icon to set
+ *
+ * Sets the icon of the tag using the given `GIcon`.
+ *
+ * Since: 1.2
+ */
void
adw_tag_set_gicon (AdwTag *self,
GIcon *icon)
@@ -367,14 +419,33 @@ adw_tag_set_gicon (AdwTag *self,
}
}
+/**
+ * adw_tag_get_paintable:
+ * @self: the tag
+ *
+ * Retrieves the icon of the tag set using [method@Tag.set_paintable].
+ *
+ * Returns: (transfer none): the icon of the tag
+ *
+ * Since: 1.2
+ */
GdkPaintable *
-adw_tag_get_paintable (AdwTag *self)
+adw_tag_get_paintable (AdwTag *self)
{
g_return_val_if_fail (ADW_IS_TAG (self), NULL);
return self->paintable;
}
+/**
+ * adw_tag_set_paintable:
+ * @self: a tag
+ * @paintable: (nullable): the icon of the tag
+ *
+ * Sets the icon of the tag using the given `GdkPaintable`.
+ *
+ * Since: 1.2
+ */
void
adw_tag_set_paintable (AdwTag *self,
GdkPaintable *paintable)
@@ -389,6 +460,16 @@ adw_tag_set_paintable (AdwTag *self,
}
}
+/**
+ * adw_tag_has_icon:
+ * @self: a tag
+ *
+ * Checks whether the tag should display an icon.
+ *
+ * Returns: true if the tag has an icon
+ *
+ * Since: 1.2
+ */
gboolean
adw_tag_has_icon (AdwTag *self)
{
@@ -397,12 +478,30 @@ adw_tag_has_icon (AdwTag *self)
return self->icon_type != ADW_TAG_ICON_NONE;
}
+/*< private >
+ * adw_tag_get_icon_type:
+ * @self: a tag
+ *
+ * Retrieves the type of icon used by the tag.
+ *
+ * Returns: the type of the icon
+ */
AdwTagIconType
adw_tag_get_icon_type (AdwTag *self)
{
return self->icon_type;
}
+/**
+ * adw_tag_get_action_name:
+ * @self: a tag
+ *
+ * Retrieves the name of the action set using [method@Tag.set_action_name].
+ *
+ * Returns: (transfer none): the name of the action
+ *
+ * Since: 1.2
+ */
const char *
adw_tag_get_action_name (AdwTag *self)
{
@@ -411,6 +510,16 @@ adw_tag_get_action_name (AdwTag *self)
return self->action_name;
}
+/**
+ * adw_tag_set_action_name:
+ * @self: a tag
+ * @action_name: the name of the action for the tag
+ *
+ * Sets the name of the action to be activated when the tag is
+ * activated.
+ *
+ * Since: 1.2
+ */
void
adw_tag_set_action_name (AdwTag *self,
const char *action_name)
@@ -426,6 +535,17 @@ adw_tag_set_action_name (AdwTag *self,
g_object_notify_by_pspec (G_OBJECT (self), tag_props[PROP_TAG_ACTION_NAME]);
}
+/**
+ * adw_tag_get_action_target_value:
+ * @self: a tag
+ *
+ * Retrieves the target value for the tag's action set using
+ * [method@Tag.set_action_target_value].
+ *
+ * Returns: (transfer none): the action's target value
+ *
+ * Since: 1.2
+ */
GVariant *
adw_tag_get_action_target_value (AdwTag *self)
{
@@ -434,6 +554,20 @@ adw_tag_get_action_target_value (AdwTag *self)
return self->action_target;
}
+/**
+ * adw_tag_set_action_target_value:
+ * @self: a tag
+ * @action_target: (nullable): the target value for the tag's action
+ *
+ * Sets the target value for the tag's action.
+ *
+ * The value will be used when activating the action bound to the
+ * tag.
+ *
+ * If @action_target has a floating reference, it will be sunk.
+ *
+ * Since: 1.2
+ */
void
adw_tag_set_action_target_value (AdwTag *self,
GVariant *action_target)
@@ -453,6 +587,18 @@ adw_tag_set_action_target_value (AdwTag *self,
g_object_notify_by_pspec (G_OBJECT (self), tag_props[PROP_TAG_ACTION_TARGET]);
}
+/**
+ * adw_tag_set_action_target:
+ * @self: a tag
+ * @format_string: the `GVariant` format string for the data
+ * @...: the target value for the action
+ *
+ * Sets the target value for the tag's action.
+ *
+ * See also: [method@Tag.set_action_target_value].
+ *
+ * Since: 1.2
+ */
void
adw_tag_set_action_target (AdwTag *self,
const char *format_string,
@@ -468,12 +614,23 @@ adw_tag_set_action_target (AdwTag *self,
va_end (args);
}
+/**
+ * adw_tag_set_detailed_action_name:
+ * @self: a tag
+ * @detailed_action_name: (nullable): a detailed action name and target
+ *
+ * Sets the tag's action and target value.
+ *
+ * See also: [func@Gio.Action.parse_detailed_name]
+ *
+ * Since: 1.2
+ */
void
adw_tag_set_detailed_action_name (AdwTag *self,
const char *detailed_action_name)
{
char *name;
- GVariant *target;
+ GVariant *target = NULL;
GError *error = NULL;
g_return_if_fail (ADW_IS_TAG (self));
diff --git a/src/adw-tagged-entry.c b/src/adw-tagged-entry.c
new file mode 100644
index 00000000..d1d6eee7
--- /dev/null
+++ b/src/adw-tagged-entry.c
@@ -0,0 +1,1155 @@
+/* adw-tagged-entry.c: Tagged entry widget
+ *
+ * SPDX-FileCopyrightText: 2022 Emmanuele Bassi
+ * SPDX-FileCopyrightText: 2019 Matthias Clasen
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "adw-tagged-entry.h"
+
+#include "adw-tag-match-private.h"
+#include "adw-tag-widget-private.h"
+
+/**
+ * AdwTaggedEntry:
+ *
+ * An entry that allows you to have tags near the text.
+ *
+ * ## AdwTaggedEntry as GtkBuildable
+ *
+ * You can include tags directly inside the UI definition of a tagged entry
+ * by using the `<child>` element to add objects of type [class Adw Tag]; for
+ * instance, the following definition:
+ *
+ * ```xml
+ * <object class="AdwTaggedEntry">
+ * <child>
+ * <object class="AdwTag">
+ * <property name="gicon">
+ * <object class="GThemedIcon">
+ * <property name="names">go-down-symbolic</property>
+ * </object>
+ * </property>
+ * <property name="label">First Tag</property>
+ * <property name="show-close">False</property>
+ * </object>
+ * </child>
+ * </object>
+ * ```
+ *
+ * while create an `AdwTaggedEntry` with a single tag, whose label is set to
+ * "First Tag"; the tag will not have a "close" button.
+ *
+ * ## CSS nodes
+ *
+ * `AdwTaggedEntry` has a single CSS node with the name `entry` and the
+ * CSS class `tagged`.
+ *
+ * Each tag has a single CSS node with the name `tag`, and contains a `label`
+ * node; if the [property Adw Tag:has-icon] property is true, the tag will have
+ * an `image` node; similarly, if the [property Adw Tag:show-close] is true,
+ * the tag will have a `button` node.
+ *
+ * ```
+ * entry.tagged
+ * ├── .tags
+ * ┊ ├── tag
+ * ┊ ┊ ├── [image]
+ * ┊ ┊ ├── label
+ * ┊ ┊ ╰── [button]
+ * ┊ ┊
+ * ┊ ╰── tag
+ * ╰── text
+ * ```
+ */
+struct _AdwTaggedEntry
+{
+ GtkWidget parent_instance;
+
+ GtkWidget *tags_box;
+ GtkWidget *text;
+
+ GListModel *tags;
+
+ GHashTable *widget_for_tag;
+
+ char *delimiters;
+ char *search;
+
+ guint idle_match_id;
+
+ GString *buffer;
+
+ /* Completion popover */
+ GtkWidget *popover;
+ GtkWidget *list_view;
+
+ GtkListItemFactory *factory;
+ GtkFilter *filter;
+ GtkMapListModel *map_model;
+ GtkSingleSelection *selection;
+
+ GtkExpression *match_expression;
+ GListModel *match_model;
+
+ AdwTaggedEntryMatchFunc match_func;
+ gpointer match_func_data;
+ GDestroyNotify match_func_notify;
+};
+
+enum
+{
+ PROP_PLACEHOLDER_TEXT = 1,
+ PROP_DELIMITER_CHARS,
+ PROP_MATCH_MODEL,
+ PROP_MATCH_EXPRESSION,
+ N_PROPS
+};
+
+static void buildable_iface_init (GtkBuildableIface *iface);
+static void editable_iface_init (GtkEditableInterface *iface);
+
+static AdwTag *default_match_func (AdwTaggedEntry *self,
+ const char *text,
+ gpointer item,
+ gpointer user_data);
+
+static void adw_tagged_entry_update_map (AdwTaggedEntry *self);
+
+static void on_text_notify (GtkText *text,
+ GParamSpec *pspec,
+ AdwTaggedEntry *self);
+
+static GtkBuildableIface *parent_buildable_iface;
+
+static GParamSpec *entry_props[N_PROPS];
+
+G_DEFINE_TYPE_WITH_CODE (AdwTaggedEntry, adw_tagged_entry, GTK_TYPE_WIDGET,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE, editable_iface_init))
+
+static GtkEditable *
+adw_tagged_entry_editable_get_delegate (GtkEditable *editable)
+{
+ AdwTaggedEntry *self = ADW_TAGGED_ENTRY (editable);
+
+ return GTK_EDITABLE (self->text);
+}
+
+static void
+on_text_insert_text (GtkEditable *editable,
+ const char *text,
+ int length,
+ int *position,
+ AdwTaggedEntry *self)
+{
+ if (self->match_model != NULL)
+ return;
+
+ if (self->delimiters == NULL)
+ return;
+
+ if (self->buffer == NULL)
+ self->buffer = g_string_new (NULL);
+
+ g_string_append (self->buffer, text);
+
+ char last = self->buffer->str[self->buffer->len - 1];
+
+ gsize delimiters_len = strlen (self->delimiters);
+ for (gsize i = 0; i < delimiters_len; i++) {
+ if (last == self->delimiters[i]) {
+ AdwTag *tag = adw_tag_new ();
+
+ g_autofree char *label = g_strndup (self->buffer->str, self->buffer->len - 1);
+
+ adw_tag_set_show_close (tag, TRUE);
+ adw_tag_set_label (tag, label);
+
+ adw_tagged_entry_add_tag (self, tag);
+
+ gtk_editable_delete_text (editable, 0, -1);
+
+ g_signal_stop_emission_by_name (editable, "insert-text");
+ break;
+ }
+ }
+}
+
+static void
+on_text_delete_text (GtkEditable *editable,
+ int start,
+ int end,
+ AdwTaggedEntry *self)
+{
+ if (self->buffer == NULL)
+ return;
+
+ if (self->match_model != NULL)
+ return;
+
+ if (self->delimiters == NULL)
+ return;
+
+ g_string_erase (self->buffer, start, end - start);
+}
+
+static void
+editable_iface_init (GtkEditableInterface *iface)
+{
+ iface->get_delegate = adw_tagged_entry_editable_get_delegate;
+}
+
+static void
+on_tag_closed (AdwTaggedEntry *self,
+ AdwTag *tag)
+{
+ adw_tagged_entry_remove_tag (self, tag);
+}
+
+static void
+adw_tagged_entry_add_tag_internal (AdwTaggedEntry *self,
+ AdwTag *tag,
+ gboolean remove_ref)
+{
+ g_list_store_append (G_LIST_STORE (self->tags), tag);
+
+ GtkWidget *tag_widget = g_object_new (ADW_TYPE_TAG_WIDGET,
+ "tag", tag,
+ NULL);
+
+ g_signal_connect_swapped (tag_widget, "closed", G_CALLBACK (on_tag_closed), self);
+
+ gtk_flow_box_append (GTK_FLOW_BOX (self->tags_box), tag_widget);
+
+ g_hash_table_insert (self->widget_for_tag, tag, tag_widget);
+
+ if (remove_ref)
+ g_object_unref (tag);
+}
+
+static void
+adw_tagged_entry_list_item__setup (AdwTaggedEntry *self,
+ GtkListItem *item,
+ GtkSignalListItemFactory *factory)
+{
+ GtkWidget *label = gtk_label_new (NULL);
+
+ gtk_label_set_xalign (GTK_LABEL (label), 0.0);
+
+ gtk_list_item_set_child (item, label);
+}
+
+static void
+adw_tagged_entry_list_item__bind (AdwTaggedEntry *self,
+ GtkListItem *item,
+ GtkSignalListItemFactory *factory)
+{
+ AdwTagMatch *match = gtk_list_item_get_item (item);
+ GtkWidget *label = gtk_list_item_get_child (item);
+
+ gtk_label_set_text (GTK_LABEL (label), adw_tag_match_get_string (match));
+}
+
+static void
+adw_tagged_entry_setup_list_factory (AdwTaggedEntry *self)
+{
+ GtkListItemFactory *factory = gtk_signal_list_item_factory_new ();
+
+ g_signal_connect_swapped (factory, "setup", G_CALLBACK (adw_tagged_entry_list_item__setup), self);
+ g_signal_connect_swapped (factory, "bind", G_CALLBACK (adw_tagged_entry_list_item__bind), self);
+
+ gtk_list_view_set_factory (GTK_LIST_VIEW (self->list_view), factory);
+ g_object_unref (factory);
+
+ self->factory = factory;
+}
+
+static void
+adw_tagged_entry_set_popover_visible (AdwTaggedEntry *self,
+ gboolean visible)
+{
+ visible = !!visible;
+
+ if (gtk_widget_get_visible (self->popover) == visible) {
+ return;
+ }
+
+ if (g_list_model_get_n_items (G_LIST_MODEL (self->selection)) == 0) {
+ visible = FALSE;
+ }
+
+ if (visible) {
+ if (!gtk_widget_has_focus (self->text))
+ gtk_text_grab_focus_without_selecting (GTK_TEXT (self->text));
+ gtk_single_selection_set_selected (self->selection, GTK_INVALID_LIST_POSITION);
+ gtk_popover_popup (GTK_POPOVER (self->popover));
+ } else {
+ gtk_popover_popdown (GTK_POPOVER (self->popover));
+ }
+}
+
+static void
+adw_tagged_entry_apply_selection (AdwTaggedEntry *self)
+{
+ AdwTagMatch *match = gtk_single_selection_get_selected_item (self->selection);
+ if (match == NULL) {
+ return;
+ }
+
+ AdwTag *tag = adw_tag_match_get_tag (match);
+
+ adw_tagged_entry_add_tag_internal (self, tag, FALSE);
+
+ g_signal_handlers_block_by_func (self->text, on_text_notify, self);
+ gtk_editable_delete_text (GTK_EDITABLE (self->text), 0, -1);
+ g_signal_handlers_unblock_by_func (self->text, on_text_notify, self);
+
+ adw_tagged_entry_set_popover_visible (self, FALSE);
+}
+
+static void
+on_list_row_activate (GtkListView *self,
+ guint position,
+ gpointer user_data)
+{
+ adw_tagged_entry_set_popover_visible (user_data, FALSE);
+ adw_tagged_entry_apply_selection (user_data);
+}
+
+static inline gboolean
+keyval_is_cursor_move (guint keyval)
+{
+ if (keyval == GDK_KEY_Up || keyval == GDK_KEY_KP_Up)
+ return TRUE;
+
+ if (keyval == GDK_KEY_Down || keyval == GDK_KEY_KP_Down)
+ return TRUE;
+
+ if (keyval == GDK_KEY_Page_Up || keyval == GDK_KEY_Page_Down)
+ return TRUE;
+
+ return FALSE;
+}
+
+#define PAGE_STEP 10
+
+static gboolean
+adw_tagged_entry__key_pressed (AdwTaggedEntry *self,
+ guint keyval,
+ guint keycode,
+ GdkModifierType state,
+ GtkEventControllerKey *controller)
+{
+ if (self->selection == NULL)
+ return FALSE;
+
+ if (state & (GDK_SHIFT_MASK | GDK_ALT_MASK | GDK_CONTROL_MASK))
+ return FALSE;
+
+ guint matches = g_list_model_get_n_items (G_LIST_MODEL (self->selection));
+
+ if (keyval == GDK_KEY_Return ||
+ keyval == GDK_KEY_KP_Enter ||
+ keyval == GDK_KEY_ISO_Enter) {
+ /* Shortcut: complete if there's only one match */
+ if (matches == 1) {
+ adw_tagged_entry_set_popover_visible (self, FALSE);
+ gtk_single_selection_set_selected (self->selection, 0);
+ adw_tagged_entry_apply_selection (self);
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ if (keyval == GDK_KEY_Escape) {
+ adw_tagged_entry_set_popover_visible (self, FALSE);
+ return TRUE;
+ }
+
+ guint selected = gtk_single_selection_get_selected (self->selection);
+
+ if (keyval_is_cursor_move (keyval)) {
+ if (keyval == GDK_KEY_Up || keyval == GDK_KEY_KP_Up) {
+ if (selected == 0)
+ selected = GTK_INVALID_LIST_POSITION;
+ else if (selected == GTK_INVALID_LIST_POSITION)
+ selected = matches - 1;
+ else
+ selected -= 1;
+ } else if (keyval == GDK_KEY_Down || keyval == GDK_KEY_KP_Down) {
+ if (selected == matches - 1)
+ selected = GTK_INVALID_LIST_POSITION;
+ else if (selected == GTK_INVALID_LIST_POSITION)
+ selected = 0;
+ else
+ selected += 1;
+ } else if (keyval == GDK_KEY_Page_Up) {
+ if (selected == 0)
+ selected = GTK_INVALID_LIST_POSITION;
+ else if (selected == GTK_INVALID_LIST_POSITION)
+ selected = matches - 1;
+ else if (selected >= PAGE_STEP)
+ selected -= PAGE_STEP;
+ else
+ selected = 0;
+ } else if (keyval == GDK_KEY_Page_Down) {
+ if (selected == matches - 1)
+ selected = GTK_INVALID_LIST_POSITION;
+ else if (selected == GTK_INVALID_LIST_POSITION)
+ selected = 0;
+ else if (selected + PAGE_STEP < matches)
+ selected += PAGE_STEP;
+ else
+ selected = matches - 1;
+ }
+
+ gtk_single_selection_set_selected (self->selection, selected);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+#undef PAGE_STEP
+
+static gboolean
+on_text_notify_idle (gpointer data)
+{
+ AdwTaggedEntry *self = data;
+
+ if (self->map_model == NULL)
+ goto out;
+
+ const char *text = gtk_editable_get_text (GTK_EDITABLE (self->text));
+
+ g_free (self->search);
+ self->search = g_strdup (text);
+
+ adw_tagged_entry_update_map (self);
+
+ guint matches = g_list_model_get_n_items (G_LIST_MODEL (self->selection));
+
+ adw_tagged_entry_set_popover_visible (self, matches > 0);
+
+out:
+ self->idle_match_id = 0;
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+on_text_notify (GtkText *text,
+ GParamSpec *pspec,
+ AdwTaggedEntry *self)
+{
+ if (self->match_model == NULL)
+ return;
+
+ if (self->idle_match_id == 0) {
+ self->idle_match_id = g_idle_add (on_text_notify_idle, self);
+ g_source_set_name_by_id (self->idle_match_id, "[adw] tagged entry text notify");
+ }
+}
+
+static void
+adw_tagged_entry_buildable_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const char *type)
+{
+ if (ADW_IS_TAG (child)) {
+ adw_tagged_entry_add_tag_internal (ADW_TAGGED_ENTRY (buildable),
+ ADW_TAG (child),
+ FALSE);
+ } else {
+ parent_buildable_iface->add_child (buildable, builder, child, type);
+ }
+}
+
+static void
+buildable_iface_init (GtkBuildableIface *iface)
+{
+ parent_buildable_iface = g_type_interface_peek_parent (iface);
+
+ iface->add_child = adw_tagged_entry_buildable_add_child;
+}
+
+static void
+adw_tagged_entry_set_property (GObject *gobject,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ AdwTaggedEntry *self = ADW_TAGGED_ENTRY (gobject);
+
+ if (gtk_editable_delegate_set_property (gobject, prop_id, value, pspec))
+ return;
+
+ switch (prop_id) {
+ case PROP_PLACEHOLDER_TEXT:
+ adw_tagged_entry_set_placeholder_text (self, g_value_get_string (value));
+ break;
+
+ case PROP_DELIMITER_CHARS:
+ adw_tagged_entry_set_delimiter_chars (self, g_value_get_string (value));
+ break;
+
+ case PROP_MATCH_MODEL:
+ adw_tagged_entry_set_match_model (self, g_value_get_object (value));
+ break;
+
+ case PROP_MATCH_EXPRESSION:
+ adw_tagged_entry_set_match_expression (self, gtk_value_get_expression (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+ }
+}
+
+static void
+adw_tagged_entry_get_property (GObject *gobject,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ AdwTaggedEntry *self = ADW_TAGGED_ENTRY (gobject);
+
+ if (gtk_editable_delegate_get_property (gobject, prop_id, value, pspec))
+ return;
+
+ switch (prop_id) {
+ case PROP_PLACEHOLDER_TEXT:
+ g_value_set_string (value, adw_tagged_entry_get_placeholder_text (self));
+ break;
+
+ case PROP_DELIMITER_CHARS:
+ g_value_set_string (value, self->delimiters);
+ break;
+
+ case PROP_MATCH_MODEL:
+ g_value_set_object (value, self->match_model);
+ break;
+
+ case PROP_MATCH_EXPRESSION:
+ gtk_value_set_expression (value, self->match_expression);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+ }
+}
+
+static void
+adw_tagged_entry_dispose (GObject *gobject)
+{
+ AdwTaggedEntry *self = ADW_TAGGED_ENTRY (gobject);
+
+ if (self->text != NULL)
+ gtk_editable_finish_delegate (GTK_EDITABLE (self));
+
+ adw_tagged_entry_set_match_func (self, NULL, NULL, NULL);
+ adw_tagged_entry_set_match_model (self, NULL);
+ adw_tagged_entry_set_match_expression (self, NULL);
+
+ g_clear_pointer (&self->text, gtk_widget_unparent);
+ g_clear_pointer (&self->tags_box, gtk_widget_unparent);
+ g_clear_pointer (&self->popover, gtk_widget_unparent);
+
+ g_clear_object (&self->tags);
+ g_clear_pointer (&self->widget_for_tag, g_hash_table_unref);
+
+ g_clear_pointer (&self->delimiters, g_free);
+ g_clear_pointer (&self->search, g_free);
+
+ if (self->buffer != NULL) {
+ g_string_free (self->buffer, TRUE);
+ self->buffer = NULL;
+ }
+
+ G_OBJECT_CLASS (adw_tagged_entry_parent_class)->dispose (gobject);
+}
+
+static void
+adw_tagged_entry_class_init (AdwTaggedEntryClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ gobject_class->set_property = adw_tagged_entry_set_property;
+ gobject_class->get_property = adw_tagged_entry_get_property;
+ gobject_class->dispose = adw_tagged_entry_dispose;
+
+ /**
+ * AdwTaggedEntry:placeholder-text:
+ *
+ * The text that will be displayed in the tagged entry when it is empty
+ * and unfocused.
+ *
+ * Since: 1.2
+ */
+ entry_props[PROP_PLACEHOLDER_TEXT] =
+ g_param_spec_string ("placeholder-text", NULL, NULL,
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS |
+ G_PARAM_EXPLICIT_NOTIFY);
+ /**
+ * AdwTaggedEntry:delimiter-chars:
+ *
+ * A set of characters used to denote a tag.
+ *
+ * If set to `NULL`, the tagged entry will not try to turn its contents
+ * into tags.
+ *
+ * Since: 1.2
+ */
+ entry_props[PROP_DELIMITER_CHARS] =
+ g_param_spec_string ("delimiter-chars", NULL, NULL,
+ " ,",
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS |
+ G_PARAM_EXPLICIT_NOTIFY);
+ /**
+ * AdwTaggedEntry:match-model:
+ *
+ * A list model containing all possible objects that can be matched
+ * to the contents of the tagged entry.
+ *
+ * Since: 1.2
+ */
+ entry_props[PROP_MATCH_MODEL] =
+ g_param_spec_object ("match-model", NULL, NULL,
+ G_TYPE_LIST_MODEL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS |
+ G_PARAM_EXPLICIT_NOTIFY);
+ /**
+ * AdwTaggedEntry:match-expression:
+ *
+ * An expression that can be used to match the contents of the
+ * [property@TaggedEntry:match-model] with the contents of the
+ * entry.
+ *
+ * Since: 1.2
+ */
+ entry_props[PROP_MATCH_EXPRESSION] =
+ gtk_param_spec_expression ("match-expression", NULL, NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS |
+ G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (gobject_class, N_PROPS, entry_props);
+ gtk_editable_install_properties (gobject_class, N_PROPS);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Adwaita/ui/adw-tagged-entry.ui");
+ gtk_widget_class_bind_template_child (widget_class, AdwTaggedEntry, tags_box);
+ gtk_widget_class_bind_template_child (widget_class, AdwTaggedEntry, text);
+ gtk_widget_class_bind_template_child (widget_class, AdwTaggedEntry, popover);
+ gtk_widget_class_bind_template_child (widget_class, AdwTaggedEntry, list_view);
+ gtk_widget_class_bind_template_callback (widget_class, on_text_insert_text);
+ gtk_widget_class_bind_template_callback (widget_class, on_text_delete_text);
+ gtk_widget_class_bind_template_callback (widget_class, adw_tagged_entry__key_pressed);
+ gtk_widget_class_bind_template_callback (widget_class, on_list_row_activate);
+
+ gtk_widget_class_set_css_name (widget_class, "entry");
+ gtk_widget_class_set_layout_manager_type (GTK_WIDGET_CLASS (klass), GTK_TYPE_BOX_LAYOUT);
+ gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_TEXT_BOX);
+}
+
+static void
+adw_tagged_entry_init (AdwTaggedEntry *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+ gtk_widget_add_css_class (GTK_WIDGET (self), "tagged");
+
+ gtk_editable_init_delegate (GTK_EDITABLE (self));
+
+ g_signal_connect (self->text, "notify::text", G_CALLBACK (on_text_notify), self);
+
+ self->tags = G_LIST_MODEL (g_list_store_new (ADW_TYPE_TAG));
+
+ self->widget_for_tag = g_hash_table_new (NULL, NULL);
+
+ self->delimiters = g_strdup (" ,");
+
+ adw_tagged_entry_setup_list_factory (self);
+}
+
+/**
+ * adw_tagged_entry_new:
+ *
+ * Creates a new tagged entry widget.
+ *
+ * Returns: (transfer floating): the new tagged entry widget
+ *
+ * Since: 1.2
+ */
+GtkWidget *
+adw_tagged_entry_new (void)
+{
+ return g_object_new (ADW_TYPE_TAGGED_ENTRY, NULL);
+}
+
+/**
+ * adw_tagged_entry_add_tag:
+ * @self: the tagged entry we want to update
+ * @tag: (transfer full): the tag object
+ *
+ * Adds a new @tag into the tagged entry.
+ *
+ * Since: 1.2
+ */
+void
+adw_tagged_entry_add_tag (AdwTaggedEntry *self,
+ AdwTag *tag)
+{
+ g_return_if_fail (ADW_IS_TAGGED_ENTRY (self));
+ g_return_if_fail (ADW_IS_TAG (tag));
+
+ guint n_tags = g_list_model_get_n_items (self->tags);
+ for (guint i = 0; i < n_tags; i++) {
+ g_autoptr(AdwTag) iter = g_list_model_get_item (self->tags, i);
+
+ if (iter == tag) {
+ g_critical ("Tag %p already set", tag);
+ return;
+ }
+ }
+
+ adw_tagged_entry_add_tag_internal (self, tag, TRUE);
+}
+
+/**
+ * adw_tagged_entry_remove_tag:
+ * @self: the tagged entry we want to update
+ * @tag: the tag to remove
+ *
+ * Removes the given tag from the tagged entry.
+ *
+ * Since: 1.2
+ */
+void
+adw_tagged_entry_remove_tag (AdwTaggedEntry *self,
+ AdwTag *tag)
+{
+ g_return_if_fail (ADW_IS_TAGGED_ENTRY (self));
+ g_return_if_fail (ADW_IS_TAG (tag));
+
+ GtkWidget *tag_widget = g_hash_table_lookup (self->widget_for_tag, tag);
+ if (tag_widget == NULL) {
+ g_critical ("No widget found for tag %p", tag);
+ return;
+ }
+
+ gtk_flow_box_remove (GTK_FLOW_BOX (self->tags_box), gtk_widget_get_parent (tag_widget));
+
+ guint n_tags = g_list_model_get_n_items (self->tags);
+ for (guint i = 0; i < n_tags; i++) {
+ AdwTag *iter = g_list_model_get_item (self->tags, i);
+
+ if (iter == tag) {
+ g_list_store_remove (G_LIST_STORE (self->tags), i);
+ g_object_unref (iter);
+ break;
+ }
+
+ g_object_unref (iter);
+ }
+}
+
+/**
+ * adw_tagged_entry_get_tags:
+ * @self: the tagged entry we want to query
+ *
+ * Retrieves a list model of all tags inside the tagged entry widget.
+ *
+ * Returns: (transfer full): a list model of all the tags
+ *
+ * Since: 1.2
+ */
+GListModel *
+adw_tagged_entry_get_tags (AdwTaggedEntry *self)
+{
+ g_return_val_if_fail (ADW_IS_TAGGED_ENTRY (self), NULL);
+
+ return self->tags;
+}
+
+/**
+ * adw_tagged_entry_remove_all_tags:
+ * @self: the tagged entry we want to change
+ *
+ * Removes all tags from the tagged entry widget.
+ *
+ * Since: 1.2
+ */
+void
+adw_tagged_entry_remove_all_tags (AdwTaggedEntry *self)
+{
+ g_return_if_fail (ADW_IS_TAGGED_ENTRY (self));
+
+ GtkWidget *child = gtk_widget_get_first_child (self->tags_box);
+ while (child != NULL) {
+ GtkWidget *next = gtk_widget_get_next_sibling (child);
+
+ gtk_flow_box_remove (GTK_FLOW_BOX (self->tags_box), child);
+
+ child = next;
+ }
+
+ g_list_store_remove_all (G_LIST_STORE (self->tags));
+}
+
+/**
+ * adw_tagged_entry_set_placeholder_text:
+ * @self: the tagged entry to update
+ * @text: (nullable): the placeholder text
+ *
+ * Sets text to be displayed in the tagged entry when it is empty.
+ *
+ * Since: 1.2
+ */
+void
+adw_tagged_entry_set_placeholder_text (AdwTaggedEntry *self,
+ const char *text)
+{
+ g_return_if_fail (ADW_IS_TAGGED_ENTRY (self));
+
+ gtk_text_set_placeholder_text (GTK_TEXT (self->text), text);
+ gtk_accessible_update_property (GTK_ACCESSIBLE (self),
+ GTK_ACCESSIBLE_PROPERTY_PLACEHOLDER, text,
+ -1);
+
+ g_object_notify_by_pspec (G_OBJECT (self), entry_props[PROP_PLACEHOLDER_TEXT]);
+}
+
+/**
+ * adw_tagged_entry_get_placeholder_text:
+ * @self: the tagged entry to query
+ *
+ * Retrieves the placeholder text of the tagged entry.
+ *
+ * Returns: (transfer none) (nullable): the placeholder text
+ *
+ * Since: 1.2
+ */
+const char *
+adw_tagged_entry_get_placeholder_text (AdwTaggedEntry *self)
+{
+ g_return_val_if_fail (ADW_IS_TAGGED_ENTRY (self), NULL);
+
+ return gtk_text_get_placeholder_text (GTK_TEXT (self->text));
+}
+
+/**
+ * adw_tagged_entry_get_delimiter_chars:
+ * @self: a tagged entry
+ *
+ * Retrieves the characters that act as delimiters for automatic tag
+ * insertion.
+ *
+ * Returns: (transfer none) (nullable): the delimiter characters
+ *
+ * Since: 1.2
+ */
+const char *
+adw_tagged_entry_get_delimiter_chars (AdwTaggedEntry *self)
+{
+ g_return_val_if_fail (ADW_IS_TAGGED_ENTRY (self), NULL);
+
+ return self->delimiters;
+}
+
+/**
+ * adw_tagged_entry_set_delimiter_chars:
+ * @self: a tagged entry
+ * @delimiters: (nullable): all the delimiter characters
+ *
+ * Sets the characters that act as a delimiter for automatic tag
+ * insertion.
+ *
+ * Whenever a matching character is inserted in the tagged entry, the
+ * current contents of the entry are replaced by a tag.
+ *
+ * If @delimiters is `NULL` then automatic tag insertion is disabled.
+ *
+ * Since: 1.2
+ */
+void
+adw_tagged_entry_set_delimiter_chars (AdwTaggedEntry *self,
+ const char *delimiters)
+{
+ g_return_if_fail (ADW_IS_TAGGED_ENTRY (self));
+
+ if (g_strcmp0 (self->delimiters, delimiters) == 0)
+ return;
+
+ g_free (self->delimiters);
+ self->delimiters = g_strdup (delimiters);
+
+ g_object_notify_by_pspec (G_OBJECT (self), entry_props[PROP_DELIMITER_CHARS]);
+}
+
+static AdwTag *
+default_match_func (AdwTaggedEntry *self,
+ const char *text,
+ gpointer item,
+ gpointer user_data G_GNUC_UNUSED)
+{
+ AdwTagMatch *match = item;
+ char *tmp1, *tmp2, *tmp3, *tmp4;
+ AdwTag *res = NULL;
+
+ tmp1 = g_utf8_normalize (adw_tag_match_get_string (match), -1, G_NORMALIZE_ALL);
+ tmp2 = g_utf8_casefold (tmp1, -1);
+
+ tmp3 = g_utf8_normalize (text, -1, G_NORMALIZE_ALL);
+ tmp4 = g_utf8_casefold (tmp3, -1);
+
+ if (g_str_has_prefix (tmp2, tmp4)) {
+ res = adw_tag_new ();
+
+ adw_tag_set_label (res, adw_tag_match_get_string (match));
+ adw_tag_set_show_close (res, TRUE);
+ }
+
+ g_free (tmp1);
+ g_free (tmp2);
+ g_free (tmp3);
+ g_free (tmp4);
+
+ return res;
+}
+
+static gboolean
+filter_func (gpointer item,
+ gpointer user_data)
+{
+ return adw_tag_match_get_tag (item) != NULL;
+}
+
+static gpointer
+map_func (gpointer item,
+ gpointer user_data)
+{
+ AdwTaggedEntry *self = user_data;
+ GValue value = G_VALUE_INIT;
+
+ if (self->match_expression != NULL)
+ gtk_expression_evaluate (self->match_expression, item, &value);
+ else if (GTK_IS_STRING_OBJECT (item))
+ g_object_get_property (item, "string", &value);
+ else {
+ g_critical ("Missing match expression for tagged entry %p, and the "
+ "match model is not a GtkStringList",
+ self);
+ g_value_set_string (&value, "No value");
+ }
+
+ AdwTagMatch *obj = adw_tag_match_new (item, g_value_get_string (&value));
+
+ g_value_unset (&value);
+
+ if (self->search != NULL) {
+ AdwTag *tag = NULL;
+
+ if (self->match_func == NULL)
+ tag = default_match_func (self, self->search, obj, NULL);
+ else
+ tag = self->match_func (self,
+ self->search,
+ adw_tag_match_get_item (obj),
+ self->match_func_data);
+
+ adw_tag_match_set_tag (obj, tag);
+ if (tag != NULL)
+ g_object_unref (tag);
+ }
+
+ return obj;
+}
+
+static void
+adw_tagged_entry_update_map (AdwTaggedEntry *self)
+{
+ gtk_map_list_model_set_map_func (self->map_model, map_func, self, NULL);
+}
+
+/**
+ * adw_tagged_entry_set_match_model:
+ * @self: a tagged entry
+ * @model: (nullable): a list model of potential matches
+ *
+ * Sets the matching model for the tagged entry.
+ *
+ * Every time a new tag is entered in the tagged entry, it is
+ * compared to the contents of the model.
+ *
+ * The comparison is automatic if @model is a [class@Gtk.StringModel],
+ * otherwise you need to call [method@TaggedEntry.set_match_func] and
+ * provide a match function.
+ *
+ * Since: 1.2
+ */
+void
+adw_tagged_entry_set_match_model (AdwTaggedEntry *self,
+ GListModel *model)
+{
+ g_return_if_fail (ADW_IS_TAGGED_ENTRY (self));
+ g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model));
+
+ if (!g_set_object (&self->match_model, model))
+ return;
+
+ if (model == NULL) {
+ gtk_list_view_set_model (GTK_LIST_VIEW (self->list_view), NULL);
+ g_clear_object (&self->selection);
+ g_clear_object (&self->map_model);
+ g_clear_object (&self->filter);
+ } else {
+ /* 1. We map the contents of the given model to a model of AdwTagMatch objects */
+ GtkMapListModel *map_model =
+ gtk_map_list_model_new (g_object_ref (model), map_func, self, NULL);
+ g_set_object (&self->map_model, map_model);
+
+ /* 2. We set up a custom filter function to eliminate non-matching elements */
+ GtkCustomFilter *filter =
+ gtk_custom_filter_new (filter_func, self, NULL);
+ GtkFilterListModel *filter_model =
+ gtk_filter_list_model_new (G_LIST_MODEL (self->map_model), GTK_FILTER (filter));
+ g_set_object (&self->filter, (GtkFilter *) filter);
+
+ adw_tagged_entry_update_map (self);
+
+ /* 3. Sort alphabetically on the string property of the tag match object */
+ GtkStringSorter *sorter =
+ gtk_string_sorter_new (gtk_property_expression_new (ADW_TYPE_TAG_MATCH, NULL, "string"));
+ gtk_string_sorter_set_ignore_case (sorter, TRUE);
+ GtkSortListModel *sort_model =
+ gtk_sort_list_model_new (G_LIST_MODEL (filter_model), GTK_SORTER (sorter));
+
+ /* 4. Create a selection model for the list view */
+ GtkSingleSelection *selection =
+ gtk_single_selection_new (G_LIST_MODEL (sort_model));
+ gtk_single_selection_set_autoselect (selection, FALSE);
+ gtk_single_selection_set_can_unselect (selection, TRUE);
+ gtk_single_selection_set_selected (selection, GTK_INVALID_LIST_POSITION);
+ g_set_object (&self->selection, selection);
+
+ /* 5. Assign the selection model to the list view */
+ gtk_list_view_set_model (GTK_LIST_VIEW (self->list_view), GTK_SELECTION_MODEL (selection));
+ g_object_unref (selection);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), entry_props[PROP_MATCH_MODEL]);
+}
+
+/**
+ * adw_tagged_entry_get_match_model:
+ * @self: a tagged entry
+ *
+ * Retrieves the model set using [method@TaggedEntry.set_match_model].
+ *
+ * Returns: (transfer none) (nullable): the model with all the possible tags
+ *
+ * Since: 1.2
+ */
+GListModel *
+adw_tagged_entry_get_match_model (AdwTaggedEntry *self)
+{
+ g_return_val_if_fail (ADW_IS_TAGGED_ENTRY (self), NULL);
+
+ return self->match_model;
+}
+
+/**
+ * adw_tagged_entry_set_match_expression:
+ * @self: a tagged entry
+ * @expression: (nullable): the expression used for matching
+ *
+ * Sets the expression used for matching tags.
+ *
+ * Since: 1.2
+ */
+void
+adw_tagged_entry_set_match_expression (AdwTaggedEntry *self,
+ GtkExpression *expression)
+{
+ g_return_if_fail (ADW_IS_TAGGED_ENTRY (self));
+ g_return_if_fail (expression == NULL || GTK_IS_EXPRESSION (expression));
+
+ if (self->match_expression == expression)
+ return;
+
+ g_clear_pointer (&self->match_expression, gtk_expression_unref);
+ self->match_expression = expression;
+ if (self->match_expression != NULL) {
+ gtk_expression_ref (self->match_expression);
+ }
+
+ adw_tagged_entry_update_map (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), entry_props[PROP_MATCH_EXPRESSION]);
+}
+
+/**
+ * adw_tagged_entry_get_match_expression:
+ * @self: a tagged entry
+ *
+ * Retrieves the matching expression set using
+ * [method@TaggedEntry.set_match_expression].
+ *
+ * Returns: (transfer none) (nullable): the matching expression
+ *
+ * Since: 1.2
+ */
+GtkExpression *
+adw_tagged_entry_get_match_expression (AdwTaggedEntry *self)
+{
+ g_return_val_if_fail (ADW_IS_TAGGED_ENTRY (self), NULL);
+
+ return self->match_expression;
+}
+
+/**
+ * adw_tagged_entry_set_match_func:
+ * @self: a tagged entry
+ * @match_func: (nullable) (scope notified) (closure user_data): the matching function for the entry
+ * @user_data: (nullable): data to be passed to the matching function
+ * @notify: (nullable): function to be called when the matching function is
+ * removed from the tagged entry
+ *
+ * Sets the matching function for the tagged entry.
+ *
+ * The default matching function will try to compare the contents of the
+ * entry with each item in the model set using [method@TaggedEntry.set_match_model],
+ * and will create a [class@Tag] instance in case of a match.
+ *
+ * You can use this function to control the matching between the entry's text
+ * and the data inside your match model, as well as the creation of the tag
+ * object.
+ *
+ * Since: 1.2
+ */
+void
+adw_tagged_entry_set_match_func (AdwTaggedEntry *self,
+ AdwTaggedEntryMatchFunc match_func,
+ gpointer user_data,
+ GDestroyNotify notify)
+{
+ g_return_if_fail (ADW_IS_TAGGED_ENTRY (self));
+
+ if (self->match_func_notify != NULL)
+ self->match_func_notify (self->match_func_data);
+
+ self->match_func = match_func;
+ self->match_func_data = user_data;
+ self->match_func_notify = notify;
+}
+
+/* }}} */
diff --git a/src/adw-tagged-entry.h b/src/adw-tagged-entry.h
new file mode 100644
index 00000000..5e35e982
--- /dev/null
+++ b/src/adw-tagged-entry.h
@@ -0,0 +1,93 @@
+/* adw-tagged-entry.h: Tagged entry widget
+ *
+ * SPDX-FileCopyrightText: 2022 Emmanuele Bassi
+ * SPDX-FileCopyrightText: 2019 Matthias Clasen
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only <adwaita.h> can be included directly."
+#endif
+
+#include "adw-version.h"
+
+#include "adw-tag.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_TAGGED_ENTRY (adw_tagged_entry_get_type())
+
+ADW_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (AdwTaggedEntry, adw_tagged_entry, ADW, TAGGED_ENTRY, GtkWidget)
+
+ADW_AVAILABLE_IN_ALL
+GtkWidget *adw_tagged_entry_new (void);
+
+ADW_AVAILABLE_IN_ALL
+void adw_tagged_entry_add_tag (AdwTaggedEntry *self,
+ AdwTag *tag);
+ADW_AVAILABLE_IN_ALL
+void adw_tagged_entry_remove_tag (AdwTaggedEntry *self,
+ AdwTag *tag);
+ADW_AVAILABLE_IN_ALL
+GListModel *adw_tagged_entry_get_tags (AdwTaggedEntry *self);
+ADW_AVAILABLE_IN_ALL
+void adw_tagged_entry_remove_all_tags (AdwTaggedEntry *self);
+
+ADW_AVAILABLE_IN_ALL
+const char *adw_tagged_entry_get_placeholder_text (AdwTaggedEntry *self);
+ADW_AVAILABLE_IN_ALL
+void adw_tagged_entry_set_placeholder_text (AdwTaggedEntry *self,
+ const char *text);
+
+ADW_AVAILABLE_IN_ALL
+const char *adw_tagged_entry_get_delimiter_chars (AdwTaggedEntry *self);
+ADW_AVAILABLE_IN_ALL
+void adw_tagged_entry_set_delimiter_chars (AdwTaggedEntry *self,
+ const char *delimiters);
+
+ADW_AVAILABLE_IN_ALL
+GListModel *adw_tagged_entry_get_match_model (AdwTaggedEntry *self);
+ADW_AVAILABLE_IN_ALL
+void adw_tagged_entry_set_match_model (AdwTaggedEntry *self,
+ GListModel *model);
+
+ADW_AVAILABLE_IN_ALL
+GtkExpression *adw_tagged_entry_get_match_expression (AdwTaggedEntry *self);
+ADW_AVAILABLE_IN_ALL
+void adw_tagged_entry_set_match_expression (AdwTaggedEntry *self,
+ GtkExpression *expression);
+
+/**
+ * AdwTaggedEntryMatchFunc:
+ * @self: the tagged entry
+ * @text: the text in the tagged entry
+ * @item: (type GObject): the item from the match model
+ *
+ * Matches the given text from the tagged entry with an item from the
+ * match model.
+ *
+ * If @text matches @item, this function returns the [class@Tag] that
+ * should be added to the tagged entry.
+ *
+ * Returns: (nullable): the tag that matches the item, if any
+ *
+ * Since: 1.2
+ */
+typedef AdwTag *(* AdwTaggedEntryMatchFunc) (AdwTaggedEntry *self,
+ const char *text,
+ gpointer item,
+ gpointer user_data);
+
+ADW_AVAILABLE_IN_ALL
+void adw_tagged_entry_set_match_func (AdwTaggedEntry *self,
+ AdwTaggedEntryMatchFunc match_func,
+ gpointer user_data,
+ GDestroyNotify notify);
+
+G_END_DECLS
diff --git a/src/adw-tagged-entry.ui b/src/adw-tagged-entry.ui
new file mode 100644
index 00000000..f84e6206
--- /dev/null
+++ b/src/adw-tagged-entry.ui
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface domain="libadwaita">
+ <requires lib="gtk" version="4.0"/>
+ <template class="AdwTaggedEntry">
+ <child>
+ <object class="GtkFlowBox" id="tags_box">
+ <property name="min-children-per-line">4</property>
+ <style>
+ <class name="tags"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkText" id="text">
+ <property name="hexpand">true</property>
+ <property name="max-width-chars">12</property>
+ <property name="width-chars">12</property>
+ <signal name="insert-text" handler="on_text_insert_text" object="AdwTaggedEntry" swapped="no"/>
+ <signal name="delete-text" handler="on_text_delete_text" object="AdwTaggedEntry" swapped="no"/>
+ <child>
+ <object class="GtkEventControllerKey">
+ <property name="name">adw-tagged-entry-key</property>
+ <signal name="key-pressed" handler="adw_tagged_entry__key_pressed" object="AdwTaggedEntry"
swapped="yes"/>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="GtkPopover" id="popover">
+ <property name="position">bottom</property>
+ <property name="autohide">false</property>
+ <property name="has-arrow">false</property>
+ <property name="halign">start</property>
+ <property name="child">
+ <object class="GtkScrolledWindow">
+ <property name="hscrollbar-policy">never</property>
+ <property name="vscrollbar-policy">automatic</property>
+ <property name="max-content-height">400</property>
+ <property name="propagate-natural-height">true</property>
+ <property name="child">
+ <object class="GtkListView" id="list_view">
+ <property name="single-click-activate">true</property>
+ <signal name="activate" handler="on_list_row_activate" object="AdwTaggedEntry" swapped="no"/>
+ </object>
+ </property>
+ </object>
+ </property>
+ <style>
+ <class name="menu"/>
+ </style>
+ </object>
+ </child>
+
+ </template>
+</interface>
diff --git a/src/adwaita.gresources.xml b/src/adwaita.gresources.xml
index 21524a9e..43adc344 100644
--- a/src/adwaita.gresources.xml
+++ b/src/adwaita.gresources.xml
@@ -18,6 +18,8 @@
<file preprocess="xml-stripblanks">adw-status-page.ui</file>
<file preprocess="xml-stripblanks">adw-tab.ui</file>
<file preprocess="xml-stripblanks">adw-tab-bar.ui</file>
+ <file preprocess="xml-stripblanks">adw-tag-widget.ui</file>
+ <file preprocess="xml-stripblanks">adw-tagged-entry.ui</file>
<file preprocess="xml-stripblanks">adw-toast-widget.ui</file>
<file preprocess="xml-stripblanks">adw-view-switcher-bar.ui</file>
<file preprocess="xml-stripblanks">adw-view-switcher-button.ui</file>
diff --git a/src/adwaita.h b/src/adwaita.h
index 34eed7de..0cfd0281 100644
--- a/src/adwaita.h
+++ b/src/adwaita.h
@@ -62,6 +62,7 @@ G_BEGIN_DECLS
#include "adw-tab-bar.h"
#include "adw-tab-view.h"
#include "adw-tag.h"
+#include "adw-tagged-entry.h"
#include "adw-timed-animation.h"
#include "adw-toast-overlay.h"
#include "adw-toast.h"
diff --git a/src/meson.build b/src/meson.build
index 6c1e5bee..7be06b2a 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -121,6 +121,7 @@ src_headers = [
'adw-tab-bar.h',
'adw-tab-view.h',
'adw-tag.h',
+ 'adw-tagged-entry.h',
'adw-timed-animation.h',
'adw-toast.h',
'adw-toast-overlay.h',
@@ -182,7 +183,7 @@ src_sources = [
'adw-tab-bar.c',
'adw-tab-view.c',
'adw-tag.c',
- 'adw-tag-widget.c',
+ 'adw-tagged-entry.c',
'adw-timed-animation.c',
'adw-toast.c',
'adw-toast-overlay.c',
@@ -206,6 +207,8 @@ libadwaita_private_sources += files([
'adw-shadow-helper.c',
'adw-tab.c',
'adw-tab-box.c',
+ 'adw-tag-match.c',
+ 'adw-tag-widget.c',
'adw-toast-widget.c',
'adw-view-switcher-button.c',
'adw-widget-utils.c',
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]