diff options
author | Matthias Clasen <mclasen@redhat.com> | 2020-06-25 13:57:17 -0400 |
---|---|---|
committer | Matthias Clasen <mclasen@redhat.com> | 2020-11-11 15:54:43 -0500 |
commit | a2897e1868e8a79fa8c14cec5cf5351601dfaab0 (patch) | |
tree | f7458e5b5fab5a8b4a4b59d3bc99a9fd44ffa498 | |
parent | 15172ebdb0816a73dcaec1a78f61031496082715 (diff) | |
download | gtk+-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.xml | 5 | ||||
-rw-r--r-- | demos/gtk-demo/dropdown.c | 253 | ||||
-rw-r--r-- | demos/gtk-demo/meson.build | 5 | ||||
-rw-r--r-- | demos/gtk-demo/suggestionentry.c | 1215 | ||||
-rw-r--r-- | demos/gtk-demo/suggestionentry.css | 28 | ||||
-rw-r--r-- | demos/gtk-demo/suggestionentry.h | 66 |
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 |