From 02619e8a474bed13b70f24b347f2b439374d1dbe Mon Sep 17 00:00:00 2001 From: Benjamin Otte Date: Sat, 16 Nov 2019 22:15:51 +0100 Subject: [PATCH] filter: Add GtkStringFilter Users provide a search filter and an expression that evaluates the items to a string and then the filter goes and matches those strings to the search term. --- gtk/gtk.h | 1 + gtk/gtkstringfilter.c | 468 +++++++++++++++++++++++++++++++++++++++++ gtk/gtkstringfilter.h | 81 +++++++ gtk/meson.build | 2 + testsuite/gtk/filter.c | 156 ++++++++++++++ 5 files changed, 708 insertions(+) create mode 100644 gtk/gtkstringfilter.c create mode 100644 gtk/gtkstringfilter.h diff --git a/gtk/gtk.h b/gtk/gtk.h index cdc1b14d2e..0cd3061260 100644 --- a/gtk/gtk.h +++ b/gtk/gtk.h @@ -227,6 +227,7 @@ #include #include #include +#include #include #include #include diff --git a/gtk/gtkstringfilter.c b/gtk/gtkstringfilter.c new file mode 100644 index 0000000000..77fa66b5b2 --- /dev/null +++ b/gtk/gtkstringfilter.c @@ -0,0 +1,468 @@ +/* + * Copyright © 2019 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#include "config.h" + +#include "gtkstringfilter.h" + +#include "gtkintl.h" +#include "gtktypebuiltins.h" + +struct _GtkStringFilter +{ + GtkFilter parent_instance; + + char *search; + char *search_prepared; + + gboolean ignore_case; + GtkStringFilterMatchMode match_mode; + + GtkExpression *expression; +}; + +enum { + PROP_0, + PROP_EXPRESSION, + PROP_IGNORE_CASE, + PROP_MATCH_MODE, + PROP_SEARCH, + NUM_PROPERTIES +}; + +G_DEFINE_TYPE (GtkStringFilter, gtk_string_filter, GTK_TYPE_FILTER) + +static GParamSpec *properties[NUM_PROPERTIES] = { NULL, }; + +static char * +gtk_string_filter_prepare (GtkStringFilter *self, + const char *s) +{ + char *tmp; + char *result; + + if (s == NULL || s[0] == '\0') + return NULL; + + tmp = g_utf8_normalize (s, -1, G_NORMALIZE_ALL); + + if (!self->ignore_case) + return tmp; + + result = g_utf8_casefold (tmp, -1); + g_free (tmp); + + return result; +} + +static gboolean +gtk_string_filter_match (GtkFilter *filter, + gpointer item) +{ + GtkStringFilter *self = GTK_STRING_FILTER (filter); + GValue value = G_VALUE_INIT; + char *prepared; + const char *s; + gboolean result; + + if (self->search_prepared == NULL) + return TRUE; + + if (self->expression == NULL || + !gtk_expression_evaluate (self->expression, item, &value)) + return FALSE; + s = g_value_get_string (&value); + if (s == NULL) + return FALSE; + prepared = gtk_string_filter_prepare (self, s); + + switch (self->match_mode) + { + case GTK_STRING_FILTER_MATCH_MODE_EXACT: + result = strcmp (prepared, self->search_prepared) == 0; + break; + case GTK_STRING_FILTER_MATCH_MODE_SUBSTRING: + result = strstr (prepared, self->search_prepared) != NULL; + break; + case GTK_STRING_FILTER_MATCH_MODE_PREFIX: + result = g_str_has_prefix (prepared, self->search_prepared); + break; + default: + g_assert_not_reached (); + } + +#if 0 + g_print ("%s (%s) %s %s (%s)\n", s, prepared, result ? "==" : "!=", self->search, self->search_prepared); +#endif + + g_free (prepared); + g_value_unset (&value); + + return result; +} + +static GtkFilterMatch +gtk_string_filter_get_strictness (GtkFilter *filter) +{ + GtkStringFilter *self = GTK_STRING_FILTER (filter); + + if (self->search == NULL) + return GTK_FILTER_MATCH_ALL; + + if (self->expression == NULL) + return GTK_FILTER_MATCH_NONE; + + return GTK_FILTER_MATCH_SOME; +} + +static void +gtk_string_filter_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtkStringFilter *self = GTK_STRING_FILTER (object); + + switch (prop_id) + { + case PROP_EXPRESSION: + gtk_string_filter_set_expression (self, g_value_get_boxed (value)); + break; + + case PROP_IGNORE_CASE: + gtk_string_filter_set_ignore_case (self, g_value_get_boolean (value)); + break; + + case PROP_MATCH_MODE: + gtk_string_filter_set_match_mode (self, g_value_get_enum (value)); + break; + + case PROP_SEARCH: + gtk_string_filter_set_search (self, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_string_filter_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtkStringFilter *self = GTK_STRING_FILTER (object); + + switch (prop_id) + { + case PROP_EXPRESSION: + g_value_set_boxed (value, self->expression); + break; + + case PROP_IGNORE_CASE: + g_value_set_boolean (value, self->ignore_case); + break; + + case PROP_MATCH_MODE: + g_value_set_enum (value, self->match_mode); + break; + + case PROP_SEARCH: + g_value_set_string (value, self->search); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_string_filter_dispose (GObject *object) +{ + GtkStringFilter *self = GTK_STRING_FILTER (object); + + g_clear_pointer (&self->search, g_free); + g_clear_pointer (&self->search_prepared, g_free); + g_clear_pointer (&self->expression, gtk_expression_unref); + + G_OBJECT_CLASS (gtk_string_filter_parent_class)->dispose (object); +} + +static void +gtk_string_filter_class_init (GtkStringFilterClass *class) +{ + GtkFilterClass *filter_class = GTK_FILTER_CLASS (class); + GObjectClass *object_class = G_OBJECT_CLASS (class); + + filter_class->match = gtk_string_filter_match; + filter_class->get_strictness = gtk_string_filter_get_strictness; + + object_class->get_property = gtk_string_filter_get_property; + object_class->set_property = gtk_string_filter_set_property; + object_class->dispose = gtk_string_filter_dispose; + + /** + * GtkStringFilter:expression: + * + * The expression to evalute on item to get a string to compare with + */ + properties[PROP_EXPRESSION] = + g_param_spec_boxed ("expression", + P_("Expression"), + P_("Expression to compare with"), + GTK_TYPE_EXPRESSION, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GtkStringFilter:ignore-case: + * + * If matching is case sensitive + */ + properties[PROP_IGNORE_CASE] = + g_param_spec_boolean ("ignore-case", + P_("Ignore case"), + P_("If matching is case sensitive"), + TRUE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GtkStringFilter:match-mode: + * + * If exact matches are necessary or if substrings are allowed + */ + properties[PROP_MATCH_MODE] = + g_param_spec_enum ("match-mode", + P_("Match mode"), + P_("If exact matches are necessary or if substrings are allowed"), + GTK_TYPE_STRING_FILTER_MATCH_MODE, + GTK_STRING_FILTER_MATCH_MODE_SUBSTRING, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GtkStringFilter:search: + * + * The search term + */ + properties[PROP_SEARCH] = + g_param_spec_string ("search", + P_("Search"), + P_("The search term"), + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, NUM_PROPERTIES, properties); + +} + +static void +gtk_string_filter_init (GtkStringFilter *self) +{ + self->ignore_case = TRUE; + self->match_mode = GTK_STRING_FILTER_MATCH_MODE_SUBSTRING; + + gtk_filter_changed (GTK_FILTER (self), GTK_FILTER_CHANGE_MATCH_ALL); +} + +/** + * gtk_string_filter_new: + * + * Creates a new string filter. + * + * You will want to set up the filter by providing a string to search for + * and by providing a property to look up on the item. + * + * Returns: a new #GtkStringFilter + **/ +GtkFilter * +gtk_string_filter_new (void) +{ + return g_object_new (GTK_TYPE_STRING_FILTER, NULL); +} + +/** + * gtk_string_filter_get_search: + * @self: a #GtkStringFilter + * + * Gets the search string set via gtk_string_filter_set_search(). + * + * Returns: (allow-none) (transfer none): The search string + **/ +const char * +gtk_string_filter_get_search (GtkStringFilter *self) +{ + g_return_val_if_fail (GTK_IS_STRING_FILTER (self), NULL); + + return self->search; +} + +/** + * gtk_string_filter_set_search: + * @self: a #GtkStringFilter + * @search: (transfer none) (nullable): The string to search for + * or %NULL to clear the search + * + * Sets the string to search for. + **/ +void +gtk_string_filter_set_search (GtkStringFilter *self, + const char *search) +{ + GtkFilterChange change; + + g_return_if_fail (GTK_IS_STRING_FILTER (self)); + + if (g_strcmp0 (self->search, search) == 0) + return; + + if (search == NULL || search[0] == 0) + change = GTK_FILTER_CHANGE_MATCH_ALL; + else if (self->search == NULL) + change = GTK_FILTER_CHANGE_MORE_STRICT; + else if (g_str_has_prefix (search, self->search)) + change = GTK_FILTER_CHANGE_MORE_STRICT; + else if (g_str_has_prefix (self->search, search)) + change = GTK_FILTER_CHANGE_LESS_STRICT; + else + change = GTK_FILTER_CHANGE_DIFFERENT; + + g_free (self->search); + g_free (self->search_prepared); + + self->search = g_strdup (search); + self->search_prepared = gtk_string_filter_prepare (self, search); + + gtk_filter_changed (GTK_FILTER (self), change); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SEARCH]); +} + +GtkExpression * +gtk_string_filter_get_expression (GtkStringFilter *self) +{ + g_return_val_if_fail (GTK_IS_STRING_FILTER (self), NULL); + + return self->expression; +} + +void +gtk_string_filter_set_expression (GtkStringFilter *self, + GtkExpression *expression) +{ + g_return_if_fail (GTK_IS_STRING_FILTER (self)); + if (expression) + { + g_return_if_fail (gtk_expression_get_value_type (expression) == G_TYPE_STRING); + } + + if (self->expression == expression) + return; + + g_clear_pointer (&self->expression, gtk_expression_unref); + self->expression = gtk_expression_ref (expression); + + if (self->search) + gtk_filter_changed (GTK_FILTER (self), GTK_FILTER_CHANGE_DIFFERENT); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_EXPRESSION]); +} + +gboolean +gtk_string_filter_get_ignore_case (GtkStringFilter *self) +{ + g_return_val_if_fail (GTK_IS_STRING_FILTER (self), TRUE); + + return self->ignore_case; +} + +void +gtk_string_filter_set_ignore_case (GtkStringFilter *self, + gboolean ignore_case) +{ + g_return_if_fail (GTK_IS_STRING_FILTER (self)); + + if (self->ignore_case == ignore_case) + return; + + self->ignore_case = ignore_case; + + if (self->search) + { + g_free (self->search_prepared); + self->search_prepared = gtk_string_filter_prepare (self, self->search); + gtk_filter_changed (GTK_FILTER (self), ignore_case ? GTK_FILTER_CHANGE_LESS_STRICT : GTK_FILTER_CHANGE_MORE_STRICT); + } + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_IGNORE_CASE]); +} + +GtkStringFilterMatchMode +gtk_string_filter_get_match_mode (GtkStringFilter *self) +{ + g_return_val_if_fail (GTK_IS_STRING_FILTER (self), GTK_STRING_FILTER_MATCH_MODE_EXACT); + + return self->match_mode; +} + + +void +gtk_string_filter_set_match_mode (GtkStringFilter *self, + GtkStringFilterMatchMode mode) +{ + GtkStringFilterMatchMode old_mode; + + g_return_if_fail (GTK_IS_STRING_FILTER (self)); + + if (self->match_mode == mode) + return; + + old_mode = self->match_mode; + self->match_mode = mode; + + if (self->search && self->expression) + { + switch (old_mode) + { + case GTK_STRING_FILTER_MATCH_MODE_EXACT: + gtk_filter_changed (GTK_FILTER (self), GTK_FILTER_CHANGE_LESS_STRICT); + break; + + case GTK_STRING_FILTER_MATCH_MODE_SUBSTRING: + gtk_filter_changed (GTK_FILTER (self), GTK_FILTER_CHANGE_MORE_STRICT); + break; + + case GTK_STRING_FILTER_MATCH_MODE_PREFIX: + if (mode == GTK_STRING_FILTER_MATCH_MODE_SUBSTRING) + gtk_filter_changed (GTK_FILTER (self), GTK_FILTER_CHANGE_LESS_STRICT); + else + gtk_filter_changed (GTK_FILTER (self), GTK_FILTER_CHANGE_MORE_STRICT); + break; + + default: + g_assert_not_reached (); + break; + } + } + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MATCH_MODE]); +} + + diff --git a/gtk/gtkstringfilter.h b/gtk/gtkstringfilter.h new file mode 100644 index 0000000000..2c5e866bac --- /dev/null +++ b/gtk/gtkstringfilter.h @@ -0,0 +1,81 @@ +/* + * Copyright © 2019 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#ifndef __GTK_STRING_FILTER_H__ +#define __GTK_STRING_FILTER_H__ + +#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION) +#error "Only can be included directly." +#endif + +#include +#include + +G_BEGIN_DECLS + +/** + * GtkStringFilterMatchMode: + * @GTK_STRING_FILTER_MATCH_MODE_EXACT: The search string and + * text must match exactly. + * @GTK_STRING_FILTER_MATCH_MODE_SUBSTRING: The search string + * must be contained as a substring inside the text. + * @GTK_STRING_FILTER_MATCH_MODE_PREFIX: The text must begin + * with the search string. + * + * Specifies how search strings are matched inside text. + */ +typedef enum { + GTK_STRING_FILTER_MATCH_MODE_EXACT, + GTK_STRING_FILTER_MATCH_MODE_SUBSTRING, + GTK_STRING_FILTER_MATCH_MODE_PREFIX +} GtkStringFilterMatchMode; + +#define GTK_TYPE_STRING_FILTER (gtk_string_filter_get_type ()) +GDK_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (GtkStringFilter, gtk_string_filter, GTK, STRING_FILTER, GtkFilter) + +GDK_AVAILABLE_IN_ALL +GtkFilter * gtk_string_filter_new (void); + +GDK_AVAILABLE_IN_ALL +const char * gtk_string_filter_get_search (GtkStringFilter *self); +GDK_AVAILABLE_IN_ALL +void gtk_string_filter_set_search (GtkStringFilter *self, + const char *search); +GDK_AVAILABLE_IN_ALL +GtkExpression * gtk_string_filter_get_expression (GtkStringFilter *self); +GDK_AVAILABLE_IN_ALL +void gtk_string_filter_set_expression (GtkStringFilter *self, + GtkExpression *expression); +GDK_AVAILABLE_IN_ALL +gboolean gtk_string_filter_get_ignore_case (GtkStringFilter *self); +GDK_AVAILABLE_IN_ALL +void gtk_string_filter_set_ignore_case (GtkStringFilter *self, + gboolean ignore_case); +GDK_AVAILABLE_IN_ALL +GtkStringFilterMatchMode gtk_string_filter_get_match_mode (GtkStringFilter *self); +GDK_AVAILABLE_IN_ALL +void gtk_string_filter_set_match_mode (GtkStringFilter *self, + GtkStringFilterMatchMode mode); + + + +G_END_DECLS + +#endif /* __GTK_STRING_FILTER_H__ */ diff --git a/gtk/meson.build b/gtk/meson.build index 485c5f084c..9a1dba92e7 100644 --- a/gtk/meson.build +++ b/gtk/meson.build @@ -368,6 +368,7 @@ gtk_public_sources = files([ 'gtkstacksidebar.c', 'gtkstackswitcher.c', 'gtkstatusbar.c', + 'gtkstringfilter.c', 'gtkstylecontext.c', 'gtkstyleprovider.c', 'gtkswitch.c', @@ -622,6 +623,7 @@ gtk_public_headers = files([ 'gtkstacksidebar.h', 'gtkstackswitcher.h', 'gtkstatusbar.h', + 'gtkstringfilter.h', 'gtkstylecontext.h', 'gtkstyleprovider.h', 'gtkswitch.h', diff --git a/testsuite/gtk/filter.c b/testsuite/gtk/filter.c index af5505e604..8e98f42be5 100644 --- a/testsuite/gtk/filter.c +++ b/testsuite/gtk/filter.c @@ -32,6 +32,90 @@ get (GListModel *model, return GPOINTER_TO_UINT (g_object_get_qdata (object, number_quark)); } +static char * +get_string (gpointer object) +{ + return g_strdup_printf ("%u", GPOINTER_TO_UINT (g_object_get_qdata (object, number_quark))); +} + +static void +append_digit (GString *s, + guint digit) +{ + static char *names[10] = { NULL, "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" }; + + if (digit == 0) + return; + + g_assert (digit < 10); + + if (s->len) + g_string_append_c (s, ' '); + g_string_append (s, names[digit]); +} + +static void +append_below_thousand (GString *s, + guint n) +{ + if (n >= 100) + { + append_digit (s, n / 100); + g_string_append (s, " hundred"); + n %= 100; + } + + if (n >= 20) + { + const char *names[10] = { NULL, NULL, "twenty", "thirty", "fourty", "fifty", "sixty", "seventy", "eighty", "ninety" }; + if (s->len) + g_string_append_c (s, ' '); + g_string_append (s, names [n / 10]); + n %= 10; + } + + if (n >= 10) + { + const char *names[10] = { "ten", "eleven", "twelve", "thirteen", "fourteen", + "fifteen", "sixteen", "seventeen", "eighteen", "nineteen" }; + if (s->len) + g_string_append_c (s, ' '); + g_string_append (s, names [n - 10]); + } + else + { + append_digit (s, n); + } +} + +static char * +get_spelled_out (gpointer object) +{ + guint n = GPOINTER_TO_UINT (g_object_get_qdata (object, number_quark)); + GString *s; + + g_assert (n < 1000000); + + if (n == 0) + return g_strdup ("Zero"); + + s = g_string_new (NULL); + + if (n >= 1000) + { + append_below_thousand (s, n / 1000); + g_string_append (s, " thousand"); + n %= 1000; + } + + append_below_thousand (s, n); + + /* Capitalize first letter so we can do case-sensitive matching */ + s->str[0] = g_ascii_toupper (s->str[0]); + + return g_string_free (s, FALSE); +} + static char * model_to_string (GListModel *model) { @@ -162,6 +246,76 @@ test_any_simple (void) g_object_unref (any); } +static void +test_string_simple (void) +{ + GtkFilterListModel *model; + GtkFilter *filter; + GtkExpression *expr; + + expr = gtk_cclosure_expression_new (G_TYPE_STRING, + NULL, + 0, NULL, + G_CALLBACK (get_string), + NULL, NULL); + + filter = gtk_string_filter_new (); + gtk_string_filter_set_expression (GTK_STRING_FILTER (filter), expr); + + model = new_model (20, filter); + assert_model (model, "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20"); + + gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "1"); + assert_model (model, "1 10 11 12 13 14 15 16 17 18 19"); + + g_object_unref (model); + g_object_unref (filter); + gtk_expression_unref (expr); +} + +static void +test_string_properties (void) +{ + GtkFilterListModel *model; + GtkFilter *filter; + GtkExpression *expr; + + expr = gtk_cclosure_expression_new (G_TYPE_STRING, + NULL, + 0, NULL, + G_CALLBACK (get_spelled_out), + NULL, NULL); + + filter = gtk_string_filter_new (); + gtk_string_filter_set_expression (GTK_STRING_FILTER (filter), expr); + + model = new_model (1000, filter); + gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "thirte"); + assert_model (model, "13 113 213 313 413 513 613 713 813 913"); + + gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "thirteen"); + assert_model (model, "13 113 213 313 413 513 613 713 813 913"); + + gtk_string_filter_set_ignore_case (GTK_STRING_FILTER (filter), FALSE); + assert_model (model, "113 213 313 413 513 613 713 813 913"); + + gtk_string_filter_set_search (GTK_STRING_FILTER (filter), "Thirteen"); + assert_model (model, "13"); + + gtk_string_filter_set_match_mode (GTK_STRING_FILTER (filter), GTK_STRING_FILTER_MATCH_MODE_EXACT); + assert_model (model, "13"); + + gtk_string_filter_set_ignore_case (GTK_STRING_FILTER (filter), TRUE); + assert_model (model, "13"); + + gtk_string_filter_set_match_mode (GTK_STRING_FILTER (filter), GTK_STRING_FILTER_MATCH_MODE_SUBSTRING); + assert_model (model, "13 113 213 313 413 513 613 713 813 913"); + + g_object_unref (model); + g_object_unref (filter); + gtk_expression_unref (expr); +} + int main (int argc, char *argv[]) { @@ -172,6 +326,8 @@ main (int argc, char *argv[]) g_test_add_func ("/filter/simple", test_simple); g_test_add_func ("/filter/any/simple", test_any_simple); + g_test_add_func ("/filter/string/simple", test_string_simple); + g_test_add_func ("/filter/string/properties", test_string_properties); return g_test_run (); }