1 /* gtd-list-store.c
2 *
3 * Copyright 2018 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: GPL-3.0-or-later
19 */
20
21 #define G_LOG_DOMAIN "GtdListStore"
22
23 #include "gtd-list-store.h"
24
25 #include <gio/gio.h>
26
27 /**
28 * SECTION:gtdliststore
29 * @title: GtdListStore
30 * @short_description: A simple implementation of #GListModel
31 * @include: gio/gio.h
32 *
33 * #GtdListStore is a simple implementation of #GListModel that stores all
34 * items in memory.
35 *
36 * It provides insertions, deletions, and lookups in logarithmic time
37 * with a fast path for the common case of iterating the list linearly.
38 */
39
40 /**
41 * GtdListStore:
42 *
43 * #GtdListStore is an opaque data structure and can only be accessed
44 * using the following functions.
45 **/
46
47 struct _GtdListStore
48 {
49 GObject parent;
50
51 GType item_type;
52 GSequence *items;
53
54 GHashTable *item_to_iter;
55
56 /* cache */
57 gint64 length;
58 gint64 last_position;
59 GSequenceIter *last_iter;
60 };
61
62 enum
63 {
64 PROP_0,
65 PROP_ITEM_TYPE,
66 N_PROPERTIES
67 };
68
69 static void gtd_list_store_iface_init (GListModelInterface *iface);
70
71 G_DEFINE_TYPE_WITH_CODE (GtdListStore, gtd_list_store, G_TYPE_OBJECT,
72 G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtd_list_store_iface_init));
73
74 static void
remove_item_from_sequence_cb(gpointer item,gpointer user_data)75 remove_item_from_sequence_cb (gpointer item,
76 gpointer user_data)
77 {
78 GSequenceIter *it;
79 GtdListStore *store;
80
81 store = (GtdListStore *)user_data;
82 it = g_hash_table_lookup (store->item_to_iter, item);
83
84 g_hash_table_remove (store->item_to_iter, item);
85 g_sequence_remove (it);
86 }
87
88 static void
gtd_list_store_items_changed(GtdListStore * store,guint position,guint removed,guint added)89 gtd_list_store_items_changed (GtdListStore *store,
90 guint position,
91 guint removed,
92 guint added)
93 {
94 /* check if the iter cache may have been invalidated */
95 if (position <= store->last_position)
96 {
97 store->last_iter = NULL;
98 store->last_position = -1;
99 }
100
101 store->length -= removed;
102 store->length += added;
103
104 g_list_model_items_changed (G_LIST_MODEL (store), position, removed, added);
105 }
106
107 static void
gtd_list_store_dispose(GObject * object)108 gtd_list_store_dispose (GObject *object)
109 {
110 GtdListStore *store = GTD_LIST_STORE (object);
111
112 g_clear_pointer (&store->item_to_iter, g_hash_table_destroy);
113 g_clear_pointer (&store->items, g_sequence_free);
114
115 G_OBJECT_CLASS (gtd_list_store_parent_class)->dispose (object);
116 }
117
118 static void
gtd_list_store_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)119 gtd_list_store_get_property (GObject *object,
120 guint property_id,
121 GValue *value,
122 GParamSpec *pspec)
123 {
124 GtdListStore *store = GTD_LIST_STORE (object);
125
126 switch (property_id)
127 {
128 case PROP_ITEM_TYPE:
129 g_value_set_gtype (value, store->item_type);
130 break;
131
132 default:
133 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
134 }
135 }
136
137 static void
gtd_list_store_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)138 gtd_list_store_set_property (GObject *object,
139 guint property_id,
140 const GValue *value,
141 GParamSpec *pspec)
142 {
143 GtdListStore *store = GTD_LIST_STORE (object);
144
145 switch (property_id)
146 {
147 case PROP_ITEM_TYPE: /* construct-only */
148 store->item_type = g_value_get_gtype (value);
149 if (!g_type_is_a (store->item_type, G_TYPE_OBJECT))
150 g_critical ("GtdListStore cannot store items of type '%s'. Items must be GObjects.",
151 g_type_name (store->item_type));
152 break;
153
154 default:
155 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
156 }
157 }
158
159 static void
gtd_list_store_class_init(GtdListStoreClass * klass)160 gtd_list_store_class_init (GtdListStoreClass *klass)
161 {
162 GObjectClass *object_class = G_OBJECT_CLASS (klass);
163
164 object_class->dispose = gtd_list_store_dispose;
165 object_class->get_property = gtd_list_store_get_property;
166 object_class->set_property = gtd_list_store_set_property;
167
168 /**
169 * GtdListStore:item-type:
170 *
171 * The type of items contained in this list store. Items must be
172 * subclasses of #GObject.
173 **/
174 g_object_class_install_property (object_class, PROP_ITEM_TYPE,
175 g_param_spec_gtype ("item-type", "", "", G_TYPE_OBJECT,
176 G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
177 }
178
179 static GType
gtd_list_store_get_item_type(GListModel * list)180 gtd_list_store_get_item_type (GListModel *list)
181 {
182 GtdListStore *store = GTD_LIST_STORE (list);
183
184 return store->item_type;
185 }
186
187 static guint
gtd_list_store_get_n_items(GListModel * list)188 gtd_list_store_get_n_items (GListModel *list)
189 {
190 GtdListStore *store = GTD_LIST_STORE (list);
191
192 return store->length;
193 }
194
195 static gpointer
gtd_list_store_get_item(GListModel * list,guint position)196 gtd_list_store_get_item (GListModel *list,
197 guint position)
198 {
199 GtdListStore *store = GTD_LIST_STORE (list);
200 GSequenceIter *it = NULL;
201
202 if (store->last_position != -1)
203 {
204 if (store->last_position == position + 1)
205 it = g_sequence_iter_prev (store->last_iter);
206 else if (store->last_position == position - 1)
207 it = g_sequence_iter_next (store->last_iter);
208 else if (store->last_position == position)
209 it = store->last_iter;
210 }
211
212 if (it == NULL)
213 it = g_sequence_get_iter_at_pos (store->items, position);
214
215 store->last_iter = it;
216 store->last_position = position;
217
218 if (g_sequence_iter_is_end (it))
219 return NULL;
220 else
221 return g_object_ref (g_sequence_get (it));
222 }
223
224 static void
gtd_list_store_iface_init(GListModelInterface * iface)225 gtd_list_store_iface_init (GListModelInterface *iface)
226 {
227 iface->get_item_type = gtd_list_store_get_item_type;
228 iface->get_n_items = gtd_list_store_get_n_items;
229 iface->get_item = gtd_list_store_get_item;
230 }
231
232 static void
gtd_list_store_init(GtdListStore * store)233 gtd_list_store_init (GtdListStore *store)
234 {
235 store->item_to_iter = g_hash_table_new (g_direct_hash, g_direct_equal);
236 store->items = g_sequence_new (g_object_unref);
237 store->last_position = -1;
238 }
239
240 /**
241 * gtd_list_store_new:
242 * @item_type: the #GType of items in the list
243 *
244 * Creates a new #GtdListStore with items of type @item_type. @item_type
245 * must be a subclass of #GObject.
246 *
247 * Returns: a new #GtdListStore
248 */
249 GtdListStore *
gtd_list_store_new(GType item_type)250 gtd_list_store_new (GType item_type)
251 {
252 /* We only allow GObjects as item types right now. This might change
253 * in the future.
254 */
255 g_return_val_if_fail (g_type_is_a (item_type, G_TYPE_OBJECT), NULL);
256
257 return g_object_new (GTD_TYPE_LIST_STORE,
258 "item-type", item_type,
259 NULL);
260 }
261
262 /**
263 * gtd_list_store_insert:
264 * @store: a #GtdListStore
265 * @position: the position at which to insert the new item
266 * @item: (type GObject): the new item
267 *
268 * Inserts @item into @store at @position. @item must be of type
269 * #GtdListStore:item-type or derived from it. @position must be smaller
270 * than the length of the list, or equal to it to append.
271 *
272 * This function takes a ref on @item.
273 *
274 * Use gtd_list_store_splice() to insert multiple items at the same time
275 * efficiently.
276 */
277 void
gtd_list_store_insert(GtdListStore * store,guint position,gpointer item)278 gtd_list_store_insert (GtdListStore *store,
279 guint position,
280 gpointer item)
281 {
282 GSequenceIter *new_it;
283 GSequenceIter *it;
284
285 g_return_if_fail (GTD_IS_LIST_STORE (store));
286 g_return_if_fail (g_type_is_a (G_OBJECT_TYPE (item), store->item_type));
287 g_return_if_fail (position <= (guint) g_sequence_get_length (store->items));
288
289 it = g_sequence_get_iter_at_pos (store->items, position);
290 new_it = g_sequence_insert_before (it, g_object_ref (item));
291
292 g_hash_table_insert (store->item_to_iter, item, new_it);
293
294 gtd_list_store_items_changed (store, position, 0, 1);
295 }
296
297 /**
298 * gtd_list_store_insert_sorted:
299 * @store: a #GtdListStore
300 * @item: (type GObject): the new item
301 * @compare_func: (scope call): pairwise comparison function for sorting
302 * @user_data: (closure): user data for @compare_func
303 *
304 * Inserts @item into @store at a position to be determined by the
305 * @compare_func.
306 *
307 * The list must already be sorted before calling this function or the
308 * result is undefined. Usually you would approach this by only ever
309 * inserting items by way of this function.
310 *
311 * This function takes a ref on @item.
312 *
313 * Returns: the position at which @item was inserted
314 */
315 guint
gtd_list_store_insert_sorted(GtdListStore * store,gpointer item,GCompareDataFunc compare_func,gpointer user_data)316 gtd_list_store_insert_sorted (GtdListStore *store,
317 gpointer item,
318 GCompareDataFunc compare_func,
319 gpointer user_data)
320 {
321 GSequenceIter *it;
322 guint position;
323
324 g_return_val_if_fail (GTD_IS_LIST_STORE (store), 0);
325 g_return_val_if_fail (g_type_is_a (G_OBJECT_TYPE (item), store->item_type), 0);
326 g_return_val_if_fail (compare_func != NULL, 0);
327
328 it = g_sequence_insert_sorted (store->items, g_object_ref (item), compare_func, user_data);
329 position = g_sequence_iter_get_position (it);
330
331 g_hash_table_insert (store->item_to_iter, item, it);
332
333 gtd_list_store_items_changed (store, position, 0, 1);
334
335 return position;
336 }
337
338 /**
339 * gtd_list_store_sort:
340 * @store: a #GtdListStore
341 * @compare_func: (scope call): pairwise comparison function for sorting
342 * @user_data: (closure): user data for @compare_func
343 *
344 * Sort the items in @store according to @compare_func.
345 */
346 void
gtd_list_store_sort(GtdListStore * store,GCompareDataFunc compare_func,gpointer user_data)347 gtd_list_store_sort (GtdListStore *store,
348 GCompareDataFunc compare_func,
349 gpointer user_data)
350 {
351 gint n_items;
352
353 g_return_if_fail (GTD_IS_LIST_STORE (store));
354 g_return_if_fail (compare_func != NULL);
355
356 g_sequence_sort (store->items, compare_func, user_data);
357
358 n_items = g_sequence_get_length (store->items);
359 gtd_list_store_items_changed (store, 0, n_items, n_items);
360 }
361
362 /**
363 * gtd_list_store_append:
364 * @store: a #GtdListStore
365 * @item: (type GObject): the new item
366 *
367 * Appends @item to @store. @item must be of type #GtdListStore:item-type.
368 *
369 * This function takes a ref on @item.
370 *
371 * Use gtd_list_store_splice() to append multiple items at the same time
372 * efficiently.
373 */
374 void
gtd_list_store_append(GtdListStore * store,gpointer item)375 gtd_list_store_append (GtdListStore *store,
376 gpointer item)
377 {
378 GSequenceIter *it;
379 guint n_items;
380
381 g_return_if_fail (GTD_IS_LIST_STORE (store));
382 g_return_if_fail (g_type_is_a (G_OBJECT_TYPE (item), store->item_type));
383
384 n_items = g_sequence_get_length (store->items);
385 it = g_sequence_append (store->items, g_object_ref (item));
386
387 g_hash_table_insert (store->item_to_iter, item, it);
388
389 gtd_list_store_items_changed (store, n_items, 0, 1);
390 }
391
392 /**
393 * gtd_list_store_remove:
394 * @store: a #GtdListStore
395 * @item: the item that is to be removed
396 *
397 * Removes the item from @store.
398 *
399 * Use gtd_list_store_splice() to remove multiple items at the same time
400 * efficiently.
401 */
402 void
gtd_list_store_remove(GtdListStore * store,gpointer item)403 gtd_list_store_remove (GtdListStore *store,
404 gpointer item)
405 {
406 GSequenceIter *it;
407 guint position;
408
409 g_return_if_fail (GTD_IS_LIST_STORE (store));
410
411 it = g_hash_table_lookup (store->item_to_iter, item);
412 if (!it)
413 return;
414
415 g_return_if_fail (!g_sequence_iter_is_end (it));
416
417 position = g_sequence_iter_get_position (it);
418
419 g_hash_table_remove (store->item_to_iter, item);
420 g_sequence_remove (it);
421 gtd_list_store_items_changed (store, position, 1, 0);
422 }
423
424 /**
425 * gtd_list_store_remove_at_position:
426 * @store: a #GtdListStore
427 * @position: the position of the item that is to be removed
428 *
429 * Removes the item from @store that is at @position. @position must be
430 * smaller than the current length of the list.
431 *
432 * Use gtd_list_store_splice() to remove multiple items at the same time
433 * efficiently.
434 */
435 void
gtd_list_store_remove_at_position(GtdListStore * store,guint position)436 gtd_list_store_remove_at_position (GtdListStore *store,
437 guint position)
438 {
439 GSequenceIter *it;
440
441 g_return_if_fail (GTD_IS_LIST_STORE (store));
442
443 it = g_sequence_get_iter_at_pos (store->items, position);
444 g_return_if_fail (!g_sequence_iter_is_end (it));
445
446 g_hash_table_remove (store->item_to_iter, g_sequence_get (it));
447 g_sequence_remove (it);
448 gtd_list_store_items_changed (store, position, 1, 0);
449 }
450
451 /**
452 * gtd_list_store_remove_all:
453 * @store: a #GtdListStore
454 *
455 * Removes all items from @store.
456 */
457 void
gtd_list_store_remove_all(GtdListStore * store)458 gtd_list_store_remove_all (GtdListStore *store)
459 {
460 guint n_items;
461
462 g_return_if_fail (GTD_IS_LIST_STORE (store));
463
464 n_items = g_sequence_get_length (store->items);
465 g_sequence_remove_range (g_sequence_get_begin_iter (store->items),
466 g_sequence_get_end_iter (store->items));
467 g_hash_table_remove_all (store->item_to_iter);
468
469 gtd_list_store_items_changed (store, 0, n_items, 0);
470 }
471
472 /**
473 * gtd_list_store_splice:
474 * @store: a #GtdListStore
475 * @position: the position at which to make the change
476 * @n_removals: the number of items to remove
477 * @additions: (array length=n_additions) (element-type GObject): the items to add
478 * @n_additions: the number of items to add
479 *
480 * Changes @store by removing @n_removals items and adding @n_additions
481 * items to it. @additions must contain @n_additions items of type
482 * #GtdListStore:item-type. %NULL is not permitted.
483 *
484 * This function is more efficient than gtd_list_store_insert() and
485 * gtd_list_store_remove(), because it only emits
486 * #GListModel::items-changed once for the change.
487 *
488 * This function takes a ref on each item in @additions.
489 *
490 * The parameters @position and @n_removals must be correct (ie:
491 * @position + @n_removals must be less than or equal to the length of
492 * the list at the time this function is called).
493 */
494 void
gtd_list_store_splice(GtdListStore * store,guint position,guint n_removals,gpointer * additions,guint n_additions)495 gtd_list_store_splice (GtdListStore *store,
496 guint position,
497 guint n_removals,
498 gpointer *additions,
499 guint n_additions)
500 {
501 GSequenceIter *it;
502 guint n_items;
503
504 g_return_if_fail (GTD_IS_LIST_STORE (store));
505 g_return_if_fail (position + n_removals >= position); /* overflow */
506
507 n_items = g_sequence_get_length (store->items);
508 g_return_if_fail (position + n_removals <= n_items);
509
510 it = g_sequence_get_iter_at_pos (store->items, position);
511
512 if (n_removals)
513 {
514 GSequenceIter *end;
515
516 end = g_sequence_iter_move (it, n_removals);
517 g_sequence_foreach_range (it, end, remove_item_from_sequence_cb, store);
518
519 it = end;
520 }
521
522 if (n_additions)
523 {
524 guint i;
525
526 for (i = 0; i < n_additions; i++)
527 {
528 if G_UNLIKELY (!g_type_is_a (G_OBJECT_TYPE (additions[i]), store->item_type))
529 {
530 g_critical ("%s: item %d is a %s instead of a %s. GtdListStore is now in an undefined state.",
531 G_STRFUNC, i, G_OBJECT_TYPE_NAME (additions[i]), g_type_name (store->item_type));
532 return;
533 }
534
535 it = g_sequence_insert_before (it, g_object_ref (additions[i]));
536 g_hash_table_insert (store->item_to_iter, additions[i], it);
537
538 it = g_sequence_iter_next (it);
539 }
540 }
541
542 gtd_list_store_items_changed (store, position, n_removals, n_additions);
543 }
544
545 /**
546 * gtd_list_store_get_item_position:
547 * @store: a #GtdListStore
548 * @item: the item to retrieve the position
549 *
550 * Retrieves the position of @items inside @store. It is a programming
551 * error to pass an @item that is not contained in @store.
552 *
553 * Returns: the position of @item in @store.
554 */
555 guint
gtd_list_store_get_item_position(GtdListStore * store,gpointer item)556 gtd_list_store_get_item_position (GtdListStore *store,
557 gpointer item)
558 {
559 GSequenceIter *iter;
560
561 g_return_val_if_fail (GTD_IS_LIST_STORE (store), 0);
562
563 iter = g_hash_table_lookup (store->item_to_iter, item);
564 g_assert (iter != NULL);
565
566 return g_sequence_iter_get_position (iter);
567 }
568