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 "gtkmaplistmodel.h" |
23 | |
24 | #include "gtkrbtreeprivate.h" |
25 | #include "gtkintl.h" |
26 | #include "gtkprivate.h" |
27 | |
28 | /** |
29 | * GtkMapListModel: |
30 | * |
31 | * A `GtkMapListModel` maps the items in a list model to different items. |
32 | * |
33 | * `GtkMapListModel` uses a [callback@Gtk.MapListModelMapFunc]. |
34 | * |
35 | * Example: Create a list of `GtkEventControllers` |
36 | * ```c |
37 | * static gpointer |
38 | * map_to_controllers (gpointer widget, |
39 | * gpointer data) |
40 | * { |
41 | * gpointer result = gtk_widget_observe_controllers (widget); |
42 | * g_object_unref (widget); |
43 | * return result; |
44 | * } |
45 | * |
46 | * widgets = gtk_widget_observe_children (widget); |
47 | * |
48 | * controllers = gtk_map_list_model_new (widgets, |
49 | * map_to_controllers, |
50 | * NULL, NULL); |
51 | * |
52 | * model = gtk_flatten_list_model_new (GTK_TYPE_EVENT_CONTROLLER, |
53 | * controllers); |
54 | * ``` |
55 | * |
56 | * `GtkMapListModel` will attempt to discard the mapped objects as soon as |
57 | * they are no longer needed and recreate them if necessary. |
58 | */ |
59 | |
60 | enum { |
61 | PROP_0, |
62 | PROP_HAS_MAP, |
63 | PROP_MODEL, |
64 | NUM_PROPERTIES |
65 | }; |
66 | |
67 | typedef struct _MapNode MapNode; |
68 | typedef struct _MapAugment MapAugment; |
69 | |
70 | struct _MapNode |
71 | { |
72 | guint n_items; |
73 | gpointer item; /* can only be set when n_items == 1 */ |
74 | }; |
75 | |
76 | struct _MapAugment |
77 | { |
78 | guint n_items; |
79 | }; |
80 | |
81 | struct _GtkMapListModel |
82 | { |
83 | GObject parent_instance; |
84 | |
85 | GListModel *model; |
86 | GtkMapListModelMapFunc map_func; |
87 | gpointer user_data; |
88 | GDestroyNotify user_destroy; |
89 | |
90 | GtkRbTree *items; /* NULL if map_func == NULL */ |
91 | }; |
92 | |
93 | struct _GtkMapListModelClass |
94 | { |
95 | GObjectClass parent_class; |
96 | }; |
97 | |
98 | static GParamSpec *properties[NUM_PROPERTIES] = { NULL, }; |
99 | |
100 | static MapNode * |
101 | gtk_map_list_model_get_nth (GtkRbTree *tree, |
102 | guint position, |
103 | guint *out_start_pos) |
104 | { |
105 | MapNode *node, *tmp; |
106 | guint start_pos = position; |
107 | |
108 | node = gtk_rb_tree_get_root (tree); |
109 | |
110 | while (node) |
111 | { |
112 | tmp = gtk_rb_tree_node_get_left (node); |
113 | if (tmp) |
114 | { |
115 | MapAugment *aug = gtk_rb_tree_get_augment (tree, node: tmp); |
116 | if (position < aug->n_items) |
117 | { |
118 | node = tmp; |
119 | continue; |
120 | } |
121 | position -= aug->n_items; |
122 | } |
123 | |
124 | if (position < node->n_items) |
125 | { |
126 | start_pos -= position; |
127 | break; |
128 | } |
129 | position -= node->n_items; |
130 | |
131 | node = gtk_rb_tree_node_get_right (node); |
132 | } |
133 | |
134 | if (out_start_pos) |
135 | *out_start_pos = start_pos; |
136 | |
137 | return node; |
138 | } |
139 | |
140 | static GType |
141 | gtk_map_list_model_get_item_type (GListModel *list) |
142 | { |
143 | return G_TYPE_OBJECT; |
144 | } |
145 | |
146 | static guint |
147 | gtk_map_list_model_get_n_items (GListModel *list) |
148 | { |
149 | GtkMapListModel *self = GTK_MAP_LIST_MODEL (ptr: list); |
150 | |
151 | if (self->model == NULL) |
152 | return 0; |
153 | |
154 | return g_list_model_get_n_items (list: self->model); |
155 | } |
156 | |
157 | static gpointer |
158 | gtk_map_list_model_get_item (GListModel *list, |
159 | guint position) |
160 | { |
161 | GtkMapListModel *self = GTK_MAP_LIST_MODEL (ptr: list); |
162 | MapNode *node; |
163 | guint offset; |
164 | |
165 | if (self->model == NULL) |
166 | return NULL; |
167 | |
168 | if (self->items == NULL) |
169 | return g_list_model_get_item (list: self->model, position); |
170 | |
171 | node = gtk_map_list_model_get_nth (tree: self->items, position, out_start_pos: &offset); |
172 | if (node == NULL) |
173 | return NULL; |
174 | |
175 | if (node->item) |
176 | return g_object_ref (node->item); |
177 | |
178 | if (offset != position) |
179 | { |
180 | MapNode *before = gtk_rb_tree_insert_before (tree: self->items, node); |
181 | before->n_items = position - offset; |
182 | node->n_items -= before->n_items; |
183 | gtk_rb_tree_node_mark_dirty (node); |
184 | } |
185 | |
186 | if (node->n_items > 1) |
187 | { |
188 | MapNode *after = gtk_rb_tree_insert_after (tree: self->items, node); |
189 | after->n_items = node->n_items - 1; |
190 | node->n_items = 1; |
191 | gtk_rb_tree_node_mark_dirty (node); |
192 | } |
193 | |
194 | node->item = self->map_func (g_list_model_get_item (list: self->model, position), self->user_data); |
195 | g_object_add_weak_pointer (object: node->item, weak_pointer_location: &node->item); |
196 | |
197 | return node->item; |
198 | } |
199 | |
200 | static void |
201 | gtk_map_list_model_model_init (GListModelInterface *iface) |
202 | { |
203 | iface->get_item_type = gtk_map_list_model_get_item_type; |
204 | iface->get_n_items = gtk_map_list_model_get_n_items; |
205 | iface->get_item = gtk_map_list_model_get_item; |
206 | } |
207 | |
208 | G_DEFINE_TYPE_WITH_CODE (GtkMapListModel, gtk_map_list_model, G_TYPE_OBJECT, |
209 | G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_map_list_model_model_init)) |
210 | |
211 | static void |
212 | gtk_map_list_model_items_changed_cb (GListModel *model, |
213 | guint position, |
214 | guint removed, |
215 | guint added, |
216 | GtkMapListModel *self) |
217 | { |
218 | MapNode *node; |
219 | guint start, end; |
220 | |
221 | if (self->items == NULL) |
222 | { |
223 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position, removed, added); |
224 | return; |
225 | } |
226 | |
227 | node = gtk_map_list_model_get_nth (tree: self->items, position, out_start_pos: &start); |
228 | g_assert (start <= position); |
229 | |
230 | while (removed > 0) |
231 | { |
232 | end = start + node->n_items; |
233 | if (start == position && end <= position + removed) |
234 | { |
235 | MapNode *next = gtk_rb_tree_node_get_next (node); |
236 | removed -= node->n_items; |
237 | gtk_rb_tree_remove (tree: self->items, node); |
238 | node = next; |
239 | } |
240 | else |
241 | { |
242 | if (end >= position + removed) |
243 | { |
244 | node->n_items -= removed; |
245 | removed = 0; |
246 | gtk_rb_tree_node_mark_dirty (node); |
247 | } |
248 | else if (start < position) |
249 | { |
250 | guint overlap = node->n_items - (position - start); |
251 | node->n_items -= overlap; |
252 | gtk_rb_tree_node_mark_dirty (node); |
253 | removed -= overlap; |
254 | start = position; |
255 | node = gtk_rb_tree_node_get_next (node); |
256 | } |
257 | } |
258 | } |
259 | |
260 | if (added) |
261 | { |
262 | if (node == NULL) |
263 | node = gtk_rb_tree_insert_before (tree: self->items, NULL); |
264 | else if (node->item) |
265 | node = gtk_rb_tree_insert_after (tree: self->items, node); |
266 | |
267 | node->n_items += added; |
268 | gtk_rb_tree_node_mark_dirty (node); |
269 | } |
270 | |
271 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position, removed, added); |
272 | } |
273 | |
274 | static void |
275 | gtk_map_list_model_set_property (GObject *object, |
276 | guint prop_id, |
277 | const GValue *value, |
278 | GParamSpec *pspec) |
279 | { |
280 | GtkMapListModel *self = GTK_MAP_LIST_MODEL (ptr: object); |
281 | |
282 | switch (prop_id) |
283 | { |
284 | case PROP_MODEL: |
285 | gtk_map_list_model_set_model (self, model: g_value_get_object (value)); |
286 | break; |
287 | |
288 | default: |
289 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
290 | break; |
291 | } |
292 | } |
293 | |
294 | static void |
295 | gtk_map_list_model_get_property (GObject *object, |
296 | guint prop_id, |
297 | GValue *value, |
298 | GParamSpec *pspec) |
299 | { |
300 | GtkMapListModel *self = GTK_MAP_LIST_MODEL (ptr: object); |
301 | |
302 | switch (prop_id) |
303 | { |
304 | case PROP_HAS_MAP: |
305 | g_value_set_boolean (value, v_boolean: self->items != NULL); |
306 | break; |
307 | |
308 | case PROP_MODEL: |
309 | g_value_set_object (value, v_object: self->model); |
310 | break; |
311 | |
312 | default: |
313 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
314 | break; |
315 | } |
316 | } |
317 | |
318 | static void |
319 | gtk_map_list_model_clear_model (GtkMapListModel *self) |
320 | { |
321 | if (self->model == NULL) |
322 | return; |
323 | |
324 | g_signal_handlers_disconnect_by_func (self->model, gtk_map_list_model_items_changed_cb, self); |
325 | g_clear_object (&self->model); |
326 | } |
327 | |
328 | static void |
329 | gtk_map_list_model_dispose (GObject *object) |
330 | { |
331 | GtkMapListModel *self = GTK_MAP_LIST_MODEL (ptr: object); |
332 | |
333 | gtk_map_list_model_clear_model (self); |
334 | if (self->user_destroy) |
335 | self->user_destroy (self->user_data); |
336 | self->map_func = NULL; |
337 | self->user_data = NULL; |
338 | self->user_destroy = NULL; |
339 | g_clear_pointer (&self->items, gtk_rb_tree_unref); |
340 | |
341 | G_OBJECT_CLASS (gtk_map_list_model_parent_class)->dispose (object); |
342 | } |
343 | |
344 | static void |
345 | gtk_map_list_model_class_init (GtkMapListModelClass *class) |
346 | { |
347 | GObjectClass *gobject_class = G_OBJECT_CLASS (class); |
348 | |
349 | gobject_class->set_property = gtk_map_list_model_set_property; |
350 | gobject_class->get_property = gtk_map_list_model_get_property; |
351 | gobject_class->dispose = gtk_map_list_model_dispose; |
352 | |
353 | /** |
354 | * GtkMapListModel:has-map: (attributes org.gtk.Property.get=gtk_map_list_model_has_map) |
355 | * |
356 | * If a map is set for this model |
357 | */ |
358 | properties[PROP_HAS_MAP] = |
359 | g_param_spec_boolean (name: "has-map" , |
360 | P_("has map" ), |
361 | P_("If a map is set for this model" ), |
362 | FALSE, |
363 | GTK_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); |
364 | |
365 | /** |
366 | * GtkMapListModel:model: (attributes org.gtk.Property.get=gtk_map_list_model_get_model org.gtk.Property.set=gtk_map_list_model_set_model) |
367 | * |
368 | * The model being mapped. |
369 | */ |
370 | properties[PROP_MODEL] = |
371 | g_param_spec_object (name: "model" , |
372 | P_("Model" ), |
373 | P_("The model being mapped" ), |
374 | G_TYPE_LIST_MODEL, |
375 | GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY); |
376 | |
377 | g_object_class_install_properties (oclass: gobject_class, n_pspecs: NUM_PROPERTIES, pspecs: properties); |
378 | } |
379 | |
380 | static void |
381 | gtk_map_list_model_init (GtkMapListModel *self) |
382 | { |
383 | } |
384 | |
385 | |
386 | static void |
387 | gtk_map_list_model_augment (GtkRbTree *map, |
388 | gpointer _aug, |
389 | gpointer _node, |
390 | gpointer left, |
391 | gpointer right) |
392 | { |
393 | MapNode *node = _node; |
394 | MapAugment *aug = _aug; |
395 | |
396 | aug->n_items = node->n_items; |
397 | |
398 | if (left) |
399 | { |
400 | MapAugment *left_aug = gtk_rb_tree_get_augment (tree: map, node: left); |
401 | aug->n_items += left_aug->n_items; |
402 | } |
403 | if (right) |
404 | { |
405 | MapAugment *right_aug = gtk_rb_tree_get_augment (tree: map, node: right); |
406 | aug->n_items += right_aug->n_items; |
407 | } |
408 | } |
409 | |
410 | /** |
411 | * gtk_map_list_model_new: |
412 | * @model: (transfer full) (nullable): The model to map |
413 | * @map_func: (nullable): map function |
414 | * @user_data: (closure): user data passed to @map_func |
415 | * @user_destroy: destroy notifier for @user_data |
416 | * |
417 | * Creates a new `GtkMapListModel` for the given arguments. |
418 | * |
419 | * Returns: a new `GtkMapListModel` |
420 | */ |
421 | GtkMapListModel * |
422 | gtk_map_list_model_new (GListModel *model, |
423 | GtkMapListModelMapFunc map_func, |
424 | gpointer user_data, |
425 | GDestroyNotify user_destroy) |
426 | { |
427 | GtkMapListModel *result; |
428 | |
429 | g_return_val_if_fail (model == NULL || G_IS_LIST_MODEL (model), NULL); |
430 | |
431 | result = g_object_new (GTK_TYPE_MAP_LIST_MODEL, |
432 | first_property_name: "model" , model, |
433 | NULL); |
434 | |
435 | /* consume the reference */ |
436 | g_clear_object (&model); |
437 | |
438 | if (map_func) |
439 | gtk_map_list_model_set_map_func (self: result, map_func, user_data, user_destroy); |
440 | |
441 | return result; |
442 | } |
443 | |
444 | static void |
445 | gtk_map_list_model_clear_node (gpointer _node) |
446 | { |
447 | MapNode *node = _node; |
448 | |
449 | if (node->item) |
450 | g_object_remove_weak_pointer (object: node->item, weak_pointer_location: &node->item); |
451 | } |
452 | |
453 | static void |
454 | gtk_map_list_model_init_items (GtkMapListModel *self) |
455 | { |
456 | if (self->map_func && self->model) |
457 | { |
458 | guint n_items; |
459 | |
460 | if (self->items) |
461 | { |
462 | gtk_rb_tree_remove_all (tree: self->items); |
463 | } |
464 | else |
465 | { |
466 | self->items = gtk_rb_tree_new (MapNode, |
467 | MapAugment, |
468 | gtk_map_list_model_augment, |
469 | gtk_map_list_model_clear_node, |
470 | NULL); |
471 | } |
472 | |
473 | n_items = g_list_model_get_n_items (list: self->model); |
474 | if (n_items) |
475 | { |
476 | MapNode *node = gtk_rb_tree_insert_before (tree: self->items, NULL); |
477 | node->n_items = g_list_model_get_n_items (list: self->model); |
478 | gtk_rb_tree_node_mark_dirty (node); |
479 | } |
480 | } |
481 | else |
482 | { |
483 | g_clear_pointer (&self->items, gtk_rb_tree_unref); |
484 | } |
485 | } |
486 | |
487 | /** |
488 | * gtk_map_list_model_set_map_func: |
489 | * @self: a `GtkMapListModel` |
490 | * @map_func: (nullable): map function |
491 | * @user_data: (closure): user data passed to @map_func |
492 | * @user_destroy: destroy notifier for @user_data |
493 | * |
494 | * Sets the function used to map items. |
495 | * |
496 | * The function will be called whenever an item needs to be mapped |
497 | * and must return the item to use for the given input item. |
498 | * |
499 | * Note that `GtkMapListModel` may call this function multiple times |
500 | * on the same item, because it may delete items it doesn't need anymore. |
501 | * |
502 | * GTK makes no effort to ensure that @map_func conforms to the item type |
503 | * of @self. It assumes that the caller knows what they are doing and the map |
504 | * function returns items of the appropriate type. |
505 | */ |
506 | void |
507 | gtk_map_list_model_set_map_func (GtkMapListModel *self, |
508 | GtkMapListModelMapFunc map_func, |
509 | gpointer user_data, |
510 | GDestroyNotify user_destroy) |
511 | { |
512 | gboolean was_maped, will_be_maped; |
513 | guint n_items; |
514 | |
515 | g_return_if_fail (GTK_IS_MAP_LIST_MODEL (self)); |
516 | g_return_if_fail (map_func != NULL || (user_data == NULL && !user_destroy)); |
517 | |
518 | was_maped = self->map_func != NULL; |
519 | will_be_maped = map_func != NULL; |
520 | |
521 | if (!was_maped && !will_be_maped) |
522 | return; |
523 | |
524 | if (self->user_destroy) |
525 | self->user_destroy (self->user_data); |
526 | |
527 | self->map_func = map_func; |
528 | self->user_data = user_data; |
529 | self->user_destroy = user_destroy; |
530 | |
531 | gtk_map_list_model_init_items (self); |
532 | |
533 | if (self->model) |
534 | n_items = g_list_model_get_n_items (list: self->model); |
535 | else |
536 | n_items = 0; |
537 | if (n_items) |
538 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: 0, removed: n_items, added: n_items); |
539 | |
540 | if (was_maped != will_be_maped) |
541 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_HAS_MAP]); |
542 | } |
543 | |
544 | /** |
545 | * gtk_map_list_model_set_model: (attributes org.gtk.Method.set_property=model) |
546 | * @self: a `GtkMapListModel` |
547 | * @model: (nullable): The model to be mapped |
548 | * |
549 | * Sets the model to be mapped. |
550 | * |
551 | * GTK makes no effort to ensure that @model conforms to the item type |
552 | * expected by the map function. It assumes that the caller knows what |
553 | * they are doing and have set up an appropriate map function. |
554 | */ |
555 | void |
556 | gtk_map_list_model_set_model (GtkMapListModel *self, |
557 | GListModel *model) |
558 | { |
559 | guint removed, added; |
560 | |
561 | g_return_if_fail (GTK_IS_MAP_LIST_MODEL (self)); |
562 | g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model)); |
563 | |
564 | if (self->model == model) |
565 | return; |
566 | |
567 | removed = g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self)); |
568 | gtk_map_list_model_clear_model (self); |
569 | |
570 | if (model) |
571 | { |
572 | self->model = g_object_ref (model); |
573 | g_signal_connect (model, "items-changed" , G_CALLBACK (gtk_map_list_model_items_changed_cb), self); |
574 | added = g_list_model_get_n_items (list: model); |
575 | } |
576 | else |
577 | { |
578 | added = 0; |
579 | } |
580 | |
581 | gtk_map_list_model_init_items (self); |
582 | |
583 | if (removed > 0 || added > 0) |
584 | g_list_model_items_changed (list: G_LIST_MODEL (ptr: self), position: 0, removed, added); |
585 | |
586 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_MODEL]); |
587 | } |
588 | |
589 | /** |
590 | * gtk_map_list_model_get_model: (attributes org.gtk.Method.get_property=model) |
591 | * @self: a `GtkMapListModel` |
592 | * |
593 | * Gets the model that is currently being mapped or %NULL if none. |
594 | * |
595 | * Returns: (nullable) (transfer none): The model that gets mapped |
596 | */ |
597 | GListModel * |
598 | gtk_map_list_model_get_model (GtkMapListModel *self) |
599 | { |
600 | g_return_val_if_fail (GTK_IS_MAP_LIST_MODEL (self), NULL); |
601 | |
602 | return self->model; |
603 | } |
604 | |
605 | /** |
606 | * gtk_map_list_model_has_map: (attributes org.gtk.Method.get_property=has-map) |
607 | * @self: a `GtkMapListModel` |
608 | * |
609 | * Checks if a map function is currently set on @self. |
610 | * |
611 | * Returns: %TRUE if a map function is set |
612 | */ |
613 | gboolean |
614 | gtk_map_list_model_has_map (GtkMapListModel *self) |
615 | { |
616 | g_return_val_if_fail (GTK_IS_MAP_LIST_MODEL (self), FALSE); |
617 | |
618 | return self->map_func != NULL; |
619 | } |
620 | |