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 | |
45 | struct _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 | |
58 | enum { |
59 | PROP_0, |
60 | PROP_EXPRESSION, |
61 | PROP_IGNORE_CASE, |
62 | PROP_MATCH_MODE, |
63 | PROP_SEARCH, |
64 | NUM_PROPERTIES |
65 | }; |
66 | |
67 | G_DEFINE_TYPE (GtkStringFilter, gtk_string_filter, GTK_TYPE_FILTER) |
68 | |
69 | static GParamSpec *properties[NUM_PROPERTIES] = { NULL, }; |
70 | |
71 | static char * |
72 | gtk_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 | */ |
95 | static gboolean |
96 | gtk_string_filter_has_search (GtkStringFilter *self) |
97 | { |
98 | return self->search_prepared != NULL; |
99 | } |
100 | |
101 | static gboolean |
102 | gtk_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 | |
147 | static GtkFilterMatch |
148 | gtk_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 | |
161 | static void |
162 | gtk_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 | |
193 | static void |
194 | gtk_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 | |
225 | static void |
226 | gtk_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 | |
237 | static void |
238 | gtk_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 | |
302 | static void |
303 | gtk_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 | */ |
320 | GtkStringFilter * |
321 | gtk_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 | **/ |
342 | const char * |
343 | gtk_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 | */ |
358 | void |
359 | gtk_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 | */ |
400 | GtkExpression * |
401 | gtk_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 | */ |
418 | void |
419 | gtk_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 | */ |
445 | gboolean |
446 | gtk_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 | */ |
460 | void |
461 | gtk_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 | */ |
489 | GtkStringFilterMatchMode |
490 | gtk_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 | */ |
504 | void |
505 | gtk_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 | |