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