diff options
author | Matthias Clasen <mclasen@redhat.com> | 2020-07-10 13:58:17 -0400 |
---|---|---|
committer | Matthias Clasen <mclasen@redhat.com> | 2020-07-10 15:40:12 -0400 |
commit | c85905dfcd6beeb8afb7bb55140d44cb209bb07c (patch) | |
tree | f090611e1cbecfefded9492428aa1553cfee8029 | |
parent | e2eba253cb6ed91a7732e8372c306e585d23b7fa (diff) | |
download | gtk+-suggestion-entry.tar.gz |
Add an experimentsuggestion-entry
This is a suggestion entry that does infix
matching, highlights the match, and sorts
the matches by relevance.
-rw-r--r-- | tests/meson.build | 1 | ||||
-rw-r--r-- | tests/testsuggestions.c | 390 |
2 files changed, 391 insertions, 0 deletions
diff --git a/tests/meson.build b/tests/meson.build index 4618df6b97..664f2327b2 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -67,6 +67,7 @@ gtk_tests = [ ['testselectionmode'], ['testsounds'], ['testspinbutton'], + ['testsuggestions'], ['testtreechanging'], ['testtreednd'], ['testtreeedit'], diff --git a/tests/testsuggestions.c b/tests/testsuggestions.c new file mode 100644 index 0000000000..4937e2bd2c --- /dev/null +++ b/tests/testsuggestions.c @@ -0,0 +1,390 @@ +#include <gtk/gtk.h> +#include <ctype.h> + +#define GTK_TYPE_MATCH_OBJECT (gtk_match_object_get_type ()) +GDK_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (GtkMatchObject, gtk_match_object, GTK, MATCH_OBJECT, GObject) + +struct _GtkMatchObject +{ + GObject parent_instance; + char *string; + int start; + int end; + int score; +}; + +enum { + PROP_STRING = 1, + PROP_START, + PROP_END, + PROP_SCORE +}; + +G_DEFINE_TYPE (GtkMatchObject, gtk_match_object, G_TYPE_OBJECT); + +static void +gtk_match_object_init (GtkMatchObject *object) +{ + object->start = -1; + object->end = -1; + object->score = 0; +} + +static void +gtk_match_object_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GtkMatchObject *self = GTK_MATCH_OBJECT (object); + + switch (property_id) + { + case PROP_STRING: + g_value_set_string (value, self->string); + break; + + case PROP_START: + g_value_set_int (value, self->start); + break; + + case PROP_END: + g_value_set_int (value, self->end); + break; + + case PROP_SCORE: + g_value_set_int (value, self->score); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +gtk_match_object_finalize (GObject *object) +{ + GtkMatchObject *self = GTK_MATCH_OBJECT (object); + + g_free (self->string); + + G_OBJECT_CLASS (gtk_match_object_parent_class)->finalize (object); +} + +static void +gtk_match_object_class_init (GtkMatchObjectClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + GParamSpec *pspec; + + object_class->finalize = gtk_match_object_finalize; + object_class->get_property = gtk_match_object_get_property; + + pspec = g_param_spec_string ("string", "String", "String", + NULL, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_STRING, pspec); + + pspec = g_param_spec_int ("start", "Start", "Match Start", + -1, G_MAXINT, -1, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_START, pspec); + + pspec = g_param_spec_int ("end", "End", "Match End", + -1, G_MAXINT, -1, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_END, pspec); + + pspec = g_param_spec_int ("score", "Score", "Score", + 0, G_MAXINT, 0, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_SCORE, pspec); +} + +static GtkMatchObject * +gtk_match_object_new (const char *string) +{ + GtkMatchObject *obj; + + obj = g_object_new (GTK_TYPE_MATCH_OBJECT, NULL); + obj->string = g_strdup (string); + + return obj; +} + +static void +gtk_match_object_set_match (GtkMatchObject *obj, + int start, + int end) +{ + g_object_freeze_notify (G_OBJECT (obj)); + + if (obj->start != start) + { + obj->start = start; + g_object_notify (G_OBJECT (obj), "start"); + } + + if (obj->end != end) + { + obj->end = end; + g_object_notify (G_OBJECT (obj), "end"); + } + + g_object_thaw_notify (G_OBJECT (obj)); +} + +static void +gtk_match_object_set_score (GtkMatchObject *obj, + int score) +{ + if (obj->score == score) + return; + + obj->score = score; + + g_object_notify (G_OBJECT (obj), "score"); +} + +static const char * +gtk_match_object_get_string (GtkMatchObject *obj) +{ + return obj->string; +} + +static int +gtk_match_object_get_start (GtkMatchObject *obj) +{ + return obj->start; +} + +static int +gtk_match_object_get_end (GtkMatchObject *obj) +{ + return obj->end; +} + +static int +gtk_match_object_get_score (GtkMatchObject *obj) +{ + return obj->score; +} + +static GListModel * +load_words (const char *file) +{ + char *contents; + gsize size; + char **lines; + GError *error = NULL; + GListStore *store; + int i; + + if (!g_file_get_contents (file, &contents, &size, &error)) + { + g_printerr ("%s", error->message); + exit (1); + } + + store = g_list_store_new (GTK_TYPE_MATCH_OBJECT); + + lines = g_strsplit (contents, "\n", -1); + + for (i = 0; lines[i]; i++) + { + char *s = g_strstrip (lines[i]); + + if (s[0] != '\0') + { + GtkMatchObject *obj = gtk_match_object_new (s); + g_list_store_append (store, obj); + g_object_unref (obj); + } + } + + g_strfreev (lines); + g_free (contents); + + return G_LIST_MODEL (store); +} + +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) +{ + GtkMatchObject *obj; + GtkWidget *label; + PangoAttrList *attrs; + PangoAttribute *attr; + const char *str; + int score; + char *text; + + obj = GTK_MATCH_OBJECT (gtk_list_item_get_item (list_item)); + label = gtk_list_item_get_child (list_item); + + str = gtk_match_object_get_string (obj); + score = gtk_match_object_get_score (obj); + + text = g_strdup_printf ("%s - %d", str, score); + gtk_label_set_label (GTK_LABEL (label), text); + g_free (text); + attrs = pango_attr_list_new (); + attr = pango_attr_weight_new (PANGO_WEIGHT_BOLD); + attr->start_index = gtk_match_object_get_start (obj); + attr->end_index = gtk_match_object_get_end (obj); + pango_attr_list_insert (attrs, attr); + gtk_label_set_attributes (GTK_LABEL (label), attrs); + pango_attr_list_unref (attrs); +} + +static void +text_changed (GObject *object, + GParamSpec *pspec, + GListModel *model) +{ + const char *text; + guint i, n; + int len; + + text = gtk_editable_get_text (GTK_EDITABLE (object)); + len = strlen (text); + + n = g_list_model_get_n_items (model); + for (i = 0; i < n; i++) + { + GtkMatchObject *obj = GTK_MATCH_OBJECT (g_list_model_get_item (model, i)); + char *p; + int start, end; + int score; + +#ifdef G_OS_WIN32 + p = strstr (obj->string, text); +#else + p = strcasestr (obj->string, text); +#endif + if (p) + { + start = p - obj->string; + end = start + len; + + /* prefer matches close to the start */ + score = 100 - start; + + /* prefer exact matches */ + if (strncmp (p, text, len) == 0) + score += 10; + + /* prefer words */ + if ((start == 0 || isspace (p[-1])) && + (p[len] == '\0' || isspace (p[len]) || ispunct (p[len]))) + score += 20; + } + else + { + start = end = -1; + score = 0; + } + + gtk_match_object_set_match (obj, start, end); + gtk_match_object_set_score (obj, score); + g_object_unref (obj); + } + + g_list_model_items_changed (model, 0, n, n); +} + +static gboolean +filter_func (gpointer item, gpointer data) +{ + return GTK_MATCH_OBJECT (item)->score > 0; +} + +static int +compare_func (gconstpointer a, + gconstpointer b, + gpointer data) +{ + const GtkMatchObject *ao = a; + const GtkMatchObject *bo = b; + return bo->score - ao->score; +} + +int +main (int argc, char *argv[]) +{ + GtkWidget *window; + GtkWidget *entry; + GListModel *words; + GListModel *sorted; + GListModel *filtered; + GtkExpression *expression; + GtkListItemFactory *factory; + GtkFilter *filter; + GtkSorter *sorter; + + words = load_words (argv[1]); + + sorter = gtk_custom_sorter_new (compare_func, NULL, NULL); + sorted = G_LIST_MODEL (gtk_sort_list_model_new (words, sorter)); + g_object_unref (sorter); + + filter = gtk_custom_filter_new (filter_func, NULL, NULL); + filtered = G_LIST_MODEL (gtk_filter_list_model_new (sorted, filter)); + g_object_unref (filter); + + gtk_init (); + + window = gtk_window_new (); + + entry = gtk_suggestion_entry_new (); + gtk_suggestion_entry_set_model (GTK_SUGGESTION_ENTRY (entry), filtered); + + gtk_suggestion_entry_set_use_filter (GTK_SUGGESTION_ENTRY (entry), FALSE); + + expression = gtk_property_expression_new (GTK_TYPE_MATCH_OBJECT, NULL, "string"); + gtk_suggestion_entry_set_expression (GTK_SUGGESTION_ENTRY (entry), expression); + gtk_expression_unref (expression); + + 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); + gtk_suggestion_entry_set_factory (GTK_SUGGESTION_ENTRY (entry), factory); + g_object_unref (factory); + + g_signal_connect (entry, "notify::text", G_CALLBACK (text_changed), words); + + gtk_widget_set_valign (entry, GTK_ALIGN_CENTER); + + gtk_window_set_child (GTK_WINDOW (window), entry); + + gtk_window_present (GTK_WINDOW (window)); + + while (g_list_model_get_n_items (gtk_window_get_toplevels ()) > 0) + g_main_context_iteration (NULL, TRUE); + + g_object_unref (words); + g_object_unref (sorted); + g_object_unref (filtered); + + return 0; +} |