1 | /* |
2 | * Copyright © 2018 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 "gtkfilterlistmodel.h" |
23 | |
24 | #include "gtkbitset.h" |
25 | #include "gtkintl.h" |
26 | #include "gtkprivate.h" |
27 | |
28 | /** |
29 | * GtkFilterListModel: |
30 | * |
31 | * `GtkFilterListModel` is a list model that filters the elements of |
32 | * the underlying model according to a `GtkFilter`. |
33 | * |
34 | * It hides some elements from the other model according to |
35 | * criteria given by a `GtkFilter`. |
36 | * |
37 | * The model can be set up to do incremental searching, so that |
38 | * filtering long lists doesn't block the UI. See |
39 | * [method@Gtk.FilterListModel.set_incremental] for details. |
40 | */ |
41 | |
42 | enum { |
43 | PROP_0, |
44 | PROP_FILTER, |
45 | PROP_INCREMENTAL, |
46 | PROP_MODEL, |
47 | PROP_PENDING, |
48 | NUM_PROPERTIES |
49 | }; |
50 | |
51 | struct _GtkFilterListModel |
52 | { |
53 | GObject parent_instance; |
54 | |
55 | GListModel *model; |
56 | GtkFilter *filter; |
57 | GtkFilterMatch strictness; |
58 | gboolean incremental; |
59 | |
60 | GtkBitset *matches; /* NULL if strictness != GTK_FILTER_MATCH_SOME */ |
61 | GtkBitset *pending; /* not yet filtered items or NULL if all filtered */ |
62 | guint pending_cb; /* idle callback handle */ |
63 | }; |
64 | |
65 | struct _GtkFilterListModelClass |
66 | { |
67 | GObjectClass parent_class; |
68 | }; |
69 | |
70 | static GParamSpec *properties[NUM_PROPERTIES] = { NULL, }; |
71 | |
72 | static GType |
73 | gtk_filter_list_model_get_item_type (GListModel *list) |
74 | { |
75 | return G_TYPE_OBJECT; |
76 | } |
77 | |
78 | static guint |
79 | gtk_filter_list_model_get_n_items (GListModel *list) |
80 | { |
81 | GtkFilterListModel *self = GTK_FILTER_LIST_MODEL (ptr: list); |
82 | |
83 | switch (self->strictness) |
84 | { |
85 | case GTK_FILTER_MATCH_NONE: |
86 | return 0; |
87 | |
88 | case GTK_FILTER_MATCH_ALL: |
89 | return g_list_model_get_n_items (list: self->model); |
90 | |
91 | case GTK_FILTER_MATCH_SOME: |
92 | return gtk_bitset_get_size (self: self->matches); |
93 | |
94 | default: |
95 | g_assert_not_reached (); |
96 | return 0; |
97 | } |
98 | } |
99 | |
100 | static gpointer |
101 | gtk_filter_list_model_get_item (GListModel *list, |
102 | guint position) |
103 | { |
104 | GtkFilterListModel *self = GTK_FILTER_LIST_MODEL (ptr: list); |
105 | guint unfiltered; |
106 | |
107 | switch (self->strictness) |
108 | { |
109 | case GTK_FILTER_MATCH_NONE: |
110 | return NULL; |
111 | |
112 | case GTK_FILTER_MATCH_ALL: |
113 | unfiltered = position; |
114 | break; |
115 | |
116 | case GTK_FILTER_MATCH_SOME: |
117 | unfiltered = gtk_bitset_get_nth (self: self->matches, nth: position); |
118 | if (unfiltered == 0 && position >= gtk_bitset_get_size (self: self->matches)) |
119 | return NULL; |
120 | break; |
121 | |
122 | default: |
123 | g_assert_not_reached (); |
124 | } |
125 | |
126 | return g_list_model_get_item (list: self->model, position: unfiltered); |
127 | } |
128 | |
129 | static void |
130 | gtk_filter_list_model_model_init (GListModelInterface *iface) |
131 | { |
132 | iface->get_item_type = gtk_filter_list_model_get_item_type; |
133 | iface->get_n_items = gtk_filter_list_model_get_n_items; |
134 | iface->get_item = gtk_filter_list_model_get_item; |
135 | } |
136 | |
137 | G_DEFINE_TYPE_WITH_CODE (GtkFilterListModel, gtk_filter_list_model, G_TYPE_OBJECT, |
138 | G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_filter_list_model_model_init)) |
139 | |
140 | static gboolean |
141 | gtk_filter_list_model_run_filter_on_item (GtkFilterListModel *self, |
142 | guint position) |
143 | { |
144 | gpointer item; |
145 | gboolean visible; |
146 | |
147 | /* all other cases should have beeen optimized away */ |
148 | g_assert (self->strictness == GTK_FILTER_MATCH_SOME); |
149 | |
150 | item = g_list_model_get_item (list: self->model, position); |
151 | visible = gtk_filter_match (self: self->filter, item); |
152 | g_object_unref (object: item); |
153 | |
154 | return visible; |
155 | } |
156 | |
157 | static void |
158 | gtk_filter_list_model_run_filter (GtkFilterListModel *self, |
159 | guint n_steps) |
160 | { |
161 | GtkBitsetIter iter; |
162 | guint i, pos; |
163 | gboolean more; |
164 | |
165 | g_return_if_fail (GTK_IS_FILTER_LIST_MODEL (self)); |
166 | |
167 | if (self->pending == NULL) |
168 | return; |
169 | |
170 | for (i = 0, more = gtk_bitset_iter_init_first (iter: &iter, set: self->pending, value: &pos); |
171 | i < n_steps && more; |
172 | i++, more = gtk_bitset_iter_next (iter: &iter, value: &pos)) |
173 | { |
174 | if (gtk_filter_list_model_run_filter_on_item (self, position: pos)) |
175 | gtk_bitset_add (self: self->matches, value: pos); |
176 | } |
177 | |
178 | if (more) |
179 | gtk_bitset_remove_range_closed (self: self->pending, first: 0, last: pos - 1); |
180 | else |
181 | g_clear_pointer (&self->pending, gtk_bitset_unref); |
182 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_PENDING]); |
183 | |
184 | return; |
185 | } |
186 | |
187 | static void |
188 | gtk_filter_list_model_stop_filtering (GtkFilterListModel *self) |
189 | { |
190 | gboolean notify_pending = self->pending != NULL; |
191 | |
192 | g_clear_pointer (&self->pending, gtk_bitset_unref); |
193 | g_clear_handle_id (&self->pending_cb, g_source_remove); |
194 | |
195 | if (notify_pending) |
196 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_PENDING]); |
197 | } |
198 | |
199 | static void |
200 | gtk_filter_list_model_emit_items_changed_for_changes (GtkFilterListModel *self, |
201 | GtkBitset *old) |
202 | { |
203 | GtkBitset *changes; |
204 | |
205 | changes = gtk_bitset_copy (self: self->matches); |
206 | gtk_bitset_difference (self: changes, other: old); |
207 | if (!gtk_bitset_is_empty (self: changes)) |
208 | { |
209 | guint min, max; |
210 | |
211 | min = gtk_bitset_get_minimum (self: changes); |
212 | max = gtk_bitset_get_maximum (self: changes); |
213 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), |
214 | position: min > 0 ? gtk_bitset_get_size_in_range (self: self->matches, first: 0, last: min - 1) : 0, |
215 | removed: gtk_bitset_get_size_in_range (self: old, first: min, last: max), |
216 | added: gtk_bitset_get_size_in_range (self: self->matches, first: min, last: max)); |
217 | } |
218 | gtk_bitset_unref (self: changes); |
219 | gtk_bitset_unref (self: old); |
220 | } |
221 | |
222 | static gboolean |
223 | gtk_filter_list_model_run_filter_cb (gpointer data) |
224 | { |
225 | GtkFilterListModel *self = data; |
226 | GtkBitset *old; |
227 | |
228 | old = gtk_bitset_copy (self: self->matches); |
229 | gtk_filter_list_model_run_filter (self, n_steps: 512); |
230 | |
231 | if (self->pending == NULL) |
232 | gtk_filter_list_model_stop_filtering (self); |
233 | |
234 | gtk_filter_list_model_emit_items_changed_for_changes (self, old); |
235 | |
236 | return G_SOURCE_CONTINUE; |
237 | } |
238 | |
239 | /* NB: bitset is (transfer full) */ |
240 | static void |
241 | gtk_filter_list_model_start_filtering (GtkFilterListModel *self, |
242 | GtkBitset *items) |
243 | { |
244 | if (self->pending) |
245 | { |
246 | gtk_bitset_union (self: self->pending, other: items); |
247 | gtk_bitset_unref (self: items); |
248 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_PENDING]); |
249 | return; |
250 | } |
251 | |
252 | if (gtk_bitset_is_empty (self: items)) |
253 | { |
254 | gtk_bitset_unref (self: items); |
255 | return; |
256 | } |
257 | |
258 | self->pending = items; |
259 | |
260 | if (!self->incremental) |
261 | { |
262 | gtk_filter_list_model_run_filter (self, G_MAXUINT); |
263 | g_assert (self->pending == NULL); |
264 | return; |
265 | } |
266 | |
267 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_PENDING]); |
268 | g_assert (self->pending_cb == 0); |
269 | self->pending_cb = g_idle_add (function: gtk_filter_list_model_run_filter_cb, data: self); |
270 | gdk_source_set_static_name_by_id (tag: self->pending_cb, name: "[gtk] gtk_filter_list_model_run_filter_cb" ); |
271 | } |
272 | |
273 | static void |
274 | gtk_filter_list_model_items_changed_cb (GListModel *model, |
275 | guint position, |
276 | guint removed, |
277 | guint added, |
278 | GtkFilterListModel *self) |
279 | { |
280 | guint filter_removed, filter_added; |
281 | |
282 | switch (self->strictness) |
283 | { |
284 | case GTK_FILTER_MATCH_NONE: |
285 | return; |
286 | |
287 | case GTK_FILTER_MATCH_ALL: |
288 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position, removed, added); |
289 | return; |
290 | |
291 | case GTK_FILTER_MATCH_SOME: |
292 | break; |
293 | |
294 | default: |
295 | g_assert_not_reached (); |
296 | } |
297 | |
298 | if (removed > 0) |
299 | filter_removed = gtk_bitset_get_size_in_range (self: self->matches, first: position, last: position + removed - 1); |
300 | else |
301 | filter_removed = 0; |
302 | |
303 | gtk_bitset_splice (self: self->matches, position, removed, added); |
304 | if (self->pending) |
305 | gtk_bitset_splice (self: self->pending, position, removed, added); |
306 | |
307 | if (added > 0) |
308 | { |
309 | gtk_filter_list_model_start_filtering (self, items: gtk_bitset_new_range (start: position, n_items: added)); |
310 | filter_added = gtk_bitset_get_size_in_range (self: self->matches, first: position, last: position + added - 1); |
311 | } |
312 | else |
313 | filter_added = 0; |
314 | |
315 | if (filter_removed > 0 || filter_added > 0) |
316 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), |
317 | position: position > 0 ? gtk_bitset_get_size_in_range (self: self->matches, first: 0, last: position - 1) : 0, |
318 | removed: filter_removed, added: filter_added); |
319 | } |
320 | |
321 | static void |
322 | gtk_filter_list_model_set_property (GObject *object, |
323 | guint prop_id, |
324 | const GValue *value, |
325 | GParamSpec *pspec) |
326 | { |
327 | GtkFilterListModel *self = GTK_FILTER_LIST_MODEL (ptr: object); |
328 | |
329 | switch (prop_id) |
330 | { |
331 | case PROP_FILTER: |
332 | gtk_filter_list_model_set_filter (self, filter: g_value_get_object (value)); |
333 | break; |
334 | |
335 | case PROP_INCREMENTAL: |
336 | gtk_filter_list_model_set_incremental (self, incremental: g_value_get_boolean (value)); |
337 | break; |
338 | |
339 | case PROP_MODEL: |
340 | gtk_filter_list_model_set_model (self, model: g_value_get_object (value)); |
341 | break; |
342 | |
343 | default: |
344 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
345 | break; |
346 | } |
347 | } |
348 | |
349 | static void |
350 | gtk_filter_list_model_get_property (GObject *object, |
351 | guint prop_id, |
352 | GValue *value, |
353 | GParamSpec *pspec) |
354 | { |
355 | GtkFilterListModel *self = GTK_FILTER_LIST_MODEL (ptr: object); |
356 | |
357 | switch (prop_id) |
358 | { |
359 | case PROP_FILTER: |
360 | g_value_set_object (value, v_object: self->filter); |
361 | break; |
362 | |
363 | case PROP_INCREMENTAL: |
364 | g_value_set_boolean (value, v_boolean: self->incremental); |
365 | break; |
366 | |
367 | case PROP_MODEL: |
368 | g_value_set_object (value, v_object: self->model); |
369 | break; |
370 | |
371 | case PROP_PENDING: |
372 | g_value_set_uint (value, v_uint: gtk_filter_list_model_get_pending (self)); |
373 | break; |
374 | |
375 | default: |
376 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
377 | break; |
378 | } |
379 | } |
380 | |
381 | static void |
382 | gtk_filter_list_model_clear_model (GtkFilterListModel *self) |
383 | { |
384 | if (self->model == NULL) |
385 | return; |
386 | |
387 | gtk_filter_list_model_stop_filtering (self); |
388 | g_signal_handlers_disconnect_by_func (self->model, gtk_filter_list_model_items_changed_cb, self); |
389 | g_clear_object (&self->model); |
390 | if (self->matches) |
391 | gtk_bitset_remove_all (self: self->matches); |
392 | } |
393 | |
394 | static void |
395 | gtk_filter_list_model_refilter (GtkFilterListModel *self, |
396 | GtkFilterChange change) |
397 | { |
398 | GtkFilterMatch new_strictness; |
399 | |
400 | if (self->model == NULL) |
401 | new_strictness = GTK_FILTER_MATCH_NONE; |
402 | else if (self->filter == NULL) |
403 | new_strictness = GTK_FILTER_MATCH_ALL; |
404 | else |
405 | new_strictness = gtk_filter_get_strictness (self: self->filter); |
406 | |
407 | /* don't set self->strictness yet so get_n_items() and friends return old values */ |
408 | |
409 | switch (new_strictness) |
410 | { |
411 | case GTK_FILTER_MATCH_NONE: |
412 | { |
413 | guint n_before = g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self)); |
414 | g_clear_pointer (&self->matches, gtk_bitset_unref); |
415 | self->strictness = new_strictness; |
416 | gtk_filter_list_model_stop_filtering (self); |
417 | if (n_before > 0) |
418 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: 0, removed: n_before, added: 0); |
419 | } |
420 | break; |
421 | |
422 | case GTK_FILTER_MATCH_ALL: |
423 | switch (self->strictness) |
424 | { |
425 | case GTK_FILTER_MATCH_NONE: |
426 | self->strictness = new_strictness; |
427 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: 0, removed: 0, added: g_list_model_get_n_items (list: self->model)); |
428 | break; |
429 | case GTK_FILTER_MATCH_ALL: |
430 | self->strictness = new_strictness; |
431 | break; |
432 | default: |
433 | case GTK_FILTER_MATCH_SOME: |
434 | { |
435 | guint n_before, n_after; |
436 | |
437 | gtk_filter_list_model_stop_filtering (self); |
438 | self->strictness = new_strictness; |
439 | n_after = g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self)); |
440 | n_before = gtk_bitset_get_size (self: self->matches); |
441 | if (n_before == n_after) |
442 | { |
443 | g_clear_pointer (&self->matches, gtk_bitset_unref); |
444 | } |
445 | else |
446 | { |
447 | GtkBitset *inverse; |
448 | guint start, end; |
449 | |
450 | inverse = gtk_bitset_new_range (start: 0, n_items: n_after); |
451 | gtk_bitset_subtract (self: inverse, other: self->matches); |
452 | /* otherwise all items would be visible */ |
453 | g_assert (!gtk_bitset_is_empty (inverse)); |
454 | |
455 | /* find first filtered */ |
456 | start = gtk_bitset_get_minimum (self: inverse); |
457 | end = n_after - gtk_bitset_get_maximum (self: inverse) - 1; |
458 | |
459 | gtk_bitset_unref (self: inverse); |
460 | |
461 | g_clear_pointer (&self->matches, gtk_bitset_unref); |
462 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: start, removed: n_before - end - start, added: n_after - end - start); |
463 | } |
464 | } |
465 | break; |
466 | } |
467 | break; |
468 | |
469 | default: |
470 | g_assert_not_reached (); |
471 | break; |
472 | |
473 | case GTK_FILTER_MATCH_SOME: |
474 | { |
475 | GtkBitset *old, *pending; |
476 | |
477 | if (self->matches == NULL) |
478 | { |
479 | if (self->strictness == GTK_FILTER_MATCH_ALL) |
480 | old = gtk_bitset_new_range (start: 0, n_items: g_list_model_get_n_items (list: self->model)); |
481 | else |
482 | old = gtk_bitset_new_empty (); |
483 | } |
484 | else |
485 | { |
486 | old = self->matches; |
487 | } |
488 | self->strictness = new_strictness; |
489 | switch (change) |
490 | { |
491 | default: |
492 | g_assert_not_reached (); |
493 | /* fall thru */ |
494 | case GTK_FILTER_CHANGE_DIFFERENT: |
495 | self->matches = gtk_bitset_new_empty (); |
496 | pending = gtk_bitset_new_range (start: 0, n_items: g_list_model_get_n_items (list: self->model)); |
497 | break; |
498 | case GTK_FILTER_CHANGE_LESS_STRICT: |
499 | self->matches = gtk_bitset_copy (self: old); |
500 | pending = gtk_bitset_new_range (start: 0, n_items: g_list_model_get_n_items (list: self->model)); |
501 | gtk_bitset_subtract (self: pending, other: self->matches); |
502 | break; |
503 | case GTK_FILTER_CHANGE_MORE_STRICT: |
504 | self->matches = gtk_bitset_new_empty (); |
505 | pending = gtk_bitset_copy (self: old); |
506 | break; |
507 | } |
508 | gtk_filter_list_model_start_filtering (self, items: pending); |
509 | |
510 | gtk_filter_list_model_emit_items_changed_for_changes (self, old); |
511 | } |
512 | } |
513 | } |
514 | |
515 | static void |
516 | gtk_filter_list_model_filter_changed_cb (GtkFilter *filter, |
517 | GtkFilterChange change, |
518 | GtkFilterListModel *self) |
519 | { |
520 | gtk_filter_list_model_refilter (self, change); |
521 | } |
522 | |
523 | static void |
524 | gtk_filter_list_model_clear_filter (GtkFilterListModel *self) |
525 | { |
526 | if (self->filter == NULL) |
527 | return; |
528 | |
529 | g_signal_handlers_disconnect_by_func (self->filter, gtk_filter_list_model_filter_changed_cb, self); |
530 | g_clear_object (&self->filter); |
531 | } |
532 | |
533 | static void |
534 | gtk_filter_list_model_dispose (GObject *object) |
535 | { |
536 | GtkFilterListModel *self = GTK_FILTER_LIST_MODEL (ptr: object); |
537 | |
538 | gtk_filter_list_model_clear_model (self); |
539 | gtk_filter_list_model_clear_filter (self); |
540 | g_clear_pointer (&self->matches, gtk_bitset_unref); |
541 | |
542 | G_OBJECT_CLASS (gtk_filter_list_model_parent_class)->dispose (object); |
543 | } |
544 | |
545 | static void |
546 | gtk_filter_list_model_class_init (GtkFilterListModelClass *class) |
547 | { |
548 | GObjectClass *gobject_class = G_OBJECT_CLASS (class); |
549 | |
550 | gobject_class->set_property = gtk_filter_list_model_set_property; |
551 | gobject_class->get_property = gtk_filter_list_model_get_property; |
552 | gobject_class->dispose = gtk_filter_list_model_dispose; |
553 | |
554 | /** |
555 | * GtkFilterListModel:filter: (attributes org.gtk.Property.get=gtk_filter_list_model_get_filter org.gtk.Property.set=gtk_filter_list_model_set_filter) |
556 | * |
557 | * The filter for this model. |
558 | */ |
559 | properties[PROP_FILTER] = |
560 | g_param_spec_object (name: "filter" , |
561 | P_("Filter" ), |
562 | P_("The filter set for this model" ), |
563 | GTK_TYPE_FILTER, |
564 | GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); |
565 | |
566 | /** |
567 | * GtkFilterListModel:incremental: (attributes org.gtk.Property.get=gtk_filter_list_model_get_incremental org.gtk.Property.set=gtk_filter_list_model_set_incremental) |
568 | * |
569 | * If the model should filter items incrementally. |
570 | */ |
571 | properties[PROP_INCREMENTAL] = |
572 | g_param_spec_boolean (name: "incremental" , |
573 | P_("Incremental" ), |
574 | P_("Filter items incrementally" ), |
575 | FALSE, |
576 | GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); |
577 | |
578 | /** |
579 | * GtkFilterListModel:model: (attributes org.gtk.Property.get=gtk_filter_list_model_get_model org.gtk.Property.set=gtk_filter_list_model_set_model) |
580 | * |
581 | * The model being filtered. |
582 | */ |
583 | properties[PROP_MODEL] = |
584 | g_param_spec_object (name: "model" , |
585 | P_("Model" ), |
586 | P_("The model being filtered" ), |
587 | G_TYPE_LIST_MODEL, |
588 | GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); |
589 | |
590 | /** |
591 | * GtkFilterListModel:pending: (attributes org.gtk.Property.get=gtk_filter_list_model_get_pending) |
592 | * |
593 | * Number of items not yet filtered. |
594 | */ |
595 | properties[PROP_PENDING] = |
596 | g_param_spec_uint (name: "pending" , |
597 | P_("Pending" ), |
598 | P_("Number of items not yet filtered" ), |
599 | minimum: 0, G_MAXUINT, default_value: 0, |
600 | GTK_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); |
601 | |
602 | g_object_class_install_properties (oclass: gobject_class, n_pspecs: NUM_PROPERTIES, pspecs: properties); |
603 | } |
604 | |
605 | static void |
606 | gtk_filter_list_model_init (GtkFilterListModel *self) |
607 | { |
608 | self->strictness = GTK_FILTER_MATCH_NONE; |
609 | } |
610 | |
611 | /** |
612 | * gtk_filter_list_model_new: |
613 | * @model: (nullable) (transfer full): the model to sort |
614 | * @filter: (nullable) (transfer full): filter |
615 | * |
616 | * Creates a new `GtkFilterListModel` that will filter @model using the given |
617 | * @filter. |
618 | * |
619 | * Returns: a new `GtkFilterListModel` |
620 | **/ |
621 | GtkFilterListModel * |
622 | gtk_filter_list_model_new (GListModel *model, |
623 | GtkFilter *filter) |
624 | { |
625 | GtkFilterListModel *result; |
626 | |
627 | g_return_val_if_fail (model == NULL || G_IS_LIST_MODEL (model), NULL); |
628 | g_return_val_if_fail (filter == NULL || GTK_IS_FILTER (filter), NULL); |
629 | |
630 | result = g_object_new (GTK_TYPE_FILTER_LIST_MODEL, |
631 | first_property_name: "model" , model, |
632 | "filter" , filter, |
633 | NULL); |
634 | |
635 | /* consume the references */ |
636 | g_clear_object (&model); |
637 | g_clear_object (&filter); |
638 | |
639 | return result; |
640 | } |
641 | |
642 | /** |
643 | * gtk_filter_list_model_set_filter: (attributes org.gtk.Method.set_property=filter) |
644 | * @self: a `GtkFilterListModel` |
645 | * @filter: (nullable) (transfer none): filter to use |
646 | * |
647 | * Sets the filter used to filter items. |
648 | **/ |
649 | void |
650 | gtk_filter_list_model_set_filter (GtkFilterListModel *self, |
651 | GtkFilter *filter) |
652 | { |
653 | g_return_if_fail (GTK_IS_FILTER_LIST_MODEL (self)); |
654 | g_return_if_fail (filter == NULL || GTK_IS_FILTER (filter)); |
655 | |
656 | if (self->filter == filter) |
657 | return; |
658 | |
659 | gtk_filter_list_model_clear_filter (self); |
660 | |
661 | if (filter) |
662 | { |
663 | self->filter = g_object_ref (filter); |
664 | g_signal_connect (filter, "changed" , G_CALLBACK (gtk_filter_list_model_filter_changed_cb), self); |
665 | gtk_filter_list_model_filter_changed_cb (filter, change: GTK_FILTER_CHANGE_DIFFERENT, self); |
666 | } |
667 | else |
668 | { |
669 | gtk_filter_list_model_refilter (self, change: GTK_FILTER_CHANGE_LESS_STRICT); |
670 | } |
671 | |
672 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_FILTER]); |
673 | } |
674 | |
675 | /** |
676 | * gtk_filter_list_model_get_filter: (attributes org.gtk.Method.get_property=filter) |
677 | * @self: a `GtkFilterListModel` |
678 | * |
679 | * Gets the `GtkFilter` currently set on @self. |
680 | * |
681 | * Returns: (nullable) (transfer none): The filter currently in use |
682 | */ |
683 | GtkFilter * |
684 | gtk_filter_list_model_get_filter (GtkFilterListModel *self) |
685 | { |
686 | g_return_val_if_fail (GTK_IS_FILTER_LIST_MODEL (self), FALSE); |
687 | |
688 | return self->filter; |
689 | } |
690 | |
691 | /** |
692 | * gtk_filter_list_model_set_model: (attributes org.gtk.Method.set_property=model) |
693 | * @self: a `GtkFilterListModel` |
694 | * @model: (nullable): The model to be filtered |
695 | * |
696 | * Sets the model to be filtered. |
697 | * |
698 | * Note that GTK makes no effort to ensure that @model conforms to |
699 | * the item type of @self. It assumes that the caller knows what they |
700 | * are doing and have set up an appropriate filter to ensure that item |
701 | * types match. |
702 | */ |
703 | void |
704 | gtk_filter_list_model_set_model (GtkFilterListModel *self, |
705 | GListModel *model) |
706 | { |
707 | guint removed, added; |
708 | |
709 | g_return_if_fail (GTK_IS_FILTER_LIST_MODEL (self)); |
710 | g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model)); |
711 | /* Note: We don't check for matching item type here, we just assume the |
712 | * filter func takes care of filtering wrong items. */ |
713 | |
714 | if (self->model == model) |
715 | return; |
716 | |
717 | removed = g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self)); |
718 | gtk_filter_list_model_clear_model (self); |
719 | |
720 | if (model) |
721 | { |
722 | self->model = g_object_ref (model); |
723 | g_signal_connect (model, "items-changed" , G_CALLBACK (gtk_filter_list_model_items_changed_cb), self); |
724 | if (removed == 0) |
725 | { |
726 | self->strictness = GTK_FILTER_MATCH_NONE; |
727 | gtk_filter_list_model_refilter (self, change: GTK_FILTER_CHANGE_LESS_STRICT); |
728 | added = 0; |
729 | } |
730 | else if (self->matches) |
731 | { |
732 | gtk_filter_list_model_start_filtering (self, items: gtk_bitset_new_range (start: 0, n_items: g_list_model_get_n_items (list: model))); |
733 | added = gtk_bitset_get_size (self: self->matches); |
734 | } |
735 | else |
736 | { |
737 | added = g_list_model_get_n_items (list: model); |
738 | } |
739 | } |
740 | else |
741 | { |
742 | self->strictness = GTK_FILTER_MATCH_NONE; |
743 | added = 0; |
744 | } |
745 | |
746 | if (removed > 0 || added > 0) |
747 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: 0, removed, added); |
748 | |
749 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_MODEL]); |
750 | } |
751 | |
752 | /** |
753 | * gtk_filter_list_model_get_model: (attributes org.gtk.Method.get_property=model) |
754 | * @self: a `GtkFilterListModel` |
755 | * |
756 | * Gets the model currently filtered or %NULL if none. |
757 | * |
758 | * Returns: (nullable) (transfer none): The model that gets filtered |
759 | */ |
760 | GListModel * |
761 | gtk_filter_list_model_get_model (GtkFilterListModel *self) |
762 | { |
763 | g_return_val_if_fail (GTK_IS_FILTER_LIST_MODEL (self), NULL); |
764 | |
765 | return self->model; |
766 | } |
767 | |
768 | /** |
769 | * gtk_filter_list_model_set_incremental: (attributes org.gtk.Method.set_property=incremental) |
770 | * @self: a `GtkFilterListModel` |
771 | * @incremental: %TRUE to enable incremental filtering |
772 | * |
773 | * Sets the filter model to do an incremental sort. |
774 | * |
775 | * When incremental filtering is enabled, the `GtkFilterListModel` will not |
776 | * run filters immediately, but will instead queue an idle handler that |
777 | * incrementally filters the items and adds them to the list. This of course |
778 | * means that items are not instantly added to the list, but only appear |
779 | * incrementally. |
780 | * |
781 | * When your filter blocks the UI while filtering, you might consider |
782 | * turning this on. Depending on your model and filters, this may become |
783 | * interesting around 10,000 to 100,000 items. |
784 | * |
785 | * By default, incremental filtering is disabled. |
786 | * |
787 | * See [method@Gtk.FilterListModel.get_pending] for progress information |
788 | * about an ongoing incremental filtering operation. |
789 | **/ |
790 | void |
791 | gtk_filter_list_model_set_incremental (GtkFilterListModel *self, |
792 | gboolean incremental) |
793 | { |
794 | g_return_if_fail (GTK_IS_FILTER_LIST_MODEL (self)); |
795 | |
796 | if (self->incremental == incremental) |
797 | return; |
798 | |
799 | self->incremental = incremental; |
800 | |
801 | if (!incremental) |
802 | { |
803 | GtkBitset *old; |
804 | gtk_filter_list_model_run_filter (self, G_MAXUINT); |
805 | |
806 | old = gtk_bitset_copy (self: self->matches); |
807 | gtk_filter_list_model_run_filter (self, n_steps: 512); |
808 | |
809 | gtk_filter_list_model_stop_filtering (self); |
810 | |
811 | gtk_filter_list_model_emit_items_changed_for_changes (self, old); |
812 | } |
813 | |
814 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_INCREMENTAL]); |
815 | } |
816 | |
817 | /** |
818 | * gtk_filter_list_model_get_incremental: (attributes org.gtk.Method.get_property=incremental) |
819 | * @self: a `GtkFilterListModel` |
820 | * |
821 | * Returns whether incremental filtering is enabled. |
822 | * |
823 | * See [method@Gtk.FilterListModel.set_incremental]. |
824 | * |
825 | * Returns: %TRUE if incremental filtering is enabled |
826 | */ |
827 | gboolean |
828 | gtk_filter_list_model_get_incremental (GtkFilterListModel *self) |
829 | { |
830 | g_return_val_if_fail (GTK_IS_FILTER_LIST_MODEL (self), FALSE); |
831 | |
832 | return self->incremental; |
833 | } |
834 | |
835 | /** |
836 | * gtk_filter_list_model_get_pending: (attributes org.gtk.Method.get_property=pending) |
837 | * @self: a `GtkFilterListModel` |
838 | * |
839 | * Returns the number of items that have not been filtered yet. |
840 | * |
841 | * You can use this value to check if @self is busy filtering by |
842 | * comparing the return value to 0 or you can compute the percentage |
843 | * of the filter remaining by dividing the return value by the total |
844 | * number of items in the underlying model: |
845 | * |
846 | * ```c |
847 | * pending = gtk_filter_list_model_get_pending (self); |
848 | * model = gtk_filter_list_model_get_model (self); |
849 | * percentage = pending / (double) g_list_model_get_n_items (model); |
850 | * ``` |
851 | * |
852 | * If no filter operation is ongoing - in particular when |
853 | * [property@Gtk.FilterListModel:incremental] is %FALSE - this |
854 | * function returns 0. |
855 | * |
856 | * Returns: The number of items not yet filtered |
857 | */ |
858 | guint |
859 | gtk_filter_list_model_get_pending (GtkFilterListModel *self) |
860 | { |
861 | g_return_val_if_fail (GTK_IS_FILTER_LIST_MODEL (self), FALSE); |
862 | |
863 | if (self->pending == NULL) |
864 | return 0; |
865 | |
866 | return gtk_bitset_get_size (self: self->pending); |
867 | } |
868 | |