[libadwaita/ebassi/tagged-entry: 31/33] Add AdwTaggedEntry
- From: Emmanuele Bassi <ebassi src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [libadwaita/ebassi/tagged-entry: 31/33] Add AdwTaggedEntry
- Date: Fri, 4 Mar 2022 19:04:54 +0000 (UTC)
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]