summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthias Clasen <mclasen@redhat.com>2020-06-25 13:57:17 -0400
committerMatthias Clasen <mclasen@redhat.com>2020-11-11 15:54:43 -0500
commita2897e1868e8a79fa8c14cec5cf5351601dfaab0 (patch)
treef7458e5b5fab5a8b4a4b59d3bc99a9fd44ffa498
parent15172ebdb0816a73dcaec1a78f61031496082715 (diff)
downloadgtk+-suggestion-entry-demo2.tar.gz
gtk-demo: Add suggestion entry demossuggestion-entry-demo2
Add a possible replacement for GtkEntryCompletion as a demo. Move the Dropdowns demo to Lists/Selections, and make it show both GtkDropDown and the suggestion entry, with some variations.
-rw-r--r--demos/gtk-demo/demo.gresource.xml5
-rw-r--r--demos/gtk-demo/dropdown.c253
-rw-r--r--demos/gtk-demo/meson.build5
-rw-r--r--demos/gtk-demo/suggestionentry.c1215
-rw-r--r--demos/gtk-demo/suggestionentry.css28
-rw-r--r--demos/gtk-demo/suggestionentry.h66
6 files changed, 1549 insertions, 23 deletions
diff --git a/demos/gtk-demo/demo.gresource.xml b/demos/gtk-demo/demo.gresource.xml
index a6e6e48a8f..72933ebef6 100644
--- a/demos/gtk-demo/demo.gresource.xml
+++ b/demos/gtk-demo/demo.gresource.xml
@@ -43,6 +43,11 @@
<file>cssview.css</file>
<file>reset.css</file>
</gresource>
+ <gresource prefix="/dropdown">
+ <file>suggestionentry.h</file>
+ <file>suggestionentry.c</file>
+ <file>suggestionentry.css</file>
+ </gresource>
<gresource prefix="/theming_style_classes">
<file>theming.ui</file>
</gresource>
diff --git a/demos/gtk-demo/dropdown.c b/demos/gtk-demo/dropdown.c
index 75b2415167..aea35f7bcb 100644
--- a/demos/gtk-demo/dropdown.c
+++ b/demos/gtk-demo/dropdown.c
@@ -1,17 +1,16 @@
-/* Drop Downs
+/* Lists/Selections
*
* The GtkDropDown widget is a modern alternative to GtkComboBox.
* It uses list models instead of tree models, and the content is
* displayed using widgets instead of cell renderers.
*
- * The examples here demonstrate how to use different kinds of
- * list models with GtkDropDown, how to use search and how to
- * display the selected item differently from the presentation
- * in the popup.
+ * This example also shows a custom widget that can replace
+ * GtkEntryCompletion or GtkComboBoxText. It is not currently
+ * part of GTK.
*/
#include <gtk/gtk.h>
-
+#include "suggestionentry.h"
#define STRING_TYPE_HOLDER (string_holder_get_type ())
G_DECLARE_FINAL_TYPE (StringHolder, string_holder, STRING, HOLDER, GObject)
@@ -273,13 +272,110 @@ get_title (gpointer item)
return g_strdup (STRING_HOLDER (item)->title);
}
+static char *
+get_file_name (gpointer item)
+{
+ return g_strdup (g_file_info_get_display_name (G_FILE_INFO (item)));
+}
+
+static void
+setup_item (GtkSignalListItemFactory *factory,
+ GtkListItem *item)
+{
+ GtkWidget *box;
+ GtkWidget *icon;
+ GtkWidget *label;
+
+ box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
+ icon = gtk_image_new ();
+ label = gtk_label_new ("");
+ gtk_label_set_xalign (GTK_LABEL (label), 0);
+ gtk_box_append (GTK_BOX (box), icon);
+ gtk_box_append (GTK_BOX (box), label);
+ gtk_list_item_set_child (item, box);
+}
+
+static void
+bind_item (GtkSignalListItemFactory *factory,
+ GtkListItem *item)
+{
+ MatchObject *match = MATCH_OBJECT (gtk_list_item_get_item (item));
+ GFileInfo *info = G_FILE_INFO (match_object_get_item (match));
+ GtkWidget *box = gtk_list_item_get_child (item);
+ GtkWidget *icon = gtk_widget_get_first_child (box);
+ GtkWidget *label = gtk_widget_get_last_child (box);
+
+ gtk_image_set_from_gicon (GTK_IMAGE (icon), g_file_info_get_icon (info));
+ gtk_label_set_label (GTK_LABEL (label), g_file_info_get_display_name (info));
+}
+
+static void
+setup_highlight_item (GtkSignalListItemFactory *factory,
+ GtkListItem *item)
+{
+ GtkWidget *label;
+
+ label = gtk_label_new ("");
+ gtk_label_set_xalign (GTK_LABEL (label), 0);
+ gtk_list_item_set_child (item, label);
+}
+
+static void
+bind_highlight_item (GtkSignalListItemFactory *factory,
+ GtkListItem *item)
+{
+ MatchObject *obj;
+ GtkWidget *label;
+ PangoAttrList *attrs;
+ PangoAttribute *attr;
+ const char *str;
+
+ obj = MATCH_OBJECT (gtk_list_item_get_item (item));
+ label = gtk_list_item_get_child (item);
+
+ str = match_object_get_string (obj);
+
+ gtk_label_set_label (GTK_LABEL (label), str);
+ attrs = pango_attr_list_new ();
+ attr = pango_attr_weight_new (PANGO_WEIGHT_BOLD);
+ attr->start_index = match_object_get_match_start (obj);
+ attr->end_index = match_object_get_match_end (obj);
+ pango_attr_list_insert (attrs, attr);
+ gtk_label_set_attributes (GTK_LABEL (label), attrs);
+ pango_attr_list_unref (attrs);
+}
+
+static void
+match_func (MatchObject *obj,
+ const char *search,
+ gpointer user_data)
+{
+ char *tmp1, *tmp2;
+ char *p;
+
+ tmp1 = g_utf8_normalize (match_object_get_string (obj), -1, G_NORMALIZE_ALL);
+ tmp2 = g_utf8_normalize (search, -1, G_NORMALIZE_ALL);
+
+ if ((p = strstr (tmp1, tmp2)) != NULL)
+ match_object_set_match (obj,
+ p - tmp1,
+ (p - tmp1) + g_utf8_strlen (search, -1),
+ 1);
+ else
+ match_object_set_match (obj, 0, 0, 0);
+
+ g_free (tmp1);
+ g_free (tmp2);
+}
+
GtkWidget *
do_dropdown (GtkWidget *do_widget)
{
static GtkWidget *window = NULL;
- GtkWidget *button, *box, *spin, *check;
+ GtkWidget *button, *box, *spin, *check, *hbox, *label, *entry;
GListModel *model;
GtkExpression *expression;
+ GtkListItemFactory *factory;
const char * const times[] = { "1 minute", "2 minutes", "5 minutes", "20 minutes", NULL };
const char * const many_times[] = {
"1 minute", "2 minutes", "5 minutes", "10 minutes", "15 minutes", "20 minutes",
@@ -292,22 +388,49 @@ do_dropdown (GtkWidget *do_widget)
const char * const device_descriptions[] = {
"Built-in Audio", "Built-in audio", "Thinkpad Tunderbolt 3 Dock USB Audio", "Thinkpad Tunderbolt 3 Dock USB Audio", NULL
};
+ char *cwd;
+ GFile *file;
+ GListModel *dir;
+ GtkStringList *strings;
if (!window)
{
window = gtk_window_new ();
gtk_window_set_display (GTK_WINDOW (window),
gtk_widget_get_display (do_widget));
- gtk_window_set_title (GTK_WINDOW (window), "Drop Downs");
+ gtk_window_set_title (GTK_WINDOW (window), "Selections");
gtk_window_set_resizable (GTK_WINDOW (window), FALSE);
g_object_add_weak_pointer (G_OBJECT (window), (gpointer *)&window);
+ hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 20);
+
+ gtk_widget_set_margin_start (hbox, 20);
+ gtk_widget_set_margin_end (hbox, 20);
+ gtk_widget_set_margin_top (hbox, 20);
+ gtk_widget_set_margin_bottom (hbox, 20);
+ gtk_window_set_child (GTK_WINDOW (window), hbox);
+
box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
- gtk_widget_set_margin_start (box, 10);
- gtk_widget_set_margin_end (box, 10);
- gtk_widget_set_margin_top (box, 10);
- gtk_widget_set_margin_bottom (box, 10);
- gtk_window_set_child (GTK_WINDOW (window), box);
+ gtk_box_append (GTK_BOX (hbox), box);
+
+ label = gtk_label_new ("Dropdowns");
+ gtk_widget_add_css_class (label, "title-4");
+ gtk_box_append (GTK_BOX (box), label);
+
+ /* A basic dropdown */
+ button = drop_down_new_from_strings (times, NULL, NULL);
+ gtk_box_append (GTK_BOX (box), button);
+
+ /* A dropdown using an expression to obtain strings */
+ button = drop_down_new_from_strings (many_times, NULL, NULL);
+ gtk_drop_down_set_enable_search (GTK_DROP_DOWN (button), TRUE);
+ expression = gtk_cclosure_expression_new (G_TYPE_STRING, NULL,
+ 0, NULL,
+ (GCallback)get_title,
+ NULL, NULL);
+ gtk_drop_down_set_expression (GTK_DROP_DOWN (button), expression);
+ gtk_expression_unref (expression);
+ gtk_box_append (GTK_BOX (box), button);
button = gtk_drop_down_new (NULL, NULL);
@@ -325,30 +448,118 @@ do_dropdown (GtkWidget *do_widget)
spin = gtk_spin_button_new_with_range (-1, g_list_model_get_n_items (G_LIST_MODEL (model)), 1);
gtk_widget_set_halign (spin, GTK_ALIGN_START);
+ gtk_widget_set_margin_start (spin, 20);
g_object_bind_property (button, "selected", spin, "value", G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
gtk_box_append (GTK_BOX (box), spin);
check = gtk_check_button_new_with_label ("Enable search");
+ gtk_widget_set_margin_start (check, 20);
g_object_bind_property (button, "enable-search", check, "active", G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
gtk_box_append (GTK_BOX (box), check);
g_object_unref (model);
- button = drop_down_new_from_strings (times, NULL, NULL);
+ /* A dropdown with a separate list factory */
+ button = drop_down_new_from_strings (device_titles, device_icons, device_descriptions);
gtk_box_append (GTK_BOX (box), button);
- button = drop_down_new_from_strings (many_times, NULL, NULL);
- gtk_drop_down_set_enable_search (GTK_DROP_DOWN (button), TRUE);
+ gtk_box_append (GTK_BOX (hbox), gtk_separator_new (GTK_ORIENTATION_VERTICAL));
+
+ box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10);
+ gtk_box_append (GTK_BOX (hbox), box);
+
+ label = gtk_label_new ("Suggestions");
+ gtk_widget_add_css_class (label, "title-4");
+ gtk_box_append (GTK_BOX (box), label);
+
+ /* A basic suggestion entry */
+ entry = suggestion_entry_new ();
+ g_object_set (entry, "placeholder-text", "Words with T or G…", NULL);
+ strings = gtk_string_list_new ((const char *[]){
+ "GNOME",
+ "gnominious",
+ "Gnomonic projection",
+ "total",
+ "totally",
+ "toto",
+ "tottery",
+ "totterer",
+ "Totten trust",
+ "totipotent",
+ "totipotency",
+ "totemism",
+ "totem pole",
+ "Totara",
+ "totalizer",
+ "totalizator",
+ "totalitarianism",
+ "total parenteral nutrition",
+ "total hysterectomy",
+ "total eclipse",
+ "Totipresence",
+ "Totipalmi",
+ "Tomboy",
+ "zombie",
+ NULL});
+ suggestion_entry_set_model (SUGGESTION_ENTRY (entry), G_LIST_MODEL (strings));
+ g_object_unref (strings);
+
+ gtk_box_append (GTK_BOX (box), entry);
+
+ /* A suggestion entry using a custom model, and no filtering */
+ entry = suggestion_entry_new ();
+
+ cwd = g_get_current_dir ();
+ file = g_file_new_for_path (cwd);
+ dir = G_LIST_MODEL (gtk_directory_list_new ("standard::display-name,standard::content-type,standard::icon,standard::size", file));
+ suggestion_entry_set_model (SUGGESTION_ENTRY (entry), dir);
+ g_object_unref (dir);
+ g_object_unref (file);
+ g_free (cwd);
+
expression = gtk_cclosure_expression_new (G_TYPE_STRING, NULL,
0, NULL,
- (GCallback)get_title,
+ (GCallback)get_file_name,
NULL, NULL);
- gtk_drop_down_set_expression (GTK_DROP_DOWN (button), expression);
+ suggestion_entry_set_expression (SUGGESTION_ENTRY (entry), expression);
gtk_expression_unref (expression);
- gtk_box_append (GTK_BOX (box), button);
- button = drop_down_new_from_strings (device_titles, device_icons, device_descriptions);
- gtk_box_append (GTK_BOX (box), button);
+ factory = gtk_signal_list_item_factory_new ();
+ g_signal_connect (factory, "setup", G_CALLBACK (setup_item), NULL);
+ g_signal_connect (factory, "bind", G_CALLBACK (bind_item), NULL);
+
+ suggestion_entry_set_factory (SUGGESTION_ENTRY (entry), factory);
+ g_object_unref (factory);
+
+ suggestion_entry_set_use_filter (SUGGESTION_ENTRY (entry), FALSE);
+ suggestion_entry_set_show_arrow (SUGGESTION_ENTRY (entry), TRUE);
+
+ gtk_box_append (GTK_BOX (box), entry);
+
+ /* A suggestion entry with match highlighting */
+ entry = suggestion_entry_new ();
+ g_object_set (entry, "placeholder-text", "Destination", NULL);
+
+ strings = gtk_string_list_new ((const char *[]){
+ "app-mockups",
+ "settings-mockups",
+ "os-mockups",
+ "software-mockups",
+ "mocktails",
+ NULL});
+ suggestion_entry_set_model (SUGGESTION_ENTRY (entry), G_LIST_MODEL (strings));
+ g_object_unref (strings);
+
+ gtk_box_append (GTK_BOX (box), entry);
+
+ suggestion_entry_set_match_func (SUGGESTION_ENTRY (entry), match_func, NULL, NULL);
+
+ factory = gtk_signal_list_item_factory_new ();
+ g_signal_connect (factory, "setup", G_CALLBACK (setup_highlight_item), NULL);
+ g_signal_connect (factory, "bind", G_CALLBACK (bind_highlight_item), NULL);
+ suggestion_entry_set_factory (SUGGESTION_ENTRY (entry), factory);
+ g_object_unref (factory);
+
}
if (!gtk_widget_get_visible (window))
diff --git a/demos/gtk-demo/meson.build b/demos/gtk-demo/meson.build
index fcddbde09e..93a78c9fca 100644
--- a/demos/gtk-demo/meson.build
+++ b/demos/gtk-demo/meson.build
@@ -19,7 +19,6 @@ demos = files([
'cursors.c',
'dialog.c',
'drawingarea.c',
- 'dropdown.c',
'dnd.c',
'editable_cells.c',
'entry_completion.c',
@@ -54,6 +53,7 @@ demos = files([
'listview_colors.c',
'listview_filebrowser.c',
'listview_minesweeper.c',
+ 'dropdown.c',
'listview_settings.c',
'listview_ucd.c',
'listview_weather.c',
@@ -123,7 +123,8 @@ extra_demo_sources = files(['main.c',
'demo3widget.c',
'pixbufpaintable.c',
'script-names.c',
- 'unicode-names.c'])
+ 'unicode-names.c',
+ 'suggestionentry.c'])
if harfbuzz_dep.found() and pangoft_dep.found()
demos += files(['font_features.c'])
diff --git a/demos/gtk-demo/suggestionentry.c b/demos/gtk-demo/suggestionentry.c
new file mode 100644
index 0000000000..a0a926979f
--- /dev/null
+++ b/demos/gtk-demo/suggestionentry.c
@@ -0,0 +1,1215 @@
+#include "suggestionentry.h"
+
+struct _MatchObject
+{
+ GObject parent_instance;
+
+ GObject *item;
+ char *string;
+ guint match_start;
+ guint match_end;
+ guint score;
+};
+
+typedef struct
+{
+ GObjectClass parent_class;
+} MatchObjectClass;
+
+enum
+{
+ PROP_ITEM = 1,
+ PROP_STRING,
+ PROP_MATCH_START,
+ PROP_MATCH_END,
+ PROP_SCORE,
+ N_MATCH_PROPERTIES
+};
+
+static GParamSpec *match_properties[N_MATCH_PROPERTIES];
+
+G_DEFINE_TYPE (MatchObject, match_object, G_TYPE_OBJECT)
+
+static void
+match_object_init (MatchObject *object)
+{
+}
+
+static void
+match_object_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ MatchObject *self = MATCH_OBJECT (object);
+
+ switch (property_id)
+ {
+ case PROP_ITEM:
+ g_value_set_object (value, self->item);
+ break;
+
+ case PROP_STRING:
+ g_value_set_string (value, self->string);
+ break;
+
+ case PROP_MATCH_START:
+ g_value_set_uint (value, self->match_start);
+ break;
+
+ case PROP_MATCH_END:
+ g_value_set_uint (value, self->match_end);
+ break;
+
+ case PROP_SCORE:
+ g_value_set_uint (value, self->score);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+match_object_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ MatchObject *self = MATCH_OBJECT (object);
+
+ switch (property_id)
+ {
+ case PROP_ITEM:
+ self->item = g_value_get_object (value);
+ break;
+
+ case PROP_STRING:
+ self->string = g_value_dup_string (value);
+ break;
+
+ case PROP_MATCH_START:
+ if (self->match_start != g_value_get_uint (value))
+ {
+ self->match_start = g_value_get_uint (value);
+ g_object_notify_by_pspec (object, pspec);
+ }
+ break;
+
+ case PROP_MATCH_END:
+ if (self->match_end != g_value_get_uint (value))
+ {
+ self->match_end = g_value_get_uint (value);
+ g_object_notify_by_pspec (object, pspec);
+ }
+ break;
+
+ case PROP_SCORE:
+ if (self->score != g_value_get_uint (value))
+ {
+ self->score = g_value_get_uint (value);
+ g_object_notify_by_pspec (object, pspec);
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+match_object_dispose (GObject *object)
+{
+ MatchObject *self = MATCH_OBJECT (object);
+
+ g_clear_object (&self->item);
+ g_clear_pointer (&self->string, g_free);
+
+ G_OBJECT_CLASS (match_object_parent_class)->dispose (object);
+}
+
+static void
+match_object_class_init (MatchObjectClass *class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+ object_class->dispose = match_object_dispose;
+ object_class->get_property = match_object_get_property;
+ object_class->set_property = match_object_set_property;
+
+ match_properties[PROP_ITEM]
+ = g_param_spec_object ("item", "Item", "Item",
+ G_TYPE_OBJECT,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS);
+ match_properties[PROP_STRING]
+ = g_param_spec_string ("string", "String", "String",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS);
+ match_properties[PROP_MATCH_START]
+ = g_param_spec_uint ("match-start", "Match Start", "Match Start",
+ 0, G_MAXUINT, 0,
+ G_PARAM_READWRITE |
+ G_PARAM_EXPLICIT_NOTIFY |
+ G_PARAM_STATIC_STRINGS);
+ match_properties[PROP_MATCH_END]
+ = g_param_spec_uint ("match-end", "Match End", "Match End",
+ 0, G_MAXUINT, 0,
+ G_PARAM_READWRITE |
+ G_PARAM_EXPLICIT_NOTIFY |
+ G_PARAM_STATIC_STRINGS);
+ match_properties[PROP_SCORE]
+ = g_param_spec_uint ("score", "Score", "Score",
+ 0, G_MAXUINT, 0,
+ G_PARAM_READWRITE |
+ G_PARAM_EXPLICIT_NOTIFY |
+ G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_MATCH_PROPERTIES, match_properties);
+}
+
+static MatchObject *
+match_object_new (gpointer item,
+ const char *string)
+{
+ return g_object_new (MATCH_TYPE_OBJECT,
+ "item", item,
+ "string", string,
+ NULL);
+}
+
+gpointer
+match_object_get_item (MatchObject *object)
+{
+ return object->item;
+}
+
+const char *
+match_object_get_string (MatchObject *object)
+{
+ return object->string;
+}
+
+guint
+match_object_get_match_start (MatchObject *object)
+{
+ return object->match_start;
+}
+
+guint
+match_object_get_match_end (MatchObject *object)
+{
+ return object->match_end;
+}
+
+guint
+match_object_get_score (MatchObject *object)
+{
+ return object->score;
+}
+
+void
+match_object_set_match (MatchObject *object,
+ guint start,
+ guint end,
+ guint score)
+{
+ g_object_freeze_notify (G_OBJECT (object));
+
+ g_object_set (object,
+ "match-start", start,
+ "match-end", end,
+ "score", score,
+ NULL);
+
+ g_object_thaw_notify (G_OBJECT (object));
+}
+
+/* ---- */
+
+struct _SuggestionEntry
+{
+ GtkWidget parent_instance;
+
+ GListModel *model;
+ GtkListItemFactory *factory;
+ GtkExpression *expression;
+
+ GtkFilter *filter;
+ GtkMapListModel *map_model;
+ GtkSingleSelection *selection;
+
+ GtkWidget *entry;
+ GtkWidget *arrow;
+ GtkWidget *popup;
+ GtkWidget *list;
+
+ char *search;
+
+ SuggestionEntryMatchFunc match_func;
+ gpointer match_data;
+ GDestroyNotify destroy;
+
+ gulong changed_id;
+
+ guint use_filter : 1;
+ guint show_arrow : 1;
+};
+
+typedef struct _SuggestionEntryClass SuggestionEntryClass;
+
+struct _SuggestionEntryClass
+{
+ GtkWidgetClass parent_class;
+};
+
+enum
+{
+ PROP_0,
+ PROP_MODEL,
+ PROP_FACTORY,
+ PROP_EXPRESSION,
+ PROP_PLACEHOLDER_TEXT,
+ PROP_POPUP_VISIBLE,
+ PROP_USE_FILTER,
+ PROP_SHOW_ARROW,
+
+ N_PROPERTIES,
+};
+
+static void suggestion_entry_set_popup_visible (SuggestionEntry *self,
+ gboolean visible);
+
+static GtkEditable *
+suggestion_entry_get_delegate (GtkEditable *editable)
+{
+ return GTK_EDITABLE (SUGGESTION_ENTRY (editable)->entry);
+}
+
+static void
+suggestion_entry_editable_init (GtkEditableInterface *iface)
+{
+ iface->get_delegate = suggestion_entry_get_delegate;
+}
+
+G_DEFINE_TYPE_WITH_CODE (SuggestionEntry, suggestion_entry, GTK_TYPE_WIDGET,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE,
+ suggestion_entry_editable_init))
+
+static GParamSpec *properties[N_PROPERTIES] = { NULL, };
+
+static void
+suggestion_entry_dispose (GObject *object)
+{
+ SuggestionEntry *self = SUGGESTION_ENTRY (object);
+
+ if (self->changed_id)
+ {
+ g_signal_handler_disconnect (self->entry, self->changed_id);
+ self->changed_id = 0;
+ }
+ g_clear_pointer (&self->entry, gtk_widget_unparent);
+ g_clear_pointer (&self->arrow, gtk_widget_unparent);
+ g_clear_pointer (&self->popup, gtk_widget_unparent);
+
+ g_clear_pointer (&self->expression, gtk_expression_unref);
+ g_clear_object (&self->factory);
+
+ g_clear_object (&self->model);
+ g_clear_object (&self->map_model);
+ g_clear_object (&self->selection);
+
+ g_clear_pointer (&self->search, g_free);
+
+ if (self->destroy)
+ self->destroy (self->match_data);
+
+ G_OBJECT_CLASS (suggestion_entry_parent_class)->dispose (object);
+}
+
+static void
+suggestion_entry_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SuggestionEntry *self = SUGGESTION_ENTRY (object);
+
+ if (gtk_editable_delegate_get_property (object, property_id, value, pspec))
+ return;
+
+ switch (property_id)
+ {
+ case PROP_MODEL:
+ g_value_set_object (value, suggestion_entry_get_model (self));
+ break;
+
+ case PROP_FACTORY:
+ g_value_set_object (value, suggestion_entry_get_factory (self));
+ break;
+
+ case PROP_EXPRESSION:
+ gtk_value_set_expression (value, suggestion_entry_get_expression (self));
+ break;
+
+ case PROP_PLACEHOLDER_TEXT:
+ g_value_set_string (value, gtk_text_get_placeholder_text (GTK_TEXT (self->entry)));
+ break;
+
+ case PROP_POPUP_VISIBLE:
+ g_value_set_boolean (value, self->popup && gtk_widget_get_visible (self->popup));
+ break;
+
+ case PROP_USE_FILTER:
+ g_value_set_boolean (value, suggestion_entry_get_use_filter (self));
+ break;
+
+ case PROP_SHOW_ARROW:
+ g_value_set_boolean (value, suggestion_entry_get_show_arrow (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+suggestion_entry_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SuggestionEntry *self = SUGGESTION_ENTRY (object);
+
+ if (gtk_editable_delegate_set_property (object, property_id, value, pspec))
+ return;
+
+ switch (property_id)
+ {
+ case PROP_MODEL:
+ suggestion_entry_set_model (self, g_value_get_object (value));
+ break;
+
+ case PROP_FACTORY:
+ suggestion_entry_set_factory (self, g_value_get_object (value));
+ break;
+
+ case PROP_EXPRESSION:
+ suggestion_entry_set_expression (self, gtk_value_get_expression (value));
+ break;
+
+ case PROP_PLACEHOLDER_TEXT:
+ gtk_text_set_placeholder_text (GTK_TEXT (self->entry), g_value_get_string (value));
+ break;
+
+ case PROP_POPUP_VISIBLE:
+ suggestion_entry_set_popup_visible (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_USE_FILTER:
+ suggestion_entry_set_use_filter (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_SHOW_ARROW:
+ suggestion_entry_set_show_arrow (self, g_value_get_boolean (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+suggestion_entry_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ SuggestionEntry *self = SUGGESTION_ENTRY (widget);
+ int arrow_min = 0, arrow_nat = 0;
+
+ gtk_widget_measure (self->entry, orientation, for_size,
+ minimum, natural,
+ minimum_baseline, natural_baseline);
+
+ if (self->arrow && gtk_widget_get_visible (self->arrow))
+ gtk_widget_measure (self->arrow, orientation, for_size,
+ &arrow_min, &arrow_nat,
+ NULL, NULL);
+}
+
+static void
+suggestion_entry_size_allocate (GtkWidget *widget,
+ int width,
+ int height,
+ int baseline)
+{
+ SuggestionEntry *self = SUGGESTION_ENTRY (widget);
+ int arrow_min = 0, arrow_nat = 0;
+
+ if (self->arrow && gtk_widget_get_visible (self->arrow))
+ gtk_widget_measure (self->arrow, GTK_ORIENTATION_HORIZONTAL, -1,
+ &arrow_min, &arrow_nat,
+ NULL, NULL);
+
+ gtk_widget_size_allocate (self->entry,
+ &(GtkAllocation) { 0, 0, width - arrow_nat, height },
+ baseline);
+
+ if (self->arrow && gtk_widget_get_visible (self->arrow))
+ gtk_widget_size_allocate (self->arrow,
+ &(GtkAllocation) { width - arrow_nat, 0, arrow_nat, height },
+ baseline);
+
+ gtk_widget_set_size_request (self->popup, gtk_widget_get_allocated_width (GTK_WIDGET (self)), -1);
+ gtk_widget_queue_resize (self->popup);
+
+ gtk_native_check_resize (GTK_NATIVE (self->popup));
+}
+
+static gboolean
+suggestion_entry_grab_focus (GtkWidget *widget)
+{
+ SuggestionEntry *self = SUGGESTION_ENTRY (widget);
+
+ return gtk_widget_grab_focus (self->entry);
+}
+
+static void
+suggestion_entry_class_init (SuggestionEntryClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = suggestion_entry_dispose;
+ object_class->get_property = suggestion_entry_get_property;
+ object_class->set_property = suggestion_entry_set_property;
+
+ widget_class->measure = suggestion_entry_measure;
+ widget_class->size_allocate = suggestion_entry_size_allocate;
+ widget_class->grab_focus = suggestion_entry_grab_focus;
+
+ properties[PROP_MODEL] =
+ g_param_spec_object ("model",
+ "Model",
+ "Model for the displayed items",
+ G_TYPE_LIST_MODEL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ properties[PROP_FACTORY] =
+ g_param_spec_object ("factory",
+ "Factory",
+ "Factory for populating list items",
+ GTK_TYPE_LIST_ITEM_FACTORY,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ properties[PROP_EXPRESSION] =
+ gtk_param_spec_expression ("expression",
+ "Expression",
+ "Expression to determine strings to search for",
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ properties[PROP_PLACEHOLDER_TEXT] =
+ g_param_spec_string ("placeholder-text",
+ "Placeholder text",
+ "Show text in the entry when it’s empty and unfocused",
+ NULL,
+ G_PARAM_READWRITE);
+
+ properties[PROP_POPUP_VISIBLE] =
+ g_param_spec_boolean ("popup-visible",
+ "Popup visible",
+ "Whether the popup with suggestions is currently visible",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ properties[PROP_USE_FILTER] =
+ g_param_spec_boolean ("use-filter",
+ "Use filter",
+ "Whether to filter the list for matches",
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ properties[PROP_SHOW_ARROW] =
+ g_param_spec_boolean ("show-arrow",
+ "Show arrow",
+ "Whether to show a clickable arrow for presenting the popup",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPERTIES, properties);
+ gtk_editable_install_properties (object_class, N_PROPERTIES);
+
+ gtk_widget_class_install_property_action (widget_class, "popup.show", "popup-visible");
+
+ gtk_widget_class_add_binding_action (widget_class,
+ GDK_KEY_Down, GDK_ALT_MASK,
+ "popup.show", NULL);
+
+ gtk_widget_class_set_css_name (widget_class, "entry");
+}
+
+static void
+setup_item (GtkSignalListItemFactory *factory,
+ GtkListItem *list_item,
+ gpointer data)
+{
+ GtkWidget *label;
+
+ label = gtk_label_new (NULL);
+ gtk_label_set_xalign (GTK_LABEL (label), 0.0);
+ gtk_list_item_set_child (list_item, label);
+}
+
+static void
+bind_item (GtkSignalListItemFactory *factory,
+ GtkListItem *list_item,
+ gpointer data)
+{
+ gpointer item;
+ GtkWidget *label;
+ GValue value = G_VALUE_INIT;
+
+ item = gtk_list_item_get_item (list_item);
+ label = gtk_list_item_get_child (list_item);
+
+ gtk_label_set_label (GTK_LABEL (label), match_object_get_string (MATCH_OBJECT (item)));
+ g_value_unset (&value);
+}
+
+static void
+suggestion_entry_set_popup_visible (SuggestionEntry *self,
+ gboolean visible)
+{
+ if (gtk_widget_get_visible (self->popup) == visible)
+ return;
+
+ if (g_list_model_get_n_items (G_LIST_MODEL (self->selection)) == 0)
+ return;
+
+ if (visible)
+ {
+ if (!gtk_widget_has_focus (self->entry))
+ gtk_text_grab_focus_without_selecting (GTK_TEXT (self->entry));
+
+ gtk_single_selection_set_selected (self->selection, GTK_INVALID_LIST_POSITION);
+ gtk_popover_popup (GTK_POPOVER (self->popup));
+ }
+ else
+ {
+ gtk_popover_popdown (GTK_POPOVER (self->popup));
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_POPUP_VISIBLE]);
+}
+
+static void update_map (SuggestionEntry *self);
+
+static gboolean
+text_changed_idle (gpointer data)
+{
+ SuggestionEntry *self = data;
+ const char *text;
+ guint matches;
+
+ if (!self->map_model)
+ return G_SOURCE_REMOVE;
+
+ text = gtk_editable_get_text (GTK_EDITABLE (self->entry));
+
+ g_free (self->search);
+ self->search = g_strdup (text);
+
+ update_map (self);
+
+ matches = g_list_model_get_n_items (G_LIST_MODEL (self->selection));
+
+ suggestion_entry_set_popup_visible (self, matches > 0);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+text_changed (GtkEditable *editable,
+ GParamSpec *pspec,
+ SuggestionEntry *self)
+{
+ /* We need to defer to an idle since GtkText sets selection bounds
+ * after notify::text
+ */
+ g_idle_add (text_changed_idle, self);
+}
+
+static void
+accept_current_selection (SuggestionEntry *self)
+{
+ gpointer item;
+
+ item = gtk_single_selection_get_selected_item (self->selection);
+ if (!item)
+ return;
+
+ g_signal_handler_block (self->entry, self->changed_id);
+
+ gtk_editable_set_text (GTK_EDITABLE (self->entry),
+ match_object_get_string (MATCH_OBJECT (item)));
+
+ gtk_editable_set_position (GTK_EDITABLE (self->entry), -1);
+
+ g_signal_handler_unblock (self->entry, self->changed_id);
+}
+
+static void
+suggestion_entry_row_activated (GtkListView *listview,
+ guint position,
+ SuggestionEntry *self)
+{
+ suggestion_entry_set_popup_visible (self, FALSE);
+ accept_current_selection (self);
+}
+
+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
+suggestion_entry_key_pressed (GtkEventControllerKey *controller,
+ guint keyval,
+ guint keycode,
+ GdkModifierType state,
+ SuggestionEntry *self)
+{
+ guint matches;
+ guint selected;
+
+ if (state & (GDK_SHIFT_MASK | GDK_ALT_MASK | GDK_CONTROL_MASK))
+ return FALSE;
+
+ if (keyval == GDK_KEY_Return ||
+ keyval == GDK_KEY_KP_Enter ||
+ keyval == GDK_KEY_ISO_Enter)
+ {
+ suggestion_entry_set_popup_visible (self, FALSE);
+ accept_current_selection (self);
+ g_free (self->search);
+ self->search = g_strdup (gtk_editable_get_text (GTK_EDITABLE (self->entry)));
+ update_map (self);
+
+ return TRUE;
+ }
+ else if (keyval == GDK_KEY_Escape)
+ {
+ if (gtk_widget_get_mapped (self->popup))
+ {
+ suggestion_entry_set_popup_visible (self, FALSE);
+
+ g_signal_handler_block (self->entry, self->changed_id);
+
+ gtk_editable_set_text (GTK_EDITABLE (self->entry), self->search ? self->search : "");
+
+ gtk_editable_set_position (GTK_EDITABLE (self->entry), -1);
+
+ g_signal_handler_unblock (self->entry, self->changed_id);
+ return TRUE;
+ }
+ }
+ else if (keyval == GDK_KEY_Right ||
+ keyval == GDK_KEY_KP_Right)
+ {
+ gtk_editable_set_position (GTK_EDITABLE (self->entry), -1);
+ return TRUE;
+ }
+ else if (keyval == GDK_KEY_Left ||
+ keyval == GDK_KEY_KP_Left)
+ {
+ return FALSE;
+ }
+ else if (keyval == GDK_KEY_Tab ||
+ keyval == GDK_KEY_KP_Tab ||
+ keyval == GDK_KEY_ISO_Left_Tab)
+ {
+ suggestion_entry_set_popup_visible (self, FALSE);
+ return FALSE; /* don't disrupt normal focus handling */
+ }
+
+ matches = g_list_model_get_n_items (G_LIST_MODEL (self->selection));
+ 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--;
+ }
+ 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++;
+ }
+ 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;
+}
+
+static void
+suggestion_entry_focus_out (GtkEventController *controller,
+ SuggestionEntry *self)
+{
+ if (!gtk_widget_get_mapped (self->popup))
+ return;
+
+ suggestion_entry_set_popup_visible (self, FALSE);
+ accept_current_selection (self);
+}
+
+static void
+set_default_factory (SuggestionEntry *self)
+{
+ GtkListItemFactory *factory;
+
+ factory = gtk_signal_list_item_factory_new ();
+
+ g_signal_connect (factory, "setup", G_CALLBACK (setup_item), self);
+ g_signal_connect (factory, "bind", G_CALLBACK (bind_item), self);
+
+ suggestion_entry_set_factory (self, factory);
+
+ g_object_unref (factory);
+}
+
+static void default_match_func (MatchObject *object,
+ const char *search,
+ gpointer data);
+
+static void
+suggestion_entry_init (SuggestionEntry *self)
+{
+ GtkWidget *sw;
+ GtkEventController *controller;
+
+ if (!g_object_get_data (G_OBJECT (gdk_display_get_default ()), "suggestion-style"))
+ {
+ GtkCssProvider *provider;
+
+ provider = gtk_css_provider_new ();
+ gtk_css_provider_load_from_resource (provider, "/dropdown/suggestionentry.css");
+ gtk_style_context_add_provider_for_display (gdk_display_get_default (),
+ GTK_STYLE_PROVIDER (provider),
+ 800);
+ g_object_set_data (G_OBJECT (gdk_display_get_default ()), "suggestion-style", provider);
+ g_object_unref (provider);
+ }
+
+ self->use_filter = TRUE;
+ self->show_arrow = FALSE;
+
+ self->match_func = default_match_func;
+ self->match_data = NULL;
+ self->destroy = NULL;
+
+ gtk_widget_add_css_class (GTK_WIDGET (self), "suggestion");
+
+ self->entry = gtk_text_new ();
+ gtk_widget_set_parent (self->entry, GTK_WIDGET (self));
+ gtk_widget_set_hexpand (self->entry, TRUE);
+ gtk_editable_init_delegate (GTK_EDITABLE (self));
+ self->changed_id = g_signal_connect (self->entry, "notify::text", G_CALLBACK (text_changed), self);
+
+ self->popup = gtk_popover_new ();
+ gtk_popover_set_position (GTK_POPOVER (self->popup), GTK_POS_BOTTOM);
+ gtk_popover_set_autohide (GTK_POPOVER (self->popup), FALSE);
+ gtk_popover_set_has_arrow (GTK_POPOVER (self->popup), FALSE);
+ gtk_widget_set_halign (self->popup, GTK_ALIGN_START);
+ gtk_widget_add_css_class (self->popup, "menu");
+ gtk_widget_set_parent (self->popup, GTK_WIDGET (self));
+ sw = gtk_scrolled_window_new ();
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
+ GTK_POLICY_NEVER,
+ GTK_POLICY_AUTOMATIC);
+ gtk_scrolled_window_set_max_content_height (GTK_SCROLLED_WINDOW (sw), 400);
+ gtk_scrolled_window_set_propagate_natural_height (GTK_SCROLLED_WINDOW (sw), TRUE);
+
+ gtk_popover_set_child (GTK_POPOVER (self->popup), sw);
+ self->list = gtk_list_view_new (NULL, NULL);
+ gtk_list_view_set_single_click_activate (GTK_LIST_VIEW (self->list), TRUE);
+ g_signal_connect (self->list, "activate",
+ G_CALLBACK (suggestion_entry_row_activated), self);
+ gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), self->list);
+
+ set_default_factory (self);
+
+ controller = gtk_event_controller_key_new ();
+ gtk_event_controller_set_name (controller, "gtk-suggestion-entry");
+ g_signal_connect (controller, "key-pressed",
+ G_CALLBACK (suggestion_entry_key_pressed), self);
+ gtk_widget_add_controller (self->entry, controller);
+
+ controller = gtk_event_controller_focus_new ();
+ gtk_event_controller_set_name (controller, "gtk-suggestion-entry");
+ g_signal_connect (controller, "leave",
+ G_CALLBACK (suggestion_entry_focus_out), self);
+ gtk_widget_add_controller (self->entry, controller);
+}
+
+GtkWidget *
+suggestion_entry_new (void)
+{
+ return g_object_new (SUGGESTION_TYPE_ENTRY, NULL);
+}
+
+GListModel *
+suggestion_entry_get_model (SuggestionEntry *self)
+{
+ g_return_val_if_fail (SUGGESTION_IS_ENTRY (self), NULL);
+
+ return self->model;
+}
+
+static void
+selection_changed (GtkSingleSelection *selection,
+ GParamSpec *pspec,
+ SuggestionEntry *self)
+{
+ accept_current_selection (self);
+}
+
+static gboolean
+filter_func (gpointer item, gpointer user_data)
+{
+ SuggestionEntry *self = SUGGESTION_ENTRY (user_data);
+ guint min_score;
+
+ if (self->use_filter)
+ min_score = 1;
+ else
+ min_score = 0;
+
+ return match_object_get_score (MATCH_OBJECT (item)) >= min_score;
+}
+
+static void
+default_match_func (MatchObject *object,
+ const char *search,
+ gpointer data)
+{
+ char *tmp1, *tmp2, *tmp3, *tmp4;
+
+ tmp1 = g_utf8_normalize (match_object_get_string (object), -1, G_NORMALIZE_ALL);
+ tmp2 = g_utf8_casefold (tmp1, -1);
+
+ tmp3 = g_utf8_normalize (search, -1, G_NORMALIZE_ALL);
+ tmp4 = g_utf8_casefold (tmp3, -1);
+
+ if (g_str_has_prefix (tmp2, tmp4))
+ match_object_set_match (object, 0, g_utf8_strlen (search, -1), 1);
+ else
+ match_object_set_match (object, 0, 0, 0);
+
+ g_free (tmp1);
+ g_free (tmp2);
+ g_free (tmp3);
+ g_free (tmp4);
+}
+
+static gpointer
+map_func (gpointer item, gpointer user_data)
+{
+ SuggestionEntry *self = SUGGESTION_ENTRY (user_data);
+ GValue value = G_VALUE_INIT;
+ gpointer obj;
+
+ if (self->expression)
+ {
+ gtk_expression_evaluate (self->expression, item, &value);
+ }
+ else if (GTK_IS_STRING_OBJECT (item))
+ {
+ g_object_get_property (G_OBJECT (item), "string", &value);
+ }
+ else
+ {
+ g_critical ("Either SuggestionEntry:expression must be set "
+ "or SuggestionEntry:model must be a GtkStringList");
+ g_value_set_string (&value, "No value");
+ }
+
+ obj = match_object_new (item, g_value_get_string (&value));
+
+ g_value_unset (&value);
+
+ if (self->search && self->search[0])
+ self->match_func (obj, self->search, self->match_data);
+ else
+ match_object_set_match (obj, 0, 0, 1);
+
+ return obj;
+}
+
+static void
+update_map (SuggestionEntry *self)
+{
+ gtk_map_list_model_set_map_func (self->map_model, map_func, self, NULL);
+}
+
+void
+suggestion_entry_set_model (SuggestionEntry *self,
+ GListModel *model)
+{
+ g_return_if_fail (SUGGESTION_IS_ENTRY (self));
+ g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model));
+
+ if (!g_set_object (&self->model, model))
+ return;
+
+ if (self->selection)
+ g_signal_handlers_disconnect_by_func (self->selection, selection_changed, self);
+
+ if (model == NULL)
+ {
+ gtk_list_view_set_model (GTK_LIST_VIEW (self->list), NULL);
+ g_clear_object (&self->selection);
+ g_clear_object (&self->map_model);
+ g_clear_object (&self->filter);
+ }
+ else
+ {
+ GtkMapListModel *map_model;
+ GtkFilterListModel *filter_model;
+ GtkFilter *filter;
+ GtkSortListModel *sort_model;
+ GtkSingleSelection *selection;
+ GtkSorter *sorter;
+
+ map_model = gtk_map_list_model_new (g_object_ref (model), NULL, NULL, NULL);
+ g_set_object (&self->map_model, map_model);
+
+ update_map (self);
+
+ filter = GTK_FILTER (gtk_custom_filter_new (filter_func, self, NULL));
+ filter_model = gtk_filter_list_model_new (G_LIST_MODEL (self->map_model), filter);
+ g_set_object (&self->filter, filter);
+
+ sorter = GTK_SORTER (gtk_numeric_sorter_new (gtk_property_expression_new (MATCH_TYPE_OBJECT, NULL, "score")));
+ gtk_numeric_sorter_set_sort_order (GTK_NUMERIC_SORTER (sorter), GTK_SORT_DESCENDING);
+ sort_model = gtk_sort_list_model_new (G_LIST_MODEL (filter_model), sorter);
+
+ update_map (self);
+
+ 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);
+ gtk_list_view_set_model (GTK_LIST_VIEW (self->list), GTK_SELECTION_MODEL (selection));
+ g_object_unref (selection);
+ }
+
+ if (self->selection)
+ {
+ g_signal_connect (self->selection, "notify::selected",
+ G_CALLBACK (selection_changed), self);
+ selection_changed (self->selection, NULL, self);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MODEL]);
+}
+
+GtkListItemFactory *
+suggestion_entry_get_factory (SuggestionEntry *self)
+{
+ g_return_val_if_fail (SUGGESTION_IS_ENTRY (self), NULL);
+
+ return self->factory;
+}
+
+void
+suggestion_entry_set_factory (SuggestionEntry *self,
+ GtkListItemFactory *factory)
+{
+ g_return_if_fail (SUGGESTION_IS_ENTRY (self));
+ g_return_if_fail (factory == NULL || GTK_LIST_ITEM_FACTORY (factory));
+
+ if (!g_set_object (&self->factory, factory))
+ return;
+
+ if (self->list)
+ gtk_list_view_set_factory (GTK_LIST_VIEW (self->list), factory);
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FACTORY]);
+}
+
+void
+suggestion_entry_set_expression (SuggestionEntry *self,
+ GtkExpression *expression)
+{
+ g_return_if_fail (SUGGESTION_IS_ENTRY (self));
+ g_return_if_fail (expression == NULL ||
+ gtk_expression_get_value_type (expression) == G_TYPE_STRING);
+
+ if (self->expression == expression)
+ return;
+
+ if (self->expression)
+ gtk_expression_unref (self->expression);
+
+ self->expression = expression;
+
+ if (self->expression)
+ gtk_expression_ref (self->expression);
+
+ update_map (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_EXPRESSION]);
+}
+
+GtkExpression *
+suggestion_entry_get_expression (SuggestionEntry *self)
+{
+ g_return_val_if_fail (SUGGESTION_IS_ENTRY (self), NULL);
+
+ return self->expression;
+}
+
+void
+suggestion_entry_set_use_filter (SuggestionEntry *self,
+ gboolean use_filter)
+{
+ g_return_if_fail (SUGGESTION_IS_ENTRY (self));
+
+ if (self->use_filter == use_filter)
+ return;
+
+ self->use_filter = use_filter;
+
+ gtk_filter_changed (self->filter, GTK_FILTER_CHANGE_DIFFERENT);
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_USE_FILTER]);
+}
+
+gboolean
+suggestion_entry_get_use_filter (SuggestionEntry *self)
+{
+ g_return_val_if_fail (SUGGESTION_IS_ENTRY (self), TRUE);
+
+ return self->use_filter;
+}
+
+static void
+suggestion_entry_arrow_clicked (SuggestionEntry *self)
+{
+ gboolean visible;
+
+ visible = gtk_widget_get_visible (self->popup);
+ suggestion_entry_set_popup_visible (self, !visible);
+}
+
+void
+suggestion_entry_set_show_arrow (SuggestionEntry *self,
+ gboolean show_arrow)
+{
+ g_return_if_fail (SUGGESTION_IS_ENTRY (self));
+
+ if (self->show_arrow == show_arrow)
+ return;
+
+ self->show_arrow = show_arrow;
+
+ if (show_arrow)
+ {
+ GtkGesture *press;
+
+ self->arrow = gtk_image_new_from_icon_name ("pan-down-symbolic");
+ gtk_widget_set_tooltip_text (self->arrow, "Show suggestions");
+ gtk_widget_set_parent (self->arrow, GTK_WIDGET (self));
+
+ press = gtk_gesture_click_new ();
+ g_signal_connect_swapped (press, "released",
+ G_CALLBACK (suggestion_entry_arrow_clicked), self);
+ gtk_widget_add_controller (self->arrow, GTK_EVENT_CONTROLLER (press));
+
+ }
+ else
+ {
+ g_clear_pointer (&self->arrow, gtk_widget_unparent);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SHOW_ARROW]);
+}
+
+gboolean
+suggestion_entry_get_show_arrow (SuggestionEntry *self)
+{
+ g_return_val_if_fail (SUGGESTION_IS_ENTRY (self), FALSE);
+
+ return self->show_arrow;
+}
+
+void
+suggestion_entry_set_match_func (SuggestionEntry *self,
+ SuggestionEntryMatchFunc match_func,
+ gpointer user_data,
+ GDestroyNotify destroy)
+{
+ if (self->destroy)
+ self->destroy (self->match_data);
+ self->match_func = match_func;
+ self->match_data = user_data;
+ self->destroy = destroy;
+}
diff --git a/demos/gtk-demo/suggestionentry.css b/demos/gtk-demo/suggestionentry.css
new file mode 100644
index 0000000000..a698455a01
--- /dev/null
+++ b/demos/gtk-demo/suggestionentry.css
@@ -0,0 +1,28 @@
+entry.suggestion > popover.menu.background > contents {
+ padding: 0;
+}
+
+entry.suggestion arrow {
+ -gtk-icon-source: -gtk-icontheme('pan-down-symbolic');
+ min-height: 16px;
+ min-width: 16px;
+}
+
+entry.suggestion > popover {
+ margin-top: 6px;
+ padding: 0;
+}
+
+entry.suggestion > popover listview {
+ margin: 8px 0;
+}
+
+entry.suggestion > popover listview > row {
+ padding: 8px;
+}
+
+entry.suggestion > popover listview > row:selected {
+ outline-color: rgba(1,1,1,0.2);
+ color: @theme_text_color;
+ background-color: shade(#f6f5f4, 0.97);
+}
diff --git a/demos/gtk-demo/suggestionentry.h b/demos/gtk-demo/suggestionentry.h
new file mode 100644
index 0000000000..ede791c961
--- /dev/null
+++ b/demos/gtk-demo/suggestionentry.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+
+#define MATCH_TYPE_OBJECT (match_object_get_type ())
+#define MATCH_OBJECT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), MATCH_TYPE_OBJECT, MatchObject))
+#define MATCH_IS_OBJECT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MATCH_TYPE_OBJECT))
+
+typedef struct _MatchObject MatchObject;
+
+GType match_object_get_type (void) G_GNUC_CONST;
+
+gpointer match_object_get_item (MatchObject *object);
+const char * match_object_get_string (MatchObject *object);
+guint match_object_get_match_start (MatchObject *object);
+guint match_object_get_match_end (MatchObject *object);
+guint match_object_get_score (MatchObject *object);
+void match_object_set_match (MatchObject *object,
+ guint start,
+ guint end,
+ guint score);
+
+#define SUGGESTION_TYPE_ENTRY (suggestion_entry_get_type ())
+#define SUGGESTION_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SUGGESTION_TYPE_ENTRY, SuggestionEntry))
+#define SUGGESTION_IS_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SUGGESTION_TYPE_ENTRY))
+
+typedef struct _SuggestionEntry SuggestionEntry;
+
+GType suggestion_entry_get_type (void) G_GNUC_CONST;
+
+GtkWidget* suggestion_entry_new (void);
+
+void suggestion_entry_set_model (SuggestionEntry *self,
+ GListModel *model);
+GListModel * suggestion_entry_get_model (SuggestionEntry *self);
+
+void suggestion_entry_set_factory (SuggestionEntry *self,
+ GtkListItemFactory *factory);
+GtkListItemFactory *
+ suggestion_entry_get_factory (SuggestionEntry *self);
+
+void suggestion_entry_set_use_filter (SuggestionEntry *self,
+ gboolean use_ilter);
+gboolean suggestion_entry_get_use_filter (SuggestionEntry *self);
+
+void suggestion_entry_set_expression (SuggestionEntry *self,
+ GtkExpression *expression);
+GtkExpression * suggestion_entry_get_expression (SuggestionEntry *self);
+
+void suggestion_entry_set_show_arrow (SuggestionEntry *self,
+ gboolean show_arrow);
+gboolean suggestion_entry_get_show_arrow (SuggestionEntry *self);
+
+typedef void (* SuggestionEntryMatchFunc) (MatchObject *object,
+ const char *search,
+ gpointer user_data);
+
+void suggestion_entry_set_match_func (SuggestionEntry *self,
+ SuggestionEntryMatchFunc func,
+ gpointer user_data,
+ GDestroyNotify destroy);
+
+G_END_DECLS