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
60enum {
61 PROP_0,
62 PROP_HAS_MAP,
63 PROP_MODEL,
64 NUM_PROPERTIES
65};
66
67typedef struct _MapNode MapNode;
68typedef struct _MapAugment MapAugment;
69
70struct _MapNode
71{
72 guint n_items;
73 gpointer item; /* can only be set when n_items == 1 */
74};
75
76struct _MapAugment
77{
78 guint n_items;
79};
80
81struct _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
93struct _GtkMapListModelClass
94{
95 GObjectClass parent_class;
96};
97
98static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
99
100static MapNode *
101gtk_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
140static GType
141gtk_map_list_model_get_item_type (GListModel *list)
142{
143 return G_TYPE_OBJECT;
144}
145
146static guint
147gtk_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
157static gpointer
158gtk_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
200static void
201gtk_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
208G_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
211static void
212gtk_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
274static void
275gtk_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
294static void
295gtk_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
318static void
319gtk_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
328static void
329gtk_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
344static void
345gtk_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
380static void
381gtk_map_list_model_init (GtkMapListModel *self)
382{
383}
384
385
386static void
387gtk_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 */
421GtkMapListModel *
422gtk_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
444static void
445gtk_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
453static void
454gtk_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 */
506void
507gtk_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 */
555void
556gtk_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 */
597GListModel *
598gtk_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 */
613gboolean
614gtk_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

source code of gtk/gtk/gtkmaplistmodel.c