[libadwaita/ebassi/tagged-entry: 14/20] Add AdwTaggedEntry




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]