1/*
2 * Copyright © 2019 Benjamin Otte
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authors: Benjamin Otte <otte@gnome.org>
18 */
19
20#include "config.h"
21
22#include "gtkstringfilter.h"
23
24#include "gtkintl.h"
25#include "gtktypebuiltins.h"
26
27/**
28 * GtkStringFilter:
29 *
30 * `GtkStringFilter` determines whether to include items by comparing
31 * strings to a fixed search term.
32 *
33 * The strings are obtained from the items by evaluating a `GtkExpression`
34 * set with [method@Gtk.StringFilter.set_expression], and they are
35 * compared against a search term set with [method@Gtk.StringFilter.set_search].
36 *
37 * `GtkStringFilter` has several different modes of comparison - it
38 * can match the whole string, just a prefix, or any substring. Use
39 * [method@Gtk.StringFilter.set_match_mode] choose a mode.
40 *
41 * It is also possible to make case-insensitive comparisons, with
42 * [method@Gtk.StringFilter.set_ignore_case].
43 */
44
45struct _GtkStringFilter
46{
47 GtkFilter parent_instance;
48
49 char *search;
50 char *search_prepared;
51
52 gboolean ignore_case;
53 GtkStringFilterMatchMode match_mode;
54
55 GtkExpression *expression;
56};
57
58enum {
59 PROP_0,
60 PROP_EXPRESSION,
61 PROP_IGNORE_CASE,
62 PROP_MATCH_MODE,
63 PROP_SEARCH,
64 NUM_PROPERTIES
65};
66
67G_DEFINE_TYPE (GtkStringFilter, gtk_string_filter, GTK_TYPE_FILTER)
68
69static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
70
71static char *
72gtk_string_filter_prepare (GtkStringFilter *self,
73 const char *s)
74{
75 char *tmp;
76 char *result;
77
78 if (s == NULL || s[0] == '\0')
79 return NULL;
80
81 tmp = g_utf8_normalize (str: s, len: -1, mode: G_NORMALIZE_ALL);
82
83 if (!self->ignore_case)
84 return tmp;
85
86 result = g_utf8_casefold (str: tmp, len: -1);
87 g_free (mem: tmp);
88
89 return result;
90}
91
92/* This is necessary because code just looks at self->search otherwise
93 * and that can be the empty string...
94 */
95static gboolean
96gtk_string_filter_has_search (GtkStringFilter *self)
97{
98 return self->search_prepared != NULL;
99}
100
101static gboolean
102gtk_string_filter_match (GtkFilter *filter,
103 gpointer item)
104{
105 GtkStringFilter *self = GTK_STRING_FILTER (ptr: filter);
106 GValue value = G_VALUE_INIT;
107 char *prepared;
108 const char *s;
109 gboolean result;
110
111 if (!gtk_string_filter_has_search (self))
112 return TRUE;
113
114 if (self->expression == NULL ||
115 !gtk_expression_evaluate (self: self->expression, this_: item, value: &value))
116 return FALSE;
117 s = g_value_get_string (value: &value);
118 prepared = gtk_string_filter_prepare (self, s);
119 if (prepared == NULL)
120 return FALSE;
121
122 switch (self->match_mode)
123 {
124 case GTK_STRING_FILTER_MATCH_MODE_EXACT:
125 result = strcmp (s1: prepared, s2: self->search_prepared) == 0;
126 break;
127 case GTK_STRING_FILTER_MATCH_MODE_SUBSTRING:
128 result = strstr (haystack: prepared, needle: self->search_prepared) != NULL;
129 break;
130 case GTK_STRING_FILTER_MATCH_MODE_PREFIX:
131 result = g_str_has_prefix (str: prepared, prefix: self->search_prepared);
132 break;
133 default:
134 g_assert_not_reached ();
135 }
136
137#if 0
138 g_print ("%s (%s) %s %s (%s)\n", s, prepared, result ? "==" : "!=", self->search, self->search_prepared);
139#endif
140
141 g_free (mem: prepared);
142 g_value_unset (value: &value);
143
144 return result;
145}
146
147static GtkFilterMatch
148gtk_string_filter_get_strictness (GtkFilter *filter)
149{
150 GtkStringFilter *self = GTK_STRING_FILTER (ptr: filter);
151
152 if (!gtk_string_filter_has_search (self))
153 return GTK_FILTER_MATCH_ALL;
154
155 if (self->expression == NULL)
156 return GTK_FILTER_MATCH_NONE;
157
158 return GTK_FILTER_MATCH_SOME;
159}
160
161static void
162gtk_string_filter_set_property (GObject *object,
163 guint prop_id,
164 const GValue *value,
165 GParamSpec *pspec)
166{
167 GtkStringFilter *self = GTK_STRING_FILTER (ptr: object);
168
169 switch (prop_id)
170 {
171 case PROP_EXPRESSION:
172 gtk_string_filter_set_expression (self, expression: gtk_value_get_expression (value));
173 break;
174
175 case PROP_IGNORE_CASE:
176 gtk_string_filter_set_ignore_case (self, ignore_case: g_value_get_boolean (value));
177 break;
178
179 case PROP_MATCH_MODE:
180 gtk_string_filter_set_match_mode (self, mode: g_value_get_enum (value));
181 break;
182
183 case PROP_SEARCH:
184 gtk_string_filter_set_search (self, search: g_value_get_string (value));
185 break;
186
187 default:
188 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
189 break;
190 }
191}
192
193static void
194gtk_string_filter_get_property (GObject *object,
195 guint prop_id,
196 GValue *value,
197 GParamSpec *pspec)
198{
199 GtkStringFilter *self = GTK_STRING_FILTER (ptr: object);
200
201 switch (prop_id)
202 {
203 case PROP_EXPRESSION:
204 gtk_value_set_expression (value, expression: self->expression);
205 break;
206
207 case PROP_IGNORE_CASE:
208 g_value_set_boolean (value, v_boolean: self->ignore_case);
209 break;
210
211 case PROP_MATCH_MODE:
212 g_value_set_enum (value, v_enum: self->match_mode);
213 break;
214
215 case PROP_SEARCH:
216 g_value_set_string (value, v_string: self->search);
217 break;
218
219 default:
220 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
221 break;
222 }
223}
224
225static void
226gtk_string_filter_dispose (GObject *object)
227{
228 GtkStringFilter *self = GTK_STRING_FILTER (ptr: object);
229
230 g_clear_pointer (&self->search, g_free);
231 g_clear_pointer (&self->search_prepared, g_free);
232 g_clear_pointer (&self->expression, gtk_expression_unref);
233
234 G_OBJECT_CLASS (gtk_string_filter_parent_class)->dispose (object);
235}
236
237static void
238gtk_string_filter_class_init (GtkStringFilterClass *class)
239{
240 GtkFilterClass *filter_class = GTK_FILTER_CLASS (ptr: class);
241 GObjectClass *object_class = G_OBJECT_CLASS (class);
242
243 filter_class->match = gtk_string_filter_match;
244 filter_class->get_strictness = gtk_string_filter_get_strictness;
245
246 object_class->get_property = gtk_string_filter_get_property;
247 object_class->set_property = gtk_string_filter_set_property;
248 object_class->dispose = gtk_string_filter_dispose;
249
250 /**
251 * GtkStringFilter:expression: (type GtkExpression) (attributes org.gtk.Property.get=gtk_string_filter_get_expression org.gtk.Property.set=gtk_string_filter_set_expression)
252 *
253 * The expression to evaluate on item to get a string to compare with.
254 */
255 properties[PROP_EXPRESSION] =
256 gtk_param_spec_expression (name: "expression",
257 P_("Expression"),
258 P_("Expression to compare with"),
259 flags: G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
260
261 /**
262 * GtkStringFilter:ignore-case: (attributes org.gtk.Property.get=gtk_string_filter_get_ignore_case org.gtk.Property.set=gtk_string_filter_set_ignore_case)
263 *
264 * If matching is case sensitive.
265 */
266 properties[PROP_IGNORE_CASE] =
267 g_param_spec_boolean (name: "ignore-case",
268 P_("Ignore case"),
269 P_("If matching is case sensitive"),
270 TRUE,
271 flags: G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
272
273 /**
274 * GtkStringFilter:match-mode: (attributes org.gtk.Property.get=gtk_string_filter_get_match_mode org.gtk.Property.set=gtk_string_filter_set_match_mode)
275 *
276 * If exact matches are necessary or if substrings are allowed.
277 */
278 properties[PROP_MATCH_MODE] =
279 g_param_spec_enum (name: "match-mode",
280 P_("Match mode"),
281 P_("If exact matches are necessary or if substrings are allowed"),
282 enum_type: GTK_TYPE_STRING_FILTER_MATCH_MODE,
283 default_value: GTK_STRING_FILTER_MATCH_MODE_SUBSTRING,
284 flags: G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
285
286 /**
287 * GtkStringFilter:search: (attributes org.gtk.Property.get=gtk_string_filter_get_search org.gtk.Property.set=gtk_string_filter_set_search)
288 *
289 * The search term.
290 */
291 properties[PROP_SEARCH] =
292 g_param_spec_string (name: "search",
293 P_("Search"),
294 P_("The search term"),
295 NULL,
296 flags: G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
297
298 g_object_class_install_properties (oclass: object_class, n_pspecs: NUM_PROPERTIES, pspecs: properties);
299
300}
301
302static void
303gtk_string_filter_init (GtkStringFilter *self)
304{
305 self->ignore_case = TRUE;
306 self->match_mode = GTK_STRING_FILTER_MATCH_MODE_SUBSTRING;
307}
308
309/**
310 * gtk_string_filter_new:
311 * @expression: (transfer full) (nullable): The expression to evaluate
312 *
313 * Creates a new string filter.
314 *
315 * You will want to set up the filter by providing a string to search for
316 * and by providing a property to look up on the item.
317 *
318 * Returns: a new `GtkStringFilter`
319 */
320GtkStringFilter *
321gtk_string_filter_new (GtkExpression *expression)
322{
323 GtkStringFilter *result;
324
325 result = g_object_new (GTK_TYPE_STRING_FILTER,
326 first_property_name: "expression", expression,
327 NULL);
328
329 g_clear_pointer (&expression, gtk_expression_unref);
330
331 return result;
332}
333
334/**
335 * gtk_string_filter_get_search: (attributes org.gtk.Method.get_property=search)
336 * @self: a `GtkStringFilter`
337 *
338 * Gets the search term.
339 *
340 * Returns: (nullable) (transfer none): The search term
341 **/
342const char *
343gtk_string_filter_get_search (GtkStringFilter *self)
344{
345 g_return_val_if_fail (GTK_IS_STRING_FILTER (self), NULL);
346
347 return self->search;
348}
349
350/**
351 * gtk_string_filter_set_search: (attributes org.gtk.Method.set_property=search)
352 * @self: a `GtkStringFilter`
353 * @search: (transfer none) (nullable): The string to search for
354 * or %NULL to clear the search
355 *
356 * Sets the string to search for.
357 */
358void
359gtk_string_filter_set_search (GtkStringFilter *self,
360 const char *search)
361{
362 GtkFilterChange change;
363
364 g_return_if_fail (GTK_IS_STRING_FILTER (self));
365
366 if (g_strcmp0 (str1: self->search, str2: search) == 0)
367 return;
368
369 if (search == NULL || search[0] == 0)
370 change = GTK_FILTER_CHANGE_LESS_STRICT;
371 else if (!gtk_string_filter_has_search (self))
372 change = GTK_FILTER_CHANGE_MORE_STRICT;
373 else if (g_str_has_prefix (str: search, prefix: self->search))
374 change = GTK_FILTER_CHANGE_MORE_STRICT;
375 else if (g_str_has_prefix (str: self->search, prefix: search))
376 change = GTK_FILTER_CHANGE_LESS_STRICT;
377 else
378 change = GTK_FILTER_CHANGE_DIFFERENT;
379
380 g_free (mem: self->search);
381 g_free (mem: self->search_prepared);
382
383 self->search = g_strdup (str: search);
384 self->search_prepared = gtk_string_filter_prepare (self, s: search);
385
386 gtk_filter_changed (self: GTK_FILTER (ptr: self), change);
387
388 g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_SEARCH]);
389}
390
391/**
392 * gtk_string_filter_get_expression: (attributes org.gtk.Method.get_property=expression)
393 * @self: a `GtkStringFilter`
394 *
395 * Gets the expression that the string filter uses to
396 * obtain strings from items.
397 *
398 * Returns: (transfer none) (nullable): a `GtkExpression`
399 */
400GtkExpression *
401gtk_string_filter_get_expression (GtkStringFilter *self)
402{
403 g_return_val_if_fail (GTK_IS_STRING_FILTER (self), NULL);
404
405 return self->expression;
406}
407
408/**
409 * gtk_string_filter_set_expression: (attributes org.gtk.Method.set_property=expression)
410 * @self: a `GtkStringFilter`
411 * @expression: (nullable): a `GtkExpression`
412 *
413 * Sets the expression that the string filter uses to
414 * obtain strings from items.
415 *
416 * The expression must have a value type of %G_TYPE_STRING.
417 */
418void
419gtk_string_filter_set_expression (GtkStringFilter *self,
420 GtkExpression *expression)
421{
422 g_return_if_fail (GTK_IS_STRING_FILTER (self));
423 g_return_if_fail (expression == NULL || gtk_expression_get_value_type (expression) == G_TYPE_STRING);
424
425 if (self->expression == expression)
426 return;
427
428 g_clear_pointer (&self->expression, gtk_expression_unref);
429 self->expression = gtk_expression_ref (self: expression);
430
431 if (gtk_string_filter_has_search (self))
432 gtk_filter_changed (self: GTK_FILTER (ptr: self), change: GTK_FILTER_CHANGE_DIFFERENT);
433
434 g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_EXPRESSION]);
435}
436
437/**
438 * gtk_string_filter_get_ignore_case: (attributes org.gtk.Method.get_property=ignore-case)
439 * @self: a `GtkStringFilter`
440 *
441 * Returns whether the filter ignores case differences.
442 *
443 * Returns: %TRUE if the filter ignores case
444 */
445gboolean
446gtk_string_filter_get_ignore_case (GtkStringFilter *self)
447{
448 g_return_val_if_fail (GTK_IS_STRING_FILTER (self), TRUE);
449
450 return self->ignore_case;
451}
452
453/**
454 * gtk_string_filter_set_ignore_case: (attributes org.gtk.Method.set_property=ignore-case)
455 * @self: a `GtkStringFilter`
456 * @ignore_case: %TRUE to ignore case
457 *
458 * Sets whether the filter ignores case differences.
459 */
460void
461gtk_string_filter_set_ignore_case (GtkStringFilter *self,
462 gboolean ignore_case)
463{
464 g_return_if_fail (GTK_IS_STRING_FILTER (self));
465
466 if (self->ignore_case == ignore_case)
467 return;
468
469 self->ignore_case = ignore_case;
470
471 if (self->search)
472 {
473 g_free (mem: self->search_prepared);
474 self->search_prepared = gtk_string_filter_prepare (self, s: self->search);
475 gtk_filter_changed (self: GTK_FILTER (ptr: self), change: ignore_case ? GTK_FILTER_CHANGE_LESS_STRICT : GTK_FILTER_CHANGE_MORE_STRICT);
476 }
477
478 g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_IGNORE_CASE]);
479}
480
481/**
482 * gtk_string_filter_get_match_mode: (attributes org.gtk.Method.get_property=match-mode)
483 * @self: a `GtkStringFilter`
484 *
485 * Returns the match mode that the filter is using.
486 *
487 * Returns: the match mode of the filter
488 */
489GtkStringFilterMatchMode
490gtk_string_filter_get_match_mode (GtkStringFilter *self)
491{
492 g_return_val_if_fail (GTK_IS_STRING_FILTER (self), GTK_STRING_FILTER_MATCH_MODE_EXACT);
493
494 return self->match_mode;
495}
496
497/**
498 * gtk_string_filter_set_match_mode: (attributes org.gtk.Method.set_property=match-mode)
499 * @self: a `GtkStringFilter`
500 * @mode: the new match mode
501 *
502 * Sets the match mode for the filter.
503 */
504void
505gtk_string_filter_set_match_mode (GtkStringFilter *self,
506 GtkStringFilterMatchMode mode)
507{
508 GtkStringFilterMatchMode old_mode;
509
510 g_return_if_fail (GTK_IS_STRING_FILTER (self));
511
512 if (self->match_mode == mode)
513 return;
514
515 old_mode = self->match_mode;
516 self->match_mode = mode;
517
518 if (self->search_prepared && self->expression)
519 {
520 switch (old_mode)
521 {
522 case GTK_STRING_FILTER_MATCH_MODE_EXACT:
523 gtk_filter_changed (self: GTK_FILTER (ptr: self), change: GTK_FILTER_CHANGE_LESS_STRICT);
524 break;
525
526 case GTK_STRING_FILTER_MATCH_MODE_SUBSTRING:
527 gtk_filter_changed (self: GTK_FILTER (ptr: self), change: GTK_FILTER_CHANGE_MORE_STRICT);
528 break;
529
530 case GTK_STRING_FILTER_MATCH_MODE_PREFIX:
531 if (mode == GTK_STRING_FILTER_MATCH_MODE_SUBSTRING)
532 gtk_filter_changed (self: GTK_FILTER (ptr: self), change: GTK_FILTER_CHANGE_LESS_STRICT);
533 else
534 gtk_filter_changed (self: GTK_FILTER (ptr: self), change: GTK_FILTER_CHANGE_MORE_STRICT);
535 break;
536
537 default:
538 g_assert_not_reached ();
539 break;
540 }
541 }
542
543 g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_MATCH_MODE]);
544}
545

source code of gtk/gtk/gtkstringfilter.c