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 "gtklistitemmanagerprivate.h"
23 
24 #include "gtklistitemwidgetprivate.h"
25 #include "gtkwidgetprivate.h"
26 
27 #define GTK_LIST_VIEW_MAX_LIST_ITEMS 200
28 
29 struct _GtkListItemManager
30 {
31   GObject parent_instance;
32 
33   GtkWidget *widget;
34   GtkSelectionModel *model;
35   GtkListItemFactory *factory;
36   gboolean single_click_activate;
37   const char *item_css_name;
38   GtkAccessibleRole item_role;
39 
40   GtkRbTree *items;
41   GSList *trackers;
42 };
43 
44 struct _GtkListItemManagerClass
45 {
46   GObjectClass parent_class;
47 };
48 
49 struct _GtkListItemTracker
50 {
51   guint position;
52   GtkListItemWidget *widget;
53   guint n_before;
54   guint n_after;
55 };
56 
57 static GtkWidget *      gtk_list_item_manager_acquire_list_item (GtkListItemManager     *self,
58                                                                  guint                   position,
59                                                                  GtkWidget              *prev_sibling);
60 static GtkWidget *      gtk_list_item_manager_try_reacquire_list_item
61                                                                 (GtkListItemManager     *self,
62                                                                  GHashTable             *change,
63                                                                  guint                   position,
64                                                                  GtkWidget              *prev_sibling);
65 static void             gtk_list_item_manager_update_list_item  (GtkListItemManager     *self,
66                                                                  GtkWidget              *item,
67                                                                  guint                   position);
68 static void             gtk_list_item_manager_move_list_item    (GtkListItemManager     *self,
69                                                                  GtkWidget              *list_item,
70                                                                  guint                   position,
71                                                                  GtkWidget              *prev_sibling);
72 static void             gtk_list_item_manager_release_list_item (GtkListItemManager     *self,
73                                                                  GHashTable             *change,
74                                                                  GtkWidget              *widget);
G_DEFINE_TYPE(GtkListItemManager,gtk_list_item_manager,G_TYPE_OBJECT)75 G_DEFINE_TYPE (GtkListItemManager, gtk_list_item_manager, G_TYPE_OBJECT)
76 
77 void
78 gtk_list_item_manager_augment_node (GtkRbTree *tree,
79                                     gpointer   node_augment,
80                                     gpointer   node,
81                                     gpointer   left,
82                                     gpointer   right)
83 {
84   GtkListItemManagerItem *item = node;
85   GtkListItemManagerItemAugment *aug = node_augment;
86 
87   aug->n_items = item->n_items;
88 
89   if (left)
90     {
91       GtkListItemManagerItemAugment *left_aug = gtk_rb_tree_get_augment (tree, left);
92 
93       aug->n_items += left_aug->n_items;
94     }
95 
96   if (right)
97     {
98       GtkListItemManagerItemAugment *right_aug = gtk_rb_tree_get_augment (tree, right);
99 
100       aug->n_items += right_aug->n_items;
101     }
102 }
103 
104 static void
gtk_list_item_manager_clear_node(gpointer _item)105 gtk_list_item_manager_clear_node (gpointer _item)
106 {
107   GtkListItemManagerItem *item G_GNUC_UNUSED = _item;
108 
109   g_assert (item->widget == NULL);
110 }
111 
112 GtkListItemManager *
gtk_list_item_manager_new_for_size(GtkWidget * widget,const char * item_css_name,GtkAccessibleRole item_role,gsize element_size,gsize augment_size,GtkRbTreeAugmentFunc augment_func)113 gtk_list_item_manager_new_for_size (GtkWidget            *widget,
114                                     const char           *item_css_name,
115                                     GtkAccessibleRole     item_role,
116                                     gsize                 element_size,
117                                     gsize                 augment_size,
118                                     GtkRbTreeAugmentFunc  augment_func)
119 {
120   GtkListItemManager *self;
121 
122   g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
123   g_return_val_if_fail (element_size >= sizeof (GtkListItemManagerItem), NULL);
124   g_return_val_if_fail (augment_size >= sizeof (GtkListItemManagerItemAugment), NULL);
125 
126   self = g_object_new (GTK_TYPE_LIST_ITEM_MANAGER, NULL);
127 
128   /* not taking a ref because the widget refs us */
129   self->widget = widget;
130   self->item_css_name = g_intern_string (item_css_name);
131   self->item_role = item_role;
132 
133   self->items = gtk_rb_tree_new_for_size (element_size,
134                                           augment_size,
135                                           augment_func,
136                                           gtk_list_item_manager_clear_node,
137                                           NULL);
138 
139   return self;
140 }
141 
142 gpointer
gtk_list_item_manager_get_first(GtkListItemManager * self)143 gtk_list_item_manager_get_first (GtkListItemManager *self)
144 {
145   return gtk_rb_tree_get_first (self->items);
146 }
147 
148 gpointer
gtk_list_item_manager_get_root(GtkListItemManager * self)149 gtk_list_item_manager_get_root (GtkListItemManager *self)
150 {
151   return gtk_rb_tree_get_root (self->items);
152 }
153 
154 /*
155  * gtk_list_item_manager_get_nth:
156  * @self: a `GtkListItemManager`
157  * @position: position of the item
158  * @offset: (out): offset into the returned item
159  *
160  * Looks up the GtkListItemManagerItem that represents @position.
161  *
162  * If a the returned item represents multiple rows, the @offset into
163  * the returned item for @position will be set. If the returned item
164  * represents a row with an existing widget, @offset will always be 0.
165  *
166  * Returns: (type GtkListItemManagerItem): the item for @position or
167  *   %NULL if position is out of range
168  **/
169 gpointer
gtk_list_item_manager_get_nth(GtkListItemManager * self,guint position,guint * offset)170 gtk_list_item_manager_get_nth (GtkListItemManager *self,
171                                guint               position,
172                                guint              *offset)
173 {
174   GtkListItemManagerItem *item, *tmp;
175 
176   item = gtk_rb_tree_get_root (self->items);
177 
178   while (item)
179     {
180       tmp = gtk_rb_tree_node_get_left (item);
181       if (tmp)
182         {
183           GtkListItemManagerItemAugment *aug = gtk_rb_tree_get_augment (self->items, tmp);
184           if (position < aug->n_items)
185             {
186               item = tmp;
187               continue;
188             }
189           position -= aug->n_items;
190         }
191 
192       if (position < item->n_items)
193         break;
194       position -= item->n_items;
195 
196       item = gtk_rb_tree_node_get_right (item);
197     }
198 
199   if (offset)
200     *offset = item ? position : 0;
201 
202   return item;
203 }
204 
205 guint
gtk_list_item_manager_get_item_position(GtkListItemManager * self,gpointer item)206 gtk_list_item_manager_get_item_position (GtkListItemManager *self,
207                                          gpointer            item)
208 {
209   GtkListItemManagerItem *parent, *left;
210   int pos;
211 
212   left = gtk_rb_tree_node_get_left (item);
213   if (left)
214     {
215       GtkListItemManagerItemAugment *aug = gtk_rb_tree_get_augment (self->items, left);
216       pos = aug->n_items;
217     }
218   else
219     {
220       pos = 0;
221     }
222 
223   for (parent = gtk_rb_tree_node_get_parent (item);
224        parent != NULL;
225        parent = gtk_rb_tree_node_get_parent (item))
226     {
227       left = gtk_rb_tree_node_get_left (parent);
228 
229       if (left != item)
230         {
231           if (left)
232             {
233               GtkListItemManagerItemAugment *aug = gtk_rb_tree_get_augment (self->items, left);
234               pos += aug->n_items;
235             }
236           pos += parent->n_items;
237         }
238 
239       item = parent;
240     }
241 
242   return pos;
243 }
244 
245 gpointer
gtk_list_item_manager_get_item_augment(GtkListItemManager * self,gpointer item)246 gtk_list_item_manager_get_item_augment (GtkListItemManager *self,
247                                         gpointer            item)
248 {
249   return gtk_rb_tree_get_augment (self->items, item);
250 }
251 
252 static void
gtk_list_item_tracker_unset_position(GtkListItemManager * self,GtkListItemTracker * tracker)253 gtk_list_item_tracker_unset_position (GtkListItemManager *self,
254                                       GtkListItemTracker *tracker)
255 {
256   tracker->widget = NULL;
257   tracker->position = GTK_INVALID_LIST_POSITION;
258 }
259 
260 static gboolean
gtk_list_item_tracker_query_range(GtkListItemManager * self,GtkListItemTracker * tracker,guint n_items,guint * out_start,guint * out_n_items)261 gtk_list_item_tracker_query_range (GtkListItemManager *self,
262                                    GtkListItemTracker *tracker,
263                                    guint               n_items,
264                                    guint              *out_start,
265                                    guint              *out_n_items)
266 {
267   /* We can't look at tracker->widget here because we might not
268    * have set it yet.
269    */
270   if (tracker->position == GTK_INVALID_LIST_POSITION)
271     return FALSE;
272 
273   /* This is magic I made up that is meant to be both
274    * correct and doesn't overflow when start and/or end are close to 0 or
275    * close to max.
276    * But beware, I didn't test it.
277    */
278   *out_n_items = tracker->n_before + tracker->n_after + 1;
279   *out_n_items = MIN (*out_n_items, n_items);
280 
281   *out_start = MAX (tracker->position, tracker->n_before) - tracker->n_before;
282   *out_start = MIN (*out_start, n_items - *out_n_items);
283 
284   return TRUE;
285 }
286 
287 static void
gtk_list_item_query_tracked_range(GtkListItemManager * self,guint n_items,guint position,guint * out_n_items,gboolean * out_tracked)288 gtk_list_item_query_tracked_range (GtkListItemManager *self,
289                                    guint               n_items,
290                                    guint               position,
291                                    guint              *out_n_items,
292                                    gboolean           *out_tracked)
293 {
294   GSList *l;
295   guint tracker_start, tracker_n_items;
296 
297   g_assert (position < n_items);
298 
299   *out_tracked = FALSE;
300   *out_n_items = n_items - position;
301 
302   /* step 1: Check if position is tracked */
303 
304   for (l = self->trackers; l; l = l->next)
305     {
306       if (!gtk_list_item_tracker_query_range (self, l->data, n_items, &tracker_start, &tracker_n_items))
307         continue;
308 
309       if (tracker_start > position)
310         {
311           *out_n_items = MIN (*out_n_items, tracker_start - position);
312         }
313       else if (tracker_start + tracker_n_items <= position)
314         {
315           /* do nothing */
316         }
317       else
318         {
319           *out_tracked = TRUE;
320           *out_n_items = tracker_start + tracker_n_items - position;
321           break;
322         }
323     }
324 
325   /* If nothing's tracked, we're done */
326   if (!*out_tracked)
327     return;
328 
329   /* step 2: make the tracked range as large as possible
330    * NB: This is O(N_TRACKERS^2), but the number of trackers should be <5 */
331 restart:
332   for (l = self->trackers; l; l = l->next)
333     {
334       if (!gtk_list_item_tracker_query_range (self, l->data, n_items, &tracker_start, &tracker_n_items))
335         continue;
336 
337       if (tracker_start + tracker_n_items <= position + *out_n_items)
338         continue;
339       if (tracker_start > position + *out_n_items)
340         continue;
341 
342       if (*out_n_items + position < tracker_start + tracker_n_items)
343         {
344           *out_n_items = tracker_start + tracker_n_items - position;
345           goto restart;
346         }
347     }
348 }
349 
350 static void
gtk_list_item_manager_remove_items(GtkListItemManager * self,GHashTable * change,guint position,guint n_items)351 gtk_list_item_manager_remove_items (GtkListItemManager *self,
352                                     GHashTable         *change,
353                                     guint               position,
354                                     guint               n_items)
355 {
356   GtkListItemManagerItem *item;
357 
358   if (n_items == 0)
359     return;
360 
361   item = gtk_list_item_manager_get_nth (self, position, NULL);
362 
363   while (n_items > 0)
364     {
365       if (item->n_items > n_items)
366         {
367           item->n_items -= n_items;
368           gtk_rb_tree_node_mark_dirty (item);
369           n_items = 0;
370         }
371       else
372         {
373           GtkListItemManagerItem *next = gtk_rb_tree_node_get_next (item);
374           if (item->widget)
375             gtk_list_item_manager_release_list_item (self, change, item->widget);
376           item->widget = NULL;
377           n_items -= item->n_items;
378           gtk_rb_tree_remove (self->items, item);
379           item = next;
380         }
381     }
382 
383   gtk_widget_queue_resize (GTK_WIDGET (self->widget));
384 }
385 
386 static void
gtk_list_item_manager_add_items(GtkListItemManager * self,guint position,guint n_items)387 gtk_list_item_manager_add_items (GtkListItemManager *self,
388                                  guint               position,
389                                  guint               n_items)
390 {
391   GtkListItemManagerItem *item;
392   guint offset;
393 
394   if (n_items == 0)
395     return;
396 
397   item = gtk_list_item_manager_get_nth (self, position, &offset);
398 
399   if (item == NULL || item->widget)
400     item = gtk_rb_tree_insert_before (self->items, item);
401   item->n_items += n_items;
402   gtk_rb_tree_node_mark_dirty (item);
403 
404   gtk_widget_queue_resize (GTK_WIDGET (self->widget));
405 }
406 
407 static gboolean
gtk_list_item_manager_merge_list_items(GtkListItemManager * self,GtkListItemManagerItem * first,GtkListItemManagerItem * second)408 gtk_list_item_manager_merge_list_items (GtkListItemManager     *self,
409                                         GtkListItemManagerItem *first,
410                                         GtkListItemManagerItem *second)
411 {
412   if (first->widget || second->widget)
413     return FALSE;
414 
415   first->n_items += second->n_items;
416   gtk_rb_tree_node_mark_dirty (first);
417   gtk_rb_tree_remove (self->items, second);
418 
419   return TRUE;
420 }
421 
422 static void
gtk_list_item_manager_release_items(GtkListItemManager * self,GQueue * released)423 gtk_list_item_manager_release_items (GtkListItemManager *self,
424                                      GQueue             *released)
425 {
426   GtkListItemManagerItem *item, *prev, *next;
427   guint position, i, n_items, query_n_items;
428   gboolean tracked;
429 
430   n_items = g_list_model_get_n_items (G_LIST_MODEL (self->model));
431   position = 0;
432 
433   while (position < n_items)
434     {
435       gtk_list_item_query_tracked_range (self, n_items, position, &query_n_items, &tracked);
436       if (tracked)
437         {
438           position += query_n_items;
439           continue;
440         }
441 
442       item = gtk_list_item_manager_get_nth (self, position, &i);
443       i = position - i;
444       while (i < position + query_n_items)
445         {
446           if (item->widget)
447             {
448               g_queue_push_tail (released, item->widget);
449               item->widget = NULL;
450               i++;
451               prev = gtk_rb_tree_node_get_previous (item);
452               if (prev && gtk_list_item_manager_merge_list_items (self, prev, item))
453                 item = prev;
454               next = gtk_rb_tree_node_get_next (item);
455               if (next && next->widget == NULL)
456                 {
457                   i += next->n_items;
458                   if (!gtk_list_item_manager_merge_list_items (self, next, item))
459                     g_assert_not_reached ();
460                   item = gtk_rb_tree_node_get_next (next);
461                 }
462               else
463                 {
464                   item = next;
465                 }
466             }
467           else
468             {
469               i += item->n_items;
470               item = gtk_rb_tree_node_get_next (item);
471             }
472         }
473       position += query_n_items;
474     }
475 }
476 
477 static void
gtk_list_item_manager_ensure_items(GtkListItemManager * self,GHashTable * change,guint update_start)478 gtk_list_item_manager_ensure_items (GtkListItemManager *self,
479                                     GHashTable         *change,
480                                     guint               update_start)
481 {
482   GtkListItemManagerItem *item, *new_item;
483   GtkWidget *widget, *insert_after;
484   guint position, i, n_items, query_n_items, offset;
485   GQueue released = G_QUEUE_INIT;
486   gboolean tracked;
487 
488   if (self->model == NULL)
489     return;
490 
491   n_items = g_list_model_get_n_items (G_LIST_MODEL (self->model));
492   position = 0;
493 
494   gtk_list_item_manager_release_items (self, &released);
495 
496   while (position < n_items)
497     {
498       gtk_list_item_query_tracked_range (self, n_items, position, &query_n_items, &tracked);
499       if (!tracked)
500         {
501           position += query_n_items;
502           continue;
503         }
504 
505       item = gtk_list_item_manager_get_nth (self, position, &offset);
506       for (new_item = item;
507            new_item && new_item->widget == NULL;
508            new_item = gtk_rb_tree_node_get_previous (new_item))
509          { /* do nothing */ }
510       insert_after = new_item ? new_item->widget : NULL;
511 
512       if (offset > 0)
513         {
514           new_item = gtk_rb_tree_insert_before (self->items, item);
515           new_item->n_items = offset;
516           item->n_items -= offset;
517           gtk_rb_tree_node_mark_dirty (item);
518         }
519 
520       for (i = 0; i < query_n_items; i++)
521         {
522           if (item->n_items > 1)
523             {
524               new_item = gtk_rb_tree_insert_before (self->items, item);
525               new_item->n_items = 1;
526               item->n_items--;
527               gtk_rb_tree_node_mark_dirty (item);
528             }
529           else
530             {
531               new_item = item;
532               item = gtk_rb_tree_node_get_next (item);
533             }
534           if (new_item->widget == NULL)
535             {
536               if (change)
537                 {
538                   new_item->widget = gtk_list_item_manager_try_reacquire_list_item (self,
539                                                                                     change,
540                                                                                     position + i,
541                                                                                     insert_after);
542                 }
543               if (new_item->widget == NULL)
544                 {
545                   new_item->widget = g_queue_pop_head (&released);
546                   if (new_item->widget)
547                     {
548                       gtk_list_item_manager_move_list_item (self,
549                                                             new_item->widget,
550                                                             position + i,
551                                                             insert_after);
552                     }
553                   else
554                     {
555                       new_item->widget = gtk_list_item_manager_acquire_list_item (self,
556                                                                                   position + i,
557                                                                                   insert_after);
558                     }
559                 }
560             }
561           else
562             {
563               if (update_start <= position + i)
564                 gtk_list_item_manager_update_list_item (self, new_item->widget, position + i);
565             }
566           insert_after = new_item->widget;
567         }
568       position += query_n_items;
569     }
570 
571   while ((widget = g_queue_pop_head (&released)))
572     gtk_list_item_manager_release_list_item (self, NULL, widget);
573 }
574 
575 static void
gtk_list_item_manager_model_items_changed_cb(GListModel * model,guint position,guint removed,guint added,GtkListItemManager * self)576 gtk_list_item_manager_model_items_changed_cb (GListModel         *model,
577                                               guint               position,
578                                               guint               removed,
579                                               guint               added,
580                                               GtkListItemManager *self)
581 {
582   GHashTable *change;
583   GSList *l;
584   guint n_items;
585 
586   n_items = g_list_model_get_n_items (G_LIST_MODEL (self->model));
587   change = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify )gtk_widget_unparent);
588 
589   gtk_list_item_manager_remove_items (self, change, position, removed);
590   gtk_list_item_manager_add_items (self, position, added);
591 
592   /* Check if any tracked item was removed */
593   for (l = self->trackers; l; l = l->next)
594     {
595       GtkListItemTracker *tracker = l->data;
596 
597       if (tracker->widget == NULL)
598         continue;
599 
600       if (g_hash_table_lookup (change, gtk_list_item_widget_get_item (tracker->widget)))
601         break;
602     }
603 
604   /* At least one tracked item was removed, do a more expensive rebuild
605    * trying to find where it moved */
606   if (l)
607     {
608       GtkListItemManagerItem *item, *new_item;
609       GtkWidget *insert_after;
610       guint i, offset;
611 
612       item = gtk_list_item_manager_get_nth (self, position, &offset);
613       for (new_item = item ? gtk_rb_tree_node_get_previous (item) : gtk_rb_tree_get_last (self->items);
614            new_item && new_item->widget == NULL;
615            new_item = gtk_rb_tree_node_get_previous (new_item))
616         { }
617       if (new_item)
618         insert_after = new_item->widget;
619       else
620         insert_after = NULL; /* we're at the start */
621 
622       for (i = 0; i < added; i++)
623         {
624           GtkWidget *widget;
625 
626           widget = gtk_list_item_manager_try_reacquire_list_item (self,
627                                                                   change,
628                                                                   position + i,
629                                                                   insert_after);
630           if (widget == NULL)
631             {
632               offset++;
633               continue;
634             }
635 
636           if (offset > 0)
637             {
638               new_item = gtk_rb_tree_insert_before (self->items, item);
639               new_item->n_items = offset;
640               item->n_items -= offset;
641               offset = 0;
642               gtk_rb_tree_node_mark_dirty (item);
643             }
644 
645           if (item->n_items == 1)
646             {
647               new_item = item;
648               item = gtk_rb_tree_node_get_next (item);
649             }
650           else
651             {
652               new_item = gtk_rb_tree_insert_before (self->items, item);
653               new_item->n_items = 1;
654               item->n_items--;
655               gtk_rb_tree_node_mark_dirty (item);
656             }
657 
658           new_item->widget = widget;
659           insert_after = widget;
660         }
661     }
662 
663   /* Update tracker positions if necessary, they need to have correct
664    * positions for gtk_list_item_manager_ensure_items().
665    * We don't update the items, they will be updated by ensure_items()
666    * and then we can update them. */
667   for (l = self->trackers; l; l = l->next)
668     {
669       GtkListItemTracker *tracker = l->data;
670 
671       if (tracker->position == GTK_INVALID_LIST_POSITION)
672         {
673           /* if the list is no longer empty, set the tracker to a valid position. */
674           if (n_items > 0 && n_items == added && removed == 0)
675             tracker->position = 0;
676         }
677       else if (tracker->position >= position + removed)
678         {
679           tracker->position += added - removed;
680         }
681       else if (tracker->position >= position)
682         {
683           if (g_hash_table_lookup (change, gtk_list_item_widget_get_item (tracker->widget)))
684             {
685               /* The item is gone. Guess a good new position */
686               tracker->position = position + (tracker->position - position) * added / removed;
687               if (tracker->position >= n_items)
688                 {
689                   if (n_items == 0)
690                     tracker->position = GTK_INVALID_LIST_POSITION;
691                   else
692                     tracker->position--;
693                 }
694               tracker->widget = NULL;
695             }
696           else
697             {
698               /* item was put in its right place in the expensive loop above,
699                * and we updated its position while at it. So grab it from there.
700                */
701               tracker->position = gtk_list_item_widget_get_position (tracker->widget);
702             }
703         }
704       else
705         {
706           /* nothing changed for items before position */
707         }
708     }
709 
710   gtk_list_item_manager_ensure_items (self, change, position + added);
711 
712   /* final loop through the trackers: Grab the missing widgets.
713    * For items that had been removed and a new position was set, grab
714    * their item now that we ensured it exists.
715    */
716   for (l = self->trackers; l; l = l->next)
717     {
718       GtkListItemTracker *tracker = l->data;
719       GtkListItemManagerItem *item;
720 
721       if (tracker->widget != NULL ||
722           tracker->position == GTK_INVALID_LIST_POSITION)
723         continue;
724 
725       item = gtk_list_item_manager_get_nth (self, tracker->position, NULL);
726       g_assert (item != NULL);
727       g_assert (item->widget);
728       tracker->widget = GTK_LIST_ITEM_WIDGET (item->widget);
729     }
730 
731   g_hash_table_unref (change);
732 
733   gtk_widget_queue_resize (self->widget);
734 }
735 
736 static void
gtk_list_item_manager_model_selection_changed_cb(GListModel * model,guint position,guint n_items,GtkListItemManager * self)737 gtk_list_item_manager_model_selection_changed_cb (GListModel         *model,
738                                                   guint               position,
739                                                   guint               n_items,
740                                                   GtkListItemManager *self)
741 {
742   GtkListItemManagerItem *item;
743   guint offset;
744 
745   item = gtk_list_item_manager_get_nth (self, position, &offset);
746 
747   if (offset)
748     {
749       position += item->n_items - offset;
750       if (item->n_items - offset > n_items)
751         n_items = 0;
752       else
753         n_items -= item->n_items - offset;
754       item = gtk_rb_tree_node_get_next (item);
755     }
756 
757   while (n_items > 0)
758     {
759       if (item->widget)
760         gtk_list_item_manager_update_list_item (self, item->widget, position);
761       position += item->n_items;
762       n_items -= MIN (n_items, item->n_items);
763       item = gtk_rb_tree_node_get_next (item);
764     }
765 }
766 
767 static void
gtk_list_item_manager_clear_model(GtkListItemManager * self)768 gtk_list_item_manager_clear_model (GtkListItemManager *self)
769 {
770   GSList *l;
771 
772   if (self->model == NULL)
773     return;
774 
775   gtk_list_item_manager_remove_items (self, NULL, 0, g_list_model_get_n_items (G_LIST_MODEL (self->model)));
776   for (l = self->trackers; l; l = l->next)
777     {
778       gtk_list_item_tracker_unset_position (self, l->data);
779     }
780 
781   g_signal_handlers_disconnect_by_func (self->model,
782                                         gtk_list_item_manager_model_selection_changed_cb,
783                                         self);
784   g_signal_handlers_disconnect_by_func (self->model,
785                                         gtk_list_item_manager_model_items_changed_cb,
786                                         self);
787   g_clear_object (&self->model);
788 }
789 
790 static void
gtk_list_item_manager_dispose(GObject * object)791 gtk_list_item_manager_dispose (GObject *object)
792 {
793   GtkListItemManager *self = GTK_LIST_ITEM_MANAGER (object);
794 
795   gtk_list_item_manager_clear_model (self);
796 
797   g_clear_object (&self->factory);
798 
799   g_clear_pointer (&self->items, gtk_rb_tree_unref);
800 
801   G_OBJECT_CLASS (gtk_list_item_manager_parent_class)->dispose (object);
802 }
803 
804 static void
gtk_list_item_manager_class_init(GtkListItemManagerClass * klass)805 gtk_list_item_manager_class_init (GtkListItemManagerClass *klass)
806 {
807   GObjectClass *object_class = G_OBJECT_CLASS (klass);
808 
809   object_class->dispose = gtk_list_item_manager_dispose;
810 }
811 
812 static void
gtk_list_item_manager_init(GtkListItemManager * self)813 gtk_list_item_manager_init (GtkListItemManager *self)
814 {
815 }
816 
817 void
gtk_list_item_manager_set_factory(GtkListItemManager * self,GtkListItemFactory * factory)818 gtk_list_item_manager_set_factory (GtkListItemManager *self,
819                                    GtkListItemFactory *factory)
820 {
821   guint n_items;
822   GSList *l;
823 
824   g_return_if_fail (GTK_IS_LIST_ITEM_MANAGER (self));
825   g_return_if_fail (GTK_IS_LIST_ITEM_FACTORY (factory));
826 
827   if (self->factory == factory)
828     return;
829 
830   n_items = self->model ? g_list_model_get_n_items (G_LIST_MODEL (self->model)) : 0;
831   gtk_list_item_manager_remove_items (self, NULL, 0, n_items);
832 
833   g_set_object (&self->factory, factory);
834 
835   gtk_list_item_manager_add_items (self, 0, n_items);
836 
837   gtk_list_item_manager_ensure_items (self, NULL, G_MAXUINT);
838 
839   for (l = self->trackers; l; l = l->next)
840     {
841       GtkListItemTracker *tracker = l->data;
842       GtkListItemManagerItem *item;
843 
844       if (tracker->widget == NULL)
845         continue;
846 
847       item = gtk_list_item_manager_get_nth (self, tracker->position, NULL);
848       g_assert (item);
849       tracker->widget = GTK_LIST_ITEM_WIDGET (item->widget);
850     }
851 }
852 
853 GtkListItemFactory *
gtk_list_item_manager_get_factory(GtkListItemManager * self)854 gtk_list_item_manager_get_factory (GtkListItemManager *self)
855 {
856   g_return_val_if_fail (GTK_IS_LIST_ITEM_MANAGER (self), NULL);
857 
858   return self->factory;
859 }
860 
861 void
gtk_list_item_manager_set_model(GtkListItemManager * self,GtkSelectionModel * model)862 gtk_list_item_manager_set_model (GtkListItemManager *self,
863                                  GtkSelectionModel  *model)
864 {
865   g_return_if_fail (GTK_IS_LIST_ITEM_MANAGER (self));
866   g_return_if_fail (model == NULL || GTK_IS_SELECTION_MODEL (model));
867 
868   if (self->model == model)
869     return;
870 
871   gtk_list_item_manager_clear_model (self);
872 
873   if (model)
874     {
875       self->model = g_object_ref (model);
876 
877       g_signal_connect (model,
878                         "items-changed",
879                         G_CALLBACK (gtk_list_item_manager_model_items_changed_cb),
880                         self);
881       g_signal_connect (model,
882                         "selection-changed",
883                         G_CALLBACK (gtk_list_item_manager_model_selection_changed_cb),
884                         self);
885 
886       gtk_list_item_manager_add_items (self, 0, g_list_model_get_n_items (G_LIST_MODEL (model)));
887     }
888 }
889 
890 GtkSelectionModel *
gtk_list_item_manager_get_model(GtkListItemManager * self)891 gtk_list_item_manager_get_model (GtkListItemManager *self)
892 {
893   g_return_val_if_fail (GTK_IS_LIST_ITEM_MANAGER (self), NULL);
894 
895   return self->model;
896 }
897 
898 /*
899  * gtk_list_item_manager_acquire_list_item:
900  * @self: a `GtkListItemManager`
901  * @position: the row in the model to create a list item for
902  * @prev_sibling: the widget this widget should be inserted before or %NULL
903  *   if it should be the first widget
904  *
905  * Creates a list item widget to use for @position. No widget may
906  * yet exist that is used for @position.
907  *
908  * When the returned item is no longer needed, the caller is responsible
909  * for calling gtk_list_item_manager_release_list_item().
910  * A particular case is when the row at @position is removed. In that case,
911  * all list items in the removed range must be released before
912  * gtk_list_item_manager_model_changed() is called.
913  *
914  * Returns: a properly setup widget to use in @position
915  **/
916 static GtkWidget *
gtk_list_item_manager_acquire_list_item(GtkListItemManager * self,guint position,GtkWidget * prev_sibling)917 gtk_list_item_manager_acquire_list_item (GtkListItemManager *self,
918                                          guint               position,
919                                          GtkWidget          *prev_sibling)
920 {
921   GtkWidget *result;
922   gpointer item;
923   gboolean selected;
924 
925   g_return_val_if_fail (GTK_IS_LIST_ITEM_MANAGER (self), NULL);
926   g_return_val_if_fail (prev_sibling == NULL || GTK_IS_WIDGET (prev_sibling), NULL);
927 
928   result = gtk_list_item_widget_new (self->factory,
929                                      self->item_css_name,
930                                      self->item_role);
931 
932   gtk_list_item_widget_set_single_click_activate (GTK_LIST_ITEM_WIDGET (result), self->single_click_activate);
933 
934   item = g_list_model_get_item (G_LIST_MODEL (self->model), position);
935   selected = gtk_selection_model_is_selected (self->model, position);
936   gtk_list_item_widget_update (GTK_LIST_ITEM_WIDGET (result), position, item, selected);
937   g_object_unref (item);
938   gtk_widget_insert_after (result, self->widget, prev_sibling);
939 
940   return GTK_WIDGET (result);
941 }
942 
943 /**
944  * gtk_list_item_manager_try_acquire_list_item_from_change:
945  * @self: a `GtkListItemManager`
946  * @position: the row in the model to create a list item for
947  * @prev_sibling: the widget this widget should be inserted after or %NULL
948  *   if it should be the first widget
949  *
950  * Like gtk_list_item_manager_acquire_list_item(), but only tries to acquire list
951  * items from those previously released as part of @change.
952  * If no matching list item is found, %NULL is returned and the caller should use
953  * gtk_list_item_manager_acquire_list_item().
954  *
955  * Returns: (nullable): a properly setup widget to use in @position or %NULL if
956  *   no item for reuse existed
957  **/
958 static GtkWidget *
gtk_list_item_manager_try_reacquire_list_item(GtkListItemManager * self,GHashTable * change,guint position,GtkWidget * prev_sibling)959 gtk_list_item_manager_try_reacquire_list_item (GtkListItemManager *self,
960                                                GHashTable         *change,
961                                                guint               position,
962                                                GtkWidget          *prev_sibling)
963 {
964   GtkWidget *result;
965   gpointer item;
966 
967   g_return_val_if_fail (GTK_IS_LIST_ITEM_MANAGER (self), NULL);
968   g_return_val_if_fail (prev_sibling == NULL || GTK_IS_WIDGET (prev_sibling), NULL);
969 
970   /* XXX: can we avoid temporarily allocating items on failure? */
971   item = g_list_model_get_item (G_LIST_MODEL (self->model), position);
972   if (g_hash_table_steal_extended (change, item, NULL, (gpointer *) &result))
973     {
974       GtkListItemWidget *list_item = GTK_LIST_ITEM_WIDGET (result);
975       gtk_list_item_widget_update (list_item,
976                                    position,
977                                    gtk_list_item_widget_get_item (list_item),
978                                    gtk_selection_model_is_selected (self->model, position));
979       gtk_widget_insert_after (result, self->widget, prev_sibling);
980       /* XXX: Should we let the listview do this? */
981       gtk_widget_queue_resize (result);
982     }
983   else
984     {
985       result = NULL;
986     }
987   g_object_unref (item);
988 
989   return result;
990 }
991 
992 /**
993  * gtk_list_item_manager_move_list_item:
994  * @self: a `GtkListItemManager`
995  * @list_item: an acquired `GtkListItem` that should be moved to represent
996  *   a different row
997  * @position: the new position of that list item
998  * @prev_sibling: the new previous sibling
999  *
1000  * Moves the widget to represent a new position in the listmodel without
1001  * releasing the item.
1002  *
1003  * This is most useful when scrolling.
1004  **/
1005 static void
gtk_list_item_manager_move_list_item(GtkListItemManager * self,GtkWidget * list_item,guint position,GtkWidget * prev_sibling)1006 gtk_list_item_manager_move_list_item (GtkListItemManager     *self,
1007                                       GtkWidget              *list_item,
1008                                       guint                   position,
1009                                       GtkWidget              *prev_sibling)
1010 {
1011   gpointer item;
1012   gboolean selected;
1013 
1014   item = g_list_model_get_item (G_LIST_MODEL (self->model), position);
1015   selected = gtk_selection_model_is_selected (self->model, position);
1016   gtk_list_item_widget_update (GTK_LIST_ITEM_WIDGET (list_item),
1017                                position,
1018                                item,
1019                                selected);
1020   gtk_widget_insert_after (list_item, _gtk_widget_get_parent (list_item), prev_sibling);
1021   g_object_unref (item);
1022 }
1023 
1024 /**
1025  * gtk_list_item_manager_update_list_item:
1026  * @self: a `GtkListItemManager`
1027  * @item: a `GtkListItem` that has been acquired
1028  * @position: the new position of that list item
1029  *
1030  * Updates the position of the given @item. This function must be called whenever
1031  * the position of an item changes, like when new items are added before it.
1032  **/
1033 static void
gtk_list_item_manager_update_list_item(GtkListItemManager * self,GtkWidget * item,guint position)1034 gtk_list_item_manager_update_list_item (GtkListItemManager *self,
1035                                         GtkWidget          *item,
1036                                         guint               position)
1037 {
1038   GtkListItemWidget *list_item = GTK_LIST_ITEM_WIDGET (item);
1039   gboolean selected;
1040 
1041   g_return_if_fail (GTK_IS_LIST_ITEM_MANAGER (self));
1042   g_return_if_fail (GTK_IS_LIST_ITEM_WIDGET (item));
1043 
1044   selected = gtk_selection_model_is_selected (self->model, position);
1045   gtk_list_item_widget_update (list_item,
1046                                position,
1047                                gtk_list_item_widget_get_item (list_item),
1048                                selected);
1049 }
1050 
1051 /*
1052  * gtk_list_item_manager_release_list_item:
1053  * @self: a `GtkListItemManager`
1054  * @change: (nullable): The change associated with this release or
1055  *   %NULL if this is a final removal
1056  * @item: an item previously acquired with
1057  *   gtk_list_item_manager_acquire_list_item()
1058  *
1059  * Releases an item that was previously acquired via
1060  * gtk_list_item_manager_acquire_list_item() and is no longer in use.
1061  **/
1062 static void
gtk_list_item_manager_release_list_item(GtkListItemManager * self,GHashTable * change,GtkWidget * item)1063 gtk_list_item_manager_release_list_item (GtkListItemManager *self,
1064                                          GHashTable         *change,
1065                                          GtkWidget          *item)
1066 {
1067   g_return_if_fail (GTK_IS_LIST_ITEM_MANAGER (self));
1068   g_return_if_fail (GTK_IS_LIST_ITEM_WIDGET (item));
1069 
1070   if (change != NULL)
1071     {
1072       if (!g_hash_table_replace (change, gtk_list_item_widget_get_item (GTK_LIST_ITEM_WIDGET (item)), item))
1073         {
1074           g_warning ("FIXME: Handle the same item multiple times in the list.\nLars says this totally should not happen, but here we are.");
1075         }
1076 
1077       return;
1078     }
1079 
1080   gtk_widget_unparent (item);
1081 }
1082 
1083 void
gtk_list_item_manager_set_single_click_activate(GtkListItemManager * self,gboolean single_click_activate)1084 gtk_list_item_manager_set_single_click_activate (GtkListItemManager *self,
1085                                                  gboolean            single_click_activate)
1086 {
1087   GtkListItemManagerItem *item;
1088 
1089   g_return_if_fail (GTK_IS_LIST_ITEM_MANAGER (self));
1090 
1091   self->single_click_activate = single_click_activate;
1092 
1093   for (item = gtk_rb_tree_get_first (self->items);
1094        item != NULL;
1095        item = gtk_rb_tree_node_get_next (item))
1096     {
1097       if (item->widget)
1098         gtk_list_item_widget_set_single_click_activate (GTK_LIST_ITEM_WIDGET (item->widget), single_click_activate);
1099     }
1100 }
1101 
1102 gboolean
gtk_list_item_manager_get_single_click_activate(GtkListItemManager * self)1103 gtk_list_item_manager_get_single_click_activate (GtkListItemManager   *self)
1104 {
1105   g_return_val_if_fail (GTK_IS_LIST_ITEM_MANAGER (self), FALSE);
1106 
1107   return self->single_click_activate;
1108 }
1109 
1110 GtkListItemTracker *
gtk_list_item_tracker_new(GtkListItemManager * self)1111 gtk_list_item_tracker_new (GtkListItemManager *self)
1112 {
1113   GtkListItemTracker *tracker;
1114 
1115   g_return_val_if_fail (GTK_IS_LIST_ITEM_MANAGER (self), NULL);
1116 
1117   tracker = g_slice_new0 (GtkListItemTracker);
1118 
1119   tracker->position = GTK_INVALID_LIST_POSITION;
1120 
1121   self->trackers = g_slist_prepend (self->trackers, tracker);
1122 
1123   return tracker;
1124 }
1125 
1126 void
gtk_list_item_tracker_free(GtkListItemManager * self,GtkListItemTracker * tracker)1127 gtk_list_item_tracker_free (GtkListItemManager *self,
1128                             GtkListItemTracker *tracker)
1129 {
1130   gtk_list_item_tracker_unset_position (self, tracker);
1131 
1132   self->trackers = g_slist_remove (self->trackers, tracker);
1133 
1134   g_slice_free (GtkListItemTracker, tracker);
1135 
1136   gtk_list_item_manager_ensure_items (self, NULL, G_MAXUINT);
1137 
1138   gtk_widget_queue_resize (self->widget);
1139 }
1140 
1141 void
gtk_list_item_tracker_set_position(GtkListItemManager * self,GtkListItemTracker * tracker,guint position,guint n_before,guint n_after)1142 gtk_list_item_tracker_set_position (GtkListItemManager *self,
1143                                     GtkListItemTracker *tracker,
1144                                     guint               position,
1145                                     guint               n_before,
1146                                     guint               n_after)
1147 {
1148   GtkListItemManagerItem *item;
1149   guint n_items;
1150 
1151   gtk_list_item_tracker_unset_position (self, tracker);
1152 
1153   if (self->model == NULL)
1154     return;
1155 
1156   n_items = g_list_model_get_n_items (G_LIST_MODEL (self->model));
1157   if (position >= n_items)
1158     position = n_items - 1; /* for n_items == 0 this underflows to GTK_INVALID_LIST_POSITION */
1159 
1160   tracker->position = position;
1161   tracker->n_before = n_before;
1162   tracker->n_after = n_after;
1163 
1164   gtk_list_item_manager_ensure_items (self, NULL, G_MAXUINT);
1165 
1166   item = gtk_list_item_manager_get_nth (self, position, NULL);
1167   if (item)
1168     tracker->widget = GTK_LIST_ITEM_WIDGET (item->widget);
1169 
1170   gtk_widget_queue_resize (self->widget);
1171 }
1172 
1173 guint
gtk_list_item_tracker_get_position(GtkListItemManager * self,GtkListItemTracker * tracker)1174 gtk_list_item_tracker_get_position (GtkListItemManager *self,
1175                                     GtkListItemTracker *tracker)
1176 {
1177   return tracker->position;
1178 }
1179