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