1 | /* |
2 | * Copyright © 2019 Matthias Clasen |
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: Matthias Clasen <mclasen@redhat.com> |
18 | */ |
19 | |
20 | #include "config.h" |
21 | |
22 | #include "gtkstringsorter.h" |
23 | |
24 | #include "gtkintl.h" |
25 | #include "gtksorterprivate.h" |
26 | #include "gtktypebuiltins.h" |
27 | |
28 | /** |
29 | * GtkStringSorter: |
30 | * |
31 | * `GtkStringSorter` is a `GtkSorter` that compares strings. |
32 | * |
33 | * It does the comparison in a linguistically correct way using the |
34 | * current locale by normalizing Unicode strings and possibly case-folding |
35 | * them before performing the comparison. |
36 | * |
37 | * To obtain the strings to compare, this sorter evaluates a |
38 | * [class@Gtk.Expression]. |
39 | */ |
40 | |
41 | struct _GtkStringSorter |
42 | { |
43 | GtkSorter parent_instance; |
44 | |
45 | gboolean ignore_case; |
46 | |
47 | GtkExpression *expression; |
48 | }; |
49 | |
50 | enum { |
51 | PROP_0, |
52 | PROP_EXPRESSION, |
53 | PROP_IGNORE_CASE, |
54 | NUM_PROPERTIES |
55 | }; |
56 | |
57 | G_DEFINE_TYPE (GtkStringSorter, gtk_string_sorter, GTK_TYPE_SORTER) |
58 | |
59 | static GParamSpec *properties[NUM_PROPERTIES] = { NULL, }; |
60 | |
61 | static char * |
62 | gtk_string_sorter_get_key (GtkExpression *expression, |
63 | gboolean ignore_case, |
64 | gpointer item1) |
65 | { |
66 | GValue value = G_VALUE_INIT; |
67 | char *s; |
68 | |
69 | if (expression == NULL) |
70 | return NULL; |
71 | |
72 | if (!gtk_expression_evaluate (self: expression, this_: item1, value: &value)) |
73 | return NULL; |
74 | |
75 | /* If strings are NULL, order them before "". */ |
76 | if (ignore_case) |
77 | { |
78 | char *t; |
79 | |
80 | t = g_utf8_casefold (str: g_value_get_string (value: &value), len: -1); |
81 | s = g_utf8_collate_key (str: t, len: -1); |
82 | g_free (mem: t); |
83 | } |
84 | else |
85 | { |
86 | s = g_utf8_collate_key (str: g_value_get_string (value: &value), len: -1); |
87 | } |
88 | |
89 | g_value_unset (value: &value); |
90 | |
91 | return s; |
92 | } |
93 | |
94 | static GtkOrdering |
95 | gtk_string_sorter_compare (GtkSorter *sorter, |
96 | gpointer item1, |
97 | gpointer item2) |
98 | { |
99 | GtkStringSorter *self = GTK_STRING_SORTER (ptr: sorter); |
100 | char *s1, *s2; |
101 | GtkOrdering result; |
102 | |
103 | if (self->expression == NULL) |
104 | return GTK_ORDERING_EQUAL; |
105 | |
106 | s1 = gtk_string_sorter_get_key (expression: self->expression, ignore_case: self->ignore_case, item1); |
107 | s2 = gtk_string_sorter_get_key (expression: self->expression, ignore_case: self->ignore_case, item1: item2); |
108 | |
109 | result = gtk_ordering_from_cmpfunc (cmpfunc_result: g_strcmp0 (str1: s1, str2: s2)); |
110 | |
111 | g_free (mem: s1); |
112 | g_free (mem: s2); |
113 | |
114 | return result; |
115 | } |
116 | |
117 | static GtkSorterOrder |
118 | gtk_string_sorter_get_order (GtkSorter *sorter) |
119 | { |
120 | GtkStringSorter *self = GTK_STRING_SORTER (ptr: sorter); |
121 | |
122 | if (self->expression == NULL) |
123 | return GTK_SORTER_ORDER_NONE; |
124 | |
125 | return GTK_SORTER_ORDER_PARTIAL; |
126 | } |
127 | |
128 | typedef struct _GtkStringSortKeys GtkStringSortKeys; |
129 | struct _GtkStringSortKeys |
130 | { |
131 | GtkSortKeys keys; |
132 | |
133 | GtkExpression *expression; |
134 | gboolean ignore_case; |
135 | }; |
136 | |
137 | static void |
138 | gtk_string_sort_keys_free (GtkSortKeys *keys) |
139 | { |
140 | GtkStringSortKeys *self = (GtkStringSortKeys *) keys; |
141 | |
142 | gtk_expression_unref (self: self->expression); |
143 | g_slice_free (GtkStringSortKeys, self); |
144 | } |
145 | |
146 | static int |
147 | gtk_string_sort_keys_compare (gconstpointer a, |
148 | gconstpointer b, |
149 | gpointer unused) |
150 | { |
151 | const char *sa = *(const char **) a; |
152 | const char *sb = *(const char **) b; |
153 | |
154 | if (sa == NULL) |
155 | return sb == NULL ? GTK_ORDERING_EQUAL : GTK_ORDERING_LARGER; |
156 | else if (sb == NULL) |
157 | return GTK_ORDERING_SMALLER; |
158 | |
159 | return gtk_ordering_from_cmpfunc (cmpfunc_result: strcmp (s1: sa, s2: sb)); |
160 | } |
161 | |
162 | static gboolean |
163 | gtk_string_sort_keys_is_compatible (GtkSortKeys *keys, |
164 | GtkSortKeys *other) |
165 | { |
166 | return FALSE; |
167 | } |
168 | |
169 | static void |
170 | gtk_string_sort_keys_init_key (GtkSortKeys *keys, |
171 | gpointer item, |
172 | gpointer key_memory) |
173 | { |
174 | GtkStringSortKeys *self = (GtkStringSortKeys *) keys; |
175 | char **key = (char **) key_memory; |
176 | |
177 | *key = gtk_string_sorter_get_key (expression: self->expression, ignore_case: self->ignore_case, item1: item); |
178 | } |
179 | |
180 | static void |
181 | gtk_string_sort_keys_clear_key (GtkSortKeys *keys, |
182 | gpointer key_memory) |
183 | { |
184 | char **key = (char **) key_memory; |
185 | |
186 | g_free (mem: *key); |
187 | } |
188 | |
189 | static const GtkSortKeysClass GTK_STRING_SORT_KEYS_CLASS = |
190 | { |
191 | gtk_string_sort_keys_free, |
192 | gtk_string_sort_keys_compare, |
193 | gtk_string_sort_keys_is_compatible, |
194 | gtk_string_sort_keys_init_key, |
195 | gtk_string_sort_keys_clear_key, |
196 | }; |
197 | |
198 | static GtkSortKeys * |
199 | gtk_string_sort_keys_new (GtkStringSorter *self) |
200 | { |
201 | GtkStringSortKeys *result; |
202 | |
203 | if (self->expression == NULL) |
204 | return gtk_sort_keys_new_equal (); |
205 | |
206 | result = gtk_sort_keys_new (GtkStringSortKeys, |
207 | >K_STRING_SORT_KEYS_CLASS, |
208 | sizeof (char *), |
209 | sizeof (char *)); |
210 | |
211 | result->expression = gtk_expression_ref (self: self->expression); |
212 | result->ignore_case = self->ignore_case; |
213 | |
214 | return (GtkSortKeys *) result; |
215 | } |
216 | |
217 | static void |
218 | gtk_string_sorter_set_property (GObject *object, |
219 | guint prop_id, |
220 | const GValue *value, |
221 | GParamSpec *pspec) |
222 | { |
223 | GtkStringSorter *self = GTK_STRING_SORTER (ptr: object); |
224 | |
225 | switch (prop_id) |
226 | { |
227 | case PROP_EXPRESSION: |
228 | gtk_string_sorter_set_expression (self, expression: gtk_value_get_expression (value)); |
229 | break; |
230 | |
231 | case PROP_IGNORE_CASE: |
232 | gtk_string_sorter_set_ignore_case (self, ignore_case: g_value_get_boolean (value)); |
233 | break; |
234 | |
235 | default: |
236 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
237 | break; |
238 | } |
239 | } |
240 | |
241 | static void |
242 | gtk_string_sorter_get_property (GObject *object, |
243 | guint prop_id, |
244 | GValue *value, |
245 | GParamSpec *pspec) |
246 | { |
247 | GtkStringSorter *self = GTK_STRING_SORTER (ptr: object); |
248 | |
249 | switch (prop_id) |
250 | { |
251 | case PROP_EXPRESSION: |
252 | gtk_value_set_expression (value, expression: self->expression); |
253 | break; |
254 | |
255 | case PROP_IGNORE_CASE: |
256 | g_value_set_boolean (value, v_boolean: self->ignore_case); |
257 | break; |
258 | |
259 | default: |
260 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
261 | break; |
262 | } |
263 | } |
264 | |
265 | static void |
266 | gtk_string_sorter_dispose (GObject *object) |
267 | { |
268 | GtkStringSorter *self = GTK_STRING_SORTER (ptr: object); |
269 | |
270 | g_clear_pointer (&self->expression, gtk_expression_unref); |
271 | |
272 | G_OBJECT_CLASS (gtk_string_sorter_parent_class)->dispose (object); |
273 | } |
274 | |
275 | static void |
276 | gtk_string_sorter_class_init (GtkStringSorterClass *class) |
277 | { |
278 | GtkSorterClass *sorter_class = GTK_SORTER_CLASS (ptr: class); |
279 | GObjectClass *object_class = G_OBJECT_CLASS (class); |
280 | |
281 | sorter_class->compare = gtk_string_sorter_compare; |
282 | sorter_class->get_order = gtk_string_sorter_get_order; |
283 | |
284 | object_class->get_property = gtk_string_sorter_get_property; |
285 | object_class->set_property = gtk_string_sorter_set_property; |
286 | object_class->dispose = gtk_string_sorter_dispose; |
287 | |
288 | /** |
289 | * GtkStringSorter:expression: (type GtkExpression) (attributes org.gtk.Property.get=gtk_string_sorter_get_expression org.gtk.Property.set=gtk_string_sorter_set_expression) |
290 | * |
291 | * The expression to evaluate on item to get a string to compare with. |
292 | */ |
293 | properties[PROP_EXPRESSION] = |
294 | gtk_param_spec_expression (name: "expression" , |
295 | P_("Expression" ), |
296 | P_("Expression to compare with" ), |
297 | flags: G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); |
298 | |
299 | /** |
300 | * GtkStringSorter:ignore-case: (attributes org.gtk.Property.get=gtk_string_sorter_get_ignore_case org.gtk.Property.set=gtk_string_sorter_set_ignore_case) |
301 | * |
302 | * If matching is case sensitive. |
303 | */ |
304 | properties[PROP_IGNORE_CASE] = |
305 | g_param_spec_boolean (name: "ignore-case" , |
306 | P_("Ignore case" ), |
307 | P_("If matching is case sensitive" ), |
308 | TRUE, |
309 | flags: G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); |
310 | |
311 | g_object_class_install_properties (oclass: object_class, n_pspecs: NUM_PROPERTIES, pspecs: properties); |
312 | |
313 | } |
314 | |
315 | static void |
316 | gtk_string_sorter_init (GtkStringSorter *self) |
317 | { |
318 | self->ignore_case = TRUE; |
319 | |
320 | gtk_sorter_changed_with_keys (self: GTK_SORTER (ptr: self), |
321 | change: GTK_SORTER_CHANGE_DIFFERENT, |
322 | keys: gtk_string_sort_keys_new (self)); |
323 | } |
324 | |
325 | /** |
326 | * gtk_string_sorter_new: |
327 | * @expression: (transfer full) (nullable): The expression to evaluate |
328 | * |
329 | * Creates a new string sorter that compares items using the given |
330 | * @expression. |
331 | * |
332 | * Unless an expression is set on it, this sorter will always |
333 | * compare items as invalid. |
334 | * |
335 | * Returns: a new `GtkStringSorter` |
336 | */ |
337 | GtkStringSorter * |
338 | gtk_string_sorter_new (GtkExpression *expression) |
339 | { |
340 | GtkStringSorter *result; |
341 | |
342 | result = g_object_new (GTK_TYPE_STRING_SORTER, |
343 | first_property_name: "expression" , expression, |
344 | NULL); |
345 | |
346 | g_clear_pointer (&expression, gtk_expression_unref); |
347 | |
348 | return result; |
349 | } |
350 | |
351 | /** |
352 | * gtk_string_sorter_get_expression: (attributes org.gtk.Method.get_property=expression) |
353 | * @self: a `GtkStringSorter` |
354 | * |
355 | * Gets the expression that is evaluated to obtain strings from items. |
356 | * |
357 | * Returns: (transfer none) (nullable): a `GtkExpression` |
358 | */ |
359 | GtkExpression * |
360 | gtk_string_sorter_get_expression (GtkStringSorter *self) |
361 | { |
362 | g_return_val_if_fail (GTK_IS_STRING_SORTER (self), NULL); |
363 | |
364 | return self->expression; |
365 | } |
366 | |
367 | /** |
368 | * gtk_string_sorter_set_expression: (attributes org.gtk.Method.set_property=expression) |
369 | * @self: a `GtkStringSorter` |
370 | * @expression: (nullable) (transfer none): a `GtkExpression` |
371 | * |
372 | * Sets the expression that is evaluated to obtain strings from items. |
373 | * |
374 | * The expression must have the type %G_TYPE_STRING. |
375 | */ |
376 | void |
377 | gtk_string_sorter_set_expression (GtkStringSorter *self, |
378 | GtkExpression *expression) |
379 | { |
380 | g_return_if_fail (GTK_IS_STRING_SORTER (self)); |
381 | g_return_if_fail (expression == NULL || gtk_expression_get_value_type (expression) == G_TYPE_STRING); |
382 | |
383 | if (self->expression == expression) |
384 | return; |
385 | |
386 | g_clear_pointer (&self->expression, gtk_expression_unref); |
387 | if (expression) |
388 | self->expression = gtk_expression_ref (self: expression); |
389 | |
390 | gtk_sorter_changed_with_keys (self: GTK_SORTER (ptr: self), |
391 | change: GTK_SORTER_CHANGE_DIFFERENT, |
392 | keys: gtk_string_sort_keys_new (self)); |
393 | |
394 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_EXPRESSION]); |
395 | } |
396 | |
397 | /** |
398 | * gtk_string_sorter_get_ignore_case: (attributes org.gtk.Method.get_property=ignore-case) |
399 | * @self: a `GtkStringSorter` |
400 | * |
401 | * Gets whether the sorter ignores case differences. |
402 | * |
403 | * Returns: %TRUE if @self is ignoring case differences |
404 | */ |
405 | gboolean |
406 | gtk_string_sorter_get_ignore_case (GtkStringSorter *self) |
407 | { |
408 | g_return_val_if_fail (GTK_IS_STRING_SORTER (self), TRUE); |
409 | |
410 | return self->ignore_case; |
411 | } |
412 | |
413 | /** |
414 | * gtk_string_sorter_set_ignore_case: (attributes org.gtk.Method.set_property=ignore-case) |
415 | * @self: a `GtkStringSorter` |
416 | * @ignore_case: %TRUE to ignore case differences |
417 | * |
418 | * Sets whether the sorter will ignore case differences. |
419 | */ |
420 | void |
421 | gtk_string_sorter_set_ignore_case (GtkStringSorter *self, |
422 | gboolean ignore_case) |
423 | { |
424 | g_return_if_fail (GTK_IS_STRING_SORTER (self)); |
425 | |
426 | if (self->ignore_case == ignore_case) |
427 | return; |
428 | |
429 | self->ignore_case = ignore_case; |
430 | |
431 | gtk_sorter_changed_with_keys (self: GTK_SORTER (ptr: self), |
432 | change: ignore_case ? GTK_SORTER_CHANGE_LESS_STRICT : GTK_SORTER_CHANGE_MORE_STRICT, |
433 | keys: gtk_string_sort_keys_new (self)); |
434 | |
435 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_IGNORE_CASE]); |
436 | } |
437 | |