[libadwaita/ebassi/tagged-entry: 31/33] Add AdwTaggedEntry




commit de7d42831dad0b7fa57e94b7d884126d967504a5
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 allows defining
    "tags", additional UI elements that describe the contents
    or a portion of the contents of an entry.
    
    Tagged entries can be used for showing refinements to a
    search entry, specializations to a form, or rich UI controls
    to replace textual data.
    
    Current existing users of a similar UI are:
    
    - GdTaggedEntry, as used by Nautilus, Epiphany, and Photos
    - NSTokenField, as used by AppKit
    - Input Chips, as used by Android

 src/adw-tagged-entry.c | 405 +++++++++++++++++++++++++++++++++++++++++++++++++
 src/adw-tagged-entry.h |  48 ++++++
 src/adwaita.h          |   1 +
 src/meson.build        |   2 +
 4 files changed, 456 insertions(+)
---
diff --git a/src/adw-tagged-entry.c b/src/adw-tagged-entry.c
new file mode 100644
index 00000000..d7b9d8ce
--- /dev/null
+++ b/src/adw-tagged-entry.c
@@ -0,0 +1,405 @@
+/* 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-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 `AdwTag`; for
+ * instance, the following definition:
+ *
+ * ```xml
+ * <object class="AdwTaggedEntry">
+ *   <child>
+ *     <object class="AdwTag">
+ *       <property name="name">first-tag</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`.
+ */
+struct _AdwTaggedEntry
+{
+  GtkWidget parent_instance;
+
+  GtkWidget *tags_box;
+  GtkWidget *text;
+
+  GListModel *tags;
+
+  GHashTable *widget_for_tag;
+};
+
+enum
+{
+  PROP_PLACEHOLDER_TEXT = 1,
+  N_PROPS
+};
+
+static void buildable_iface_init (GtkBuildableIface *iface);
+static void editable_iface_init (GtkEditableInterface *iface);
+
+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
+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_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;
+
+  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;
+
+  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));
+
+  g_clear_pointer (&self->text, gtk_widget_unparent);
+  g_clear_pointer (&self->tags_box, gtk_widget_unparent);
+
+  g_clear_object (&self->tags);
+  g_clear_pointer (&self->widget_for_tag, g_hash_table_unref);
+
+  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.
+   */
+  entry_props[PROP_PLACEHOLDER_TEXT] =
+    g_param_spec_string ("placeholder-text", NULL, NULL,
+                         NULL,
+                         G_PARAM_READWRITE |
+                         G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (gobject_class, N_PROPS, entry_props);
+  gtk_editable_install_properties (gobject_class, N_PROPS);
+
+  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_add_css_class (GTK_WIDGET (self), "tagged");
+
+  self->tags_box = gtk_flow_box_new ();
+  gtk_flow_box_set_min_children_per_line (GTK_FLOW_BOX (self->tags_box), 4);
+  gtk_widget_set_parent (self->tags_box, GTK_WIDGET (self));
+  gtk_widget_add_css_class (self->tags_box, "tags");
+
+  self->text = gtk_text_new ();
+  gtk_widget_set_hexpand (self->text, TRUE);
+  gtk_widget_set_vexpand (self->text, TRUE);
+  gtk_widget_set_parent (self->text, GTK_WIDGET (self));
+  gtk_editable_init_delegate (GTK_EDITABLE (self));
+  gtk_editable_set_width_chars (GTK_EDITABLE (self->text), 12);
+  gtk_editable_set_max_width_chars (GTK_EDITABLE (self->text), 12);
+
+  self->tags = G_LIST_MODEL (g_list_store_new (ADW_TYPE_TAG));
+
+  self->widget_for_tag = g_hash_table_new (NULL, NULL);
+}
+
+/**
+ * adw_tagged_entry_new:
+ *
+ * Creates a new tagged entry widget.
+ *
+ * Returns: (transfer floating): the new tagged entry widget
+ */
+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.
+ *
+ * Returns: (transfer none): the tag object
+ *
+ * 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.
+ */
+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++) {
+    g_autoptr(AdwTag) iter = g_list_model_get_item (self->tags, i);
+
+    if (iter == tag) {
+      g_list_store_remove (G_LIST_STORE (self->tags), i);
+      break;
+    }
+  }
+}
+
+/**
+ * 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
+ */
+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.
+ */
+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.
+ */
+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
+ */
+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));
+}
+
+/* }}} */
diff --git a/src/adw-tagged-entry.h b/src/adw-tagged-entry.h
new file mode 100644
index 00000000..3115c529
--- /dev/null
+++ b/src/adw-tagged-entry.h
@@ -0,0 +1,48 @@
+/* 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);
+
+G_END_DECLS
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 959de158..2aa41cd5 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',
@@ -192,6 +193,7 @@ src_sources = [
   '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',


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]