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); |
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, node: 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, node: right); |
99 | |
100 | aug->n_items += right_aug->n_items; |
101 | } |
102 | } |
103 | |
104 | static void |
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 * |
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 (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 | clear_func: gtk_list_item_manager_clear_node, |
137 | NULL); |
138 | |
139 | return self; |
140 | } |
141 | |
142 | gpointer |
143 | gtk_list_item_manager_get_first (GtkListItemManager *self) |
144 | { |
145 | return gtk_rb_tree_get_first (tree: self->items); |
146 | } |
147 | |
148 | gpointer |
149 | gtk_list_item_manager_get_root (GtkListItemManager *self) |
150 | { |
151 | return gtk_rb_tree_get_root (tree: 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 |
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 (tree: self->items); |
177 | |
178 | while (item) |
179 | { |
180 | tmp = gtk_rb_tree_node_get_left (node: item); |
181 | if (tmp) |
182 | { |
183 | GtkListItemManagerItemAugment *aug = gtk_rb_tree_get_augment (tree: self->items, node: 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 (node: item); |
197 | } |
198 | |
199 | if (offset) |
200 | *offset = item ? position : 0; |
201 | |
202 | return item; |
203 | } |
204 | |
205 | guint |
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 (node: item); |
213 | if (left) |
214 | { |
215 | GtkListItemManagerItemAugment *aug = gtk_rb_tree_get_augment (tree: self->items, node: left); |
216 | pos = aug->n_items; |
217 | } |
218 | else |
219 | { |
220 | pos = 0; |
221 | } |
222 | |
223 | for (parent = gtk_rb_tree_node_get_parent (node: item); |
224 | parent != NULL; |
225 | parent = gtk_rb_tree_node_get_parent (node: item)) |
226 | { |
227 | left = gtk_rb_tree_node_get_left (node: parent); |
228 | |
229 | if (left != item) |
230 | { |
231 | if (left) |
232 | { |
233 | GtkListItemManagerItemAugment *aug = gtk_rb_tree_get_augment (tree: self->items, node: 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 |
246 | gtk_list_item_manager_get_item_augment (GtkListItemManager *self, |
247 | gpointer item) |
248 | { |
249 | return gtk_rb_tree_get_augment (tree: self->items, node: item); |
250 | } |
251 | |
252 | static void |
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 |
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 |
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, tracker: l->data, n_items, out_start: &tracker_start, out_n_items: &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, tracker: l->data, n_items, out_start: &tracker_start, out_n_items: &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 |
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 (node: item); |
369 | n_items = 0; |
370 | } |
371 | else |
372 | { |
373 | GtkListItemManagerItem *next = gtk_rb_tree_node_get_next (node: item); |
374 | if (item->widget) |
375 | gtk_list_item_manager_release_list_item (self, change, widget: item->widget); |
376 | item->widget = NULL; |
377 | n_items -= item->n_items; |
378 | gtk_rb_tree_remove (tree: self->items, node: item); |
379 | item = next; |
380 | } |
381 | } |
382 | |
383 | gtk_widget_queue_resize (GTK_WIDGET (self->widget)); |
384 | } |
385 | |
386 | static void |
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: &offset); |
398 | |
399 | if (item == NULL || item->widget) |
400 | item = gtk_rb_tree_insert_before (tree: self->items, node: item); |
401 | item->n_items += n_items; |
402 | gtk_rb_tree_node_mark_dirty (node: item); |
403 | |
404 | gtk_widget_queue_resize (GTK_WIDGET (self->widget)); |
405 | } |
406 | |
407 | static gboolean |
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 (node: first); |
417 | gtk_rb_tree_remove (tree: self->items, node: second); |
418 | |
419 | return TRUE; |
420 | } |
421 | |
422 | static void |
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 (list: G_LIST_MODEL (ptr: self->model)); |
431 | position = 0; |
432 | |
433 | while (position < n_items) |
434 | { |
435 | gtk_list_item_query_tracked_range (self, n_items, position, out_n_items: &query_n_items, out_tracked: &tracked); |
436 | if (tracked) |
437 | { |
438 | position += query_n_items; |
439 | continue; |
440 | } |
441 | |
442 | item = gtk_list_item_manager_get_nth (self, position, offset: &i); |
443 | i = position - i; |
444 | while (i < position + query_n_items) |
445 | { |
446 | g_assert (item != NULL); |
447 | if (item->widget) |
448 | { |
449 | g_queue_push_tail (queue: released, data: item->widget); |
450 | item->widget = NULL; |
451 | i++; |
452 | prev = gtk_rb_tree_node_get_previous (node: item); |
453 | if (prev && gtk_list_item_manager_merge_list_items (self, first: prev, second: item)) |
454 | item = prev; |
455 | next = gtk_rb_tree_node_get_next (node: item); |
456 | if (next && next->widget == NULL) |
457 | { |
458 | i += next->n_items; |
459 | if (!gtk_list_item_manager_merge_list_items (self, first: next, second: item)) |
460 | g_assert_not_reached (); |
461 | item = gtk_rb_tree_node_get_next (node: next); |
462 | } |
463 | else |
464 | { |
465 | item = next; |
466 | } |
467 | } |
468 | else |
469 | { |
470 | i += item->n_items; |
471 | item = gtk_rb_tree_node_get_next (node: item); |
472 | } |
473 | } |
474 | position += query_n_items; |
475 | } |
476 | } |
477 | |
478 | static void |
479 | gtk_list_item_manager_ensure_items (GtkListItemManager *self, |
480 | GHashTable *change, |
481 | guint update_start) |
482 | { |
483 | GtkListItemManagerItem *item, *new_item; |
484 | GtkWidget *widget, *insert_after; |
485 | guint position, i, n_items, query_n_items, offset; |
486 | GQueue released = G_QUEUE_INIT; |
487 | gboolean tracked; |
488 | |
489 | if (self->model == NULL) |
490 | return; |
491 | |
492 | n_items = g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self->model)); |
493 | position = 0; |
494 | |
495 | gtk_list_item_manager_release_items (self, released: &released); |
496 | |
497 | while (position < n_items) |
498 | { |
499 | gtk_list_item_query_tracked_range (self, n_items, position, out_n_items: &query_n_items, out_tracked: &tracked); |
500 | if (!tracked) |
501 | { |
502 | position += query_n_items; |
503 | continue; |
504 | } |
505 | |
506 | item = gtk_list_item_manager_get_nth (self, position, offset: &offset); |
507 | for (new_item = item; |
508 | new_item && new_item->widget == NULL; |
509 | new_item = gtk_rb_tree_node_get_previous (node: new_item)) |
510 | { /* do nothing */ } |
511 | insert_after = new_item ? new_item->widget : NULL; |
512 | |
513 | if (offset > 0) |
514 | { |
515 | g_assert (item != NULL); |
516 | new_item = gtk_rb_tree_insert_before (tree: self->items, node: item); |
517 | new_item->n_items = offset; |
518 | item->n_items -= offset; |
519 | gtk_rb_tree_node_mark_dirty (node: item); |
520 | } |
521 | |
522 | for (i = 0; i < query_n_items; i++) |
523 | { |
524 | g_assert (item != NULL); |
525 | if (item->n_items > 1) |
526 | { |
527 | new_item = gtk_rb_tree_insert_before (tree: self->items, node: item); |
528 | new_item->n_items = 1; |
529 | item->n_items--; |
530 | gtk_rb_tree_node_mark_dirty (node: item); |
531 | } |
532 | else |
533 | { |
534 | new_item = item; |
535 | item = gtk_rb_tree_node_get_next (node: item); |
536 | } |
537 | if (new_item->widget == NULL) |
538 | { |
539 | if (change) |
540 | { |
541 | new_item->widget = gtk_list_item_manager_try_reacquire_list_item (self, |
542 | change, |
543 | position: position + i, |
544 | prev_sibling: insert_after); |
545 | } |
546 | if (new_item->widget == NULL) |
547 | { |
548 | new_item->widget = g_queue_pop_head (queue: &released); |
549 | if (new_item->widget) |
550 | { |
551 | gtk_list_item_manager_move_list_item (self, |
552 | list_item: new_item->widget, |
553 | position: position + i, |
554 | prev_sibling: insert_after); |
555 | } |
556 | else |
557 | { |
558 | new_item->widget = gtk_list_item_manager_acquire_list_item (self, |
559 | position: position + i, |
560 | prev_sibling: insert_after); |
561 | } |
562 | } |
563 | } |
564 | else |
565 | { |
566 | if (update_start <= position + i) |
567 | gtk_list_item_manager_update_list_item (self, item: new_item->widget, position: position + i); |
568 | } |
569 | insert_after = new_item->widget; |
570 | } |
571 | position += query_n_items; |
572 | } |
573 | |
574 | while ((widget = g_queue_pop_head (queue: &released))) |
575 | gtk_list_item_manager_release_list_item (self, NULL, widget); |
576 | } |
577 | |
578 | static void |
579 | gtk_list_item_manager_model_items_changed_cb (GListModel *model, |
580 | guint position, |
581 | guint removed, |
582 | guint added, |
583 | GtkListItemManager *self) |
584 | { |
585 | GHashTable *change; |
586 | GSList *l; |
587 | guint n_items; |
588 | |
589 | n_items = g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self->model)); |
590 | change = g_hash_table_new_full (hash_func: g_direct_hash, key_equal_func: g_direct_equal, NULL, value_destroy_func: (GDestroyNotify )gtk_widget_unparent); |
591 | |
592 | gtk_list_item_manager_remove_items (self, change, position, n_items: removed); |
593 | gtk_list_item_manager_add_items (self, position, n_items: added); |
594 | |
595 | /* Check if any tracked item was removed */ |
596 | for (l = self->trackers; l; l = l->next) |
597 | { |
598 | GtkListItemTracker *tracker = l->data; |
599 | |
600 | if (tracker->widget == NULL) |
601 | continue; |
602 | |
603 | if (g_hash_table_lookup (hash_table: change, key: gtk_list_item_widget_get_item (self: tracker->widget))) |
604 | break; |
605 | } |
606 | |
607 | /* At least one tracked item was removed, do a more expensive rebuild |
608 | * trying to find where it moved */ |
609 | if (l) |
610 | { |
611 | GtkListItemManagerItem *item, *new_item; |
612 | GtkWidget *insert_after; |
613 | guint i, offset; |
614 | |
615 | item = gtk_list_item_manager_get_nth (self, position, offset: &offset); |
616 | for (new_item = item ? gtk_rb_tree_node_get_previous (node: item) : gtk_rb_tree_get_last (tree: self->items); |
617 | new_item && new_item->widget == NULL; |
618 | new_item = gtk_rb_tree_node_get_previous (node: new_item)) |
619 | { } |
620 | if (new_item) |
621 | insert_after = new_item->widget; |
622 | else |
623 | insert_after = NULL; /* we're at the start */ |
624 | |
625 | for (i = 0; i < added; i++) |
626 | { |
627 | GtkWidget *widget; |
628 | |
629 | widget = gtk_list_item_manager_try_reacquire_list_item (self, |
630 | change, |
631 | position: position + i, |
632 | prev_sibling: insert_after); |
633 | if (widget == NULL) |
634 | { |
635 | offset++; |
636 | continue; |
637 | } |
638 | |
639 | if (offset > 0) |
640 | { |
641 | new_item = gtk_rb_tree_insert_before (tree: self->items, node: item); |
642 | new_item->n_items = offset; |
643 | item->n_items -= offset; |
644 | offset = 0; |
645 | gtk_rb_tree_node_mark_dirty (node: item); |
646 | } |
647 | |
648 | if (item->n_items == 1) |
649 | { |
650 | new_item = item; |
651 | item = gtk_rb_tree_node_get_next (node: item); |
652 | } |
653 | else |
654 | { |
655 | new_item = gtk_rb_tree_insert_before (tree: self->items, node: item); |
656 | new_item->n_items = 1; |
657 | item->n_items--; |
658 | gtk_rb_tree_node_mark_dirty (node: item); |
659 | } |
660 | |
661 | new_item->widget = widget; |
662 | insert_after = widget; |
663 | } |
664 | } |
665 | |
666 | /* Update tracker positions if necessary, they need to have correct |
667 | * positions for gtk_list_item_manager_ensure_items(). |
668 | * We don't update the items, they will be updated by ensure_items() |
669 | * and then we can update them. */ |
670 | for (l = self->trackers; l; l = l->next) |
671 | { |
672 | GtkListItemTracker *tracker = l->data; |
673 | |
674 | if (tracker->position == GTK_INVALID_LIST_POSITION) |
675 | { |
676 | /* if the list is no longer empty, set the tracker to a valid position. */ |
677 | if (n_items > 0 && n_items == added && removed == 0) |
678 | tracker->position = 0; |
679 | } |
680 | else if (tracker->position >= position + removed) |
681 | { |
682 | tracker->position += added - removed; |
683 | } |
684 | else if (tracker->position >= position) |
685 | { |
686 | if (g_hash_table_lookup (hash_table: change, key: gtk_list_item_widget_get_item (self: tracker->widget))) |
687 | { |
688 | /* The item is gone. Guess a good new position */ |
689 | tracker->position = position + (tracker->position - position) * added / removed; |
690 | if (tracker->position >= n_items) |
691 | { |
692 | if (n_items == 0) |
693 | tracker->position = GTK_INVALID_LIST_POSITION; |
694 | else |
695 | tracker->position--; |
696 | } |
697 | tracker->widget = NULL; |
698 | } |
699 | else |
700 | { |
701 | /* item was put in its right place in the expensive loop above, |
702 | * and we updated its position while at it. So grab it from there. |
703 | */ |
704 | tracker->position = gtk_list_item_widget_get_position (self: tracker->widget); |
705 | } |
706 | } |
707 | else |
708 | { |
709 | /* nothing changed for items before position */ |
710 | } |
711 | } |
712 | |
713 | gtk_list_item_manager_ensure_items (self, change, update_start: position + added); |
714 | |
715 | /* final loop through the trackers: Grab the missing widgets. |
716 | * For items that had been removed and a new position was set, grab |
717 | * their item now that we ensured it exists. |
718 | */ |
719 | for (l = self->trackers; l; l = l->next) |
720 | { |
721 | GtkListItemTracker *tracker = l->data; |
722 | GtkListItemManagerItem *item; |
723 | |
724 | if (tracker->widget != NULL || |
725 | tracker->position == GTK_INVALID_LIST_POSITION) |
726 | continue; |
727 | |
728 | item = gtk_list_item_manager_get_nth (self, position: tracker->position, NULL); |
729 | g_assert (item != NULL); |
730 | g_assert (item->widget); |
731 | tracker->widget = GTK_LIST_ITEM_WIDGET (item->widget); |
732 | } |
733 | |
734 | g_hash_table_unref (hash_table: change); |
735 | |
736 | gtk_widget_queue_resize (widget: self->widget); |
737 | } |
738 | |
739 | static void |
740 | gtk_list_item_manager_model_selection_changed_cb (GListModel *model, |
741 | guint position, |
742 | guint n_items, |
743 | GtkListItemManager *self) |
744 | { |
745 | GtkListItemManagerItem *item; |
746 | guint offset; |
747 | |
748 | item = gtk_list_item_manager_get_nth (self, position, offset: &offset); |
749 | |
750 | if (offset) |
751 | { |
752 | position += item->n_items - offset; |
753 | if (item->n_items - offset > n_items) |
754 | n_items = 0; |
755 | else |
756 | n_items -= item->n_items - offset; |
757 | item = gtk_rb_tree_node_get_next (node: item); |
758 | } |
759 | |
760 | while (n_items > 0) |
761 | { |
762 | if (item->widget) |
763 | gtk_list_item_manager_update_list_item (self, item: item->widget, position); |
764 | position += item->n_items; |
765 | n_items -= MIN (n_items, item->n_items); |
766 | item = gtk_rb_tree_node_get_next (node: item); |
767 | } |
768 | } |
769 | |
770 | static void |
771 | gtk_list_item_manager_clear_model (GtkListItemManager *self) |
772 | { |
773 | GSList *l; |
774 | |
775 | if (self->model == NULL) |
776 | return; |
777 | |
778 | gtk_list_item_manager_remove_items (self, NULL, position: 0, n_items: g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self->model))); |
779 | for (l = self->trackers; l; l = l->next) |
780 | { |
781 | gtk_list_item_tracker_unset_position (self, tracker: l->data); |
782 | } |
783 | |
784 | g_signal_handlers_disconnect_by_func (self->model, |
785 | gtk_list_item_manager_model_selection_changed_cb, |
786 | self); |
787 | g_signal_handlers_disconnect_by_func (self->model, |
788 | gtk_list_item_manager_model_items_changed_cb, |
789 | self); |
790 | g_clear_object (&self->model); |
791 | } |
792 | |
793 | static void |
794 | gtk_list_item_manager_dispose (GObject *object) |
795 | { |
796 | GtkListItemManager *self = GTK_LIST_ITEM_MANAGER (object); |
797 | |
798 | gtk_list_item_manager_clear_model (self); |
799 | |
800 | g_clear_object (&self->factory); |
801 | |
802 | g_clear_pointer (&self->items, gtk_rb_tree_unref); |
803 | |
804 | G_OBJECT_CLASS (gtk_list_item_manager_parent_class)->dispose (object); |
805 | } |
806 | |
807 | static void |
808 | gtk_list_item_manager_class_init (GtkListItemManagerClass *klass) |
809 | { |
810 | GObjectClass *object_class = G_OBJECT_CLASS (klass); |
811 | |
812 | object_class->dispose = gtk_list_item_manager_dispose; |
813 | } |
814 | |
815 | static void |
816 | gtk_list_item_manager_init (GtkListItemManager *self) |
817 | { |
818 | } |
819 | |
820 | void |
821 | gtk_list_item_manager_set_factory (GtkListItemManager *self, |
822 | GtkListItemFactory *factory) |
823 | { |
824 | guint n_items; |
825 | GSList *l; |
826 | |
827 | g_return_if_fail (GTK_IS_LIST_ITEM_MANAGER (self)); |
828 | g_return_if_fail (factory == NULL || GTK_IS_LIST_ITEM_FACTORY (factory)); |
829 | |
830 | if (self->factory == factory) |
831 | return; |
832 | |
833 | n_items = self->model ? g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self->model)) : 0; |
834 | gtk_list_item_manager_remove_items (self, NULL, position: 0, n_items); |
835 | |
836 | g_set_object (&self->factory, factory); |
837 | |
838 | gtk_list_item_manager_add_items (self, position: 0, n_items); |
839 | |
840 | gtk_list_item_manager_ensure_items (self, NULL, G_MAXUINT); |
841 | |
842 | for (l = self->trackers; l; l = l->next) |
843 | { |
844 | GtkListItemTracker *tracker = l->data; |
845 | GtkListItemManagerItem *item; |
846 | |
847 | if (tracker->widget == NULL) |
848 | continue; |
849 | |
850 | item = gtk_list_item_manager_get_nth (self, position: tracker->position, NULL); |
851 | g_assert (item); |
852 | tracker->widget = GTK_LIST_ITEM_WIDGET (item->widget); |
853 | } |
854 | } |
855 | |
856 | GtkListItemFactory * |
857 | gtk_list_item_manager_get_factory (GtkListItemManager *self) |
858 | { |
859 | g_return_val_if_fail (GTK_IS_LIST_ITEM_MANAGER (self), NULL); |
860 | |
861 | return self->factory; |
862 | } |
863 | |
864 | void |
865 | gtk_list_item_manager_set_model (GtkListItemManager *self, |
866 | GtkSelectionModel *model) |
867 | { |
868 | g_return_if_fail (GTK_IS_LIST_ITEM_MANAGER (self)); |
869 | g_return_if_fail (model == NULL || GTK_IS_SELECTION_MODEL (model)); |
870 | |
871 | if (self->model == model) |
872 | return; |
873 | |
874 | gtk_list_item_manager_clear_model (self); |
875 | |
876 | if (model) |
877 | { |
878 | self->model = g_object_ref (model); |
879 | |
880 | g_signal_connect (model, |
881 | "items-changed" , |
882 | G_CALLBACK (gtk_list_item_manager_model_items_changed_cb), |
883 | self); |
884 | g_signal_connect (model, |
885 | "selection-changed" , |
886 | G_CALLBACK (gtk_list_item_manager_model_selection_changed_cb), |
887 | self); |
888 | |
889 | gtk_list_item_manager_add_items (self, position: 0, n_items: g_list_model_get_n_items (list: G_LIST_MODEL (ptr: model))); |
890 | } |
891 | } |
892 | |
893 | GtkSelectionModel * |
894 | gtk_list_item_manager_get_model (GtkListItemManager *self) |
895 | { |
896 | g_return_val_if_fail (GTK_IS_LIST_ITEM_MANAGER (self), NULL); |
897 | |
898 | return self->model; |
899 | } |
900 | |
901 | /* |
902 | * gtk_list_item_manager_acquire_list_item: |
903 | * @self: a `GtkListItemManager` |
904 | * @position: the row in the model to create a list item for |
905 | * @prev_sibling: the widget this widget should be inserted before or %NULL |
906 | * if it should be the first widget |
907 | * |
908 | * Creates a list item widget to use for @position. No widget may |
909 | * yet exist that is used for @position. |
910 | * |
911 | * When the returned item is no longer needed, the caller is responsible |
912 | * for calling gtk_list_item_manager_release_list_item(). |
913 | * A particular case is when the row at @position is removed. In that case, |
914 | * all list items in the removed range must be released before |
915 | * gtk_list_item_manager_model_changed() is called. |
916 | * |
917 | * Returns: a properly setup widget to use in @position |
918 | **/ |
919 | static GtkWidget * |
920 | gtk_list_item_manager_acquire_list_item (GtkListItemManager *self, |
921 | guint position, |
922 | GtkWidget *prev_sibling) |
923 | { |
924 | GtkWidget *result; |
925 | gpointer item; |
926 | gboolean selected; |
927 | |
928 | g_return_val_if_fail (GTK_IS_LIST_ITEM_MANAGER (self), NULL); |
929 | g_return_val_if_fail (prev_sibling == NULL || GTK_IS_WIDGET (prev_sibling), NULL); |
930 | |
931 | result = gtk_list_item_widget_new (factory: self->factory, |
932 | css_name: self->item_css_name, |
933 | role: self->item_role); |
934 | |
935 | gtk_list_item_widget_set_single_click_activate (GTK_LIST_ITEM_WIDGET (result), single_click_activate: self->single_click_activate); |
936 | |
937 | item = g_list_model_get_item (list: G_LIST_MODEL (ptr: self->model), position); |
938 | selected = gtk_selection_model_is_selected (model: self->model, position); |
939 | gtk_list_item_widget_update (GTK_LIST_ITEM_WIDGET (result), position, item, selected); |
940 | g_object_unref (object: item); |
941 | gtk_widget_insert_after (widget: result, parent: self->widget, previous_sibling: prev_sibling); |
942 | |
943 | return GTK_WIDGET (result); |
944 | } |
945 | |
946 | /** |
947 | * gtk_list_item_manager_try_acquire_list_item_from_change: |
948 | * @self: a `GtkListItemManager` |
949 | * @position: the row in the model to create a list item for |
950 | * @prev_sibling: the widget this widget should be inserted after or %NULL |
951 | * if it should be the first widget |
952 | * |
953 | * Like gtk_list_item_manager_acquire_list_item(), but only tries to acquire list |
954 | * items from those previously released as part of @change. |
955 | * If no matching list item is found, %NULL is returned and the caller should use |
956 | * gtk_list_item_manager_acquire_list_item(). |
957 | * |
958 | * Returns: (nullable): a properly setup widget to use in @position or %NULL if |
959 | * no item for reuse existed |
960 | **/ |
961 | static GtkWidget * |
962 | gtk_list_item_manager_try_reacquire_list_item (GtkListItemManager *self, |
963 | GHashTable *change, |
964 | guint position, |
965 | GtkWidget *prev_sibling) |
966 | { |
967 | GtkWidget *result; |
968 | gpointer item; |
969 | |
970 | g_return_val_if_fail (GTK_IS_LIST_ITEM_MANAGER (self), NULL); |
971 | g_return_val_if_fail (prev_sibling == NULL || GTK_IS_WIDGET (prev_sibling), NULL); |
972 | |
973 | /* XXX: can we avoid temporarily allocating items on failure? */ |
974 | item = g_list_model_get_item (list: G_LIST_MODEL (ptr: self->model), position); |
975 | if (g_hash_table_steal_extended (hash_table: change, lookup_key: item, NULL, stolen_value: (gpointer *) &result)) |
976 | { |
977 | GtkListItemWidget *list_item = GTK_LIST_ITEM_WIDGET (result); |
978 | gtk_list_item_widget_update (self: list_item, |
979 | position, |
980 | item: gtk_list_item_widget_get_item (self: list_item), |
981 | selected: gtk_selection_model_is_selected (model: self->model, position)); |
982 | gtk_widget_insert_after (widget: result, parent: self->widget, previous_sibling: prev_sibling); |
983 | /* XXX: Should we let the listview do this? */ |
984 | gtk_widget_queue_resize (widget: result); |
985 | } |
986 | else |
987 | { |
988 | result = NULL; |
989 | } |
990 | g_object_unref (object: item); |
991 | |
992 | return result; |
993 | } |
994 | |
995 | /** |
996 | * gtk_list_item_manager_move_list_item: |
997 | * @self: a `GtkListItemManager` |
998 | * @list_item: an acquired `GtkListItem` that should be moved to represent |
999 | * a different row |
1000 | * @position: the new position of that list item |
1001 | * @prev_sibling: the new previous sibling |
1002 | * |
1003 | * Moves the widget to represent a new position in the listmodel without |
1004 | * releasing the item. |
1005 | * |
1006 | * This is most useful when scrolling. |
1007 | **/ |
1008 | static void |
1009 | gtk_list_item_manager_move_list_item (GtkListItemManager *self, |
1010 | GtkWidget *list_item, |
1011 | guint position, |
1012 | GtkWidget *prev_sibling) |
1013 | { |
1014 | gpointer item; |
1015 | gboolean selected; |
1016 | |
1017 | item = g_list_model_get_item (list: G_LIST_MODEL (ptr: self->model), position); |
1018 | selected = gtk_selection_model_is_selected (model: self->model, position); |
1019 | gtk_list_item_widget_update (GTK_LIST_ITEM_WIDGET (list_item), |
1020 | position, |
1021 | item, |
1022 | selected); |
1023 | gtk_widget_insert_after (widget: list_item, parent: _gtk_widget_get_parent (widget: list_item), previous_sibling: prev_sibling); |
1024 | g_object_unref (object: item); |
1025 | } |
1026 | |
1027 | /** |
1028 | * gtk_list_item_manager_update_list_item: |
1029 | * @self: a `GtkListItemManager` |
1030 | * @item: a `GtkListItem` that has been acquired |
1031 | * @position: the new position of that list item |
1032 | * |
1033 | * Updates the position of the given @item. This function must be called whenever |
1034 | * the position of an item changes, like when new items are added before it. |
1035 | **/ |
1036 | static void |
1037 | gtk_list_item_manager_update_list_item (GtkListItemManager *self, |
1038 | GtkWidget *item, |
1039 | guint position) |
1040 | { |
1041 | GtkListItemWidget *list_item = GTK_LIST_ITEM_WIDGET (item); |
1042 | gboolean selected; |
1043 | |
1044 | g_return_if_fail (GTK_IS_LIST_ITEM_MANAGER (self)); |
1045 | g_return_if_fail (GTK_IS_LIST_ITEM_WIDGET (item)); |
1046 | |
1047 | selected = gtk_selection_model_is_selected (model: self->model, position); |
1048 | gtk_list_item_widget_update (self: list_item, |
1049 | position, |
1050 | item: gtk_list_item_widget_get_item (self: list_item), |
1051 | selected); |
1052 | } |
1053 | |
1054 | /* |
1055 | * gtk_list_item_manager_release_list_item: |
1056 | * @self: a `GtkListItemManager` |
1057 | * @change: (nullable): The change associated with this release or |
1058 | * %NULL if this is a final removal |
1059 | * @item: an item previously acquired with |
1060 | * gtk_list_item_manager_acquire_list_item() |
1061 | * |
1062 | * Releases an item that was previously acquired via |
1063 | * gtk_list_item_manager_acquire_list_item() and is no longer in use. |
1064 | **/ |
1065 | static void |
1066 | gtk_list_item_manager_release_list_item (GtkListItemManager *self, |
1067 | GHashTable *change, |
1068 | GtkWidget *item) |
1069 | { |
1070 | g_return_if_fail (GTK_IS_LIST_ITEM_MANAGER (self)); |
1071 | g_return_if_fail (GTK_IS_LIST_ITEM_WIDGET (item)); |
1072 | |
1073 | if (change != NULL) |
1074 | { |
1075 | if (!g_hash_table_replace (hash_table: change, key: gtk_list_item_widget_get_item (GTK_LIST_ITEM_WIDGET (item)), value: item)) |
1076 | { |
1077 | g_warning ("FIXME: Handle the same item multiple times in the list.\nLars says this totally should not happen, but here we are." ); |
1078 | } |
1079 | |
1080 | return; |
1081 | } |
1082 | |
1083 | gtk_widget_unparent (widget: item); |
1084 | } |
1085 | |
1086 | void |
1087 | gtk_list_item_manager_set_single_click_activate (GtkListItemManager *self, |
1088 | gboolean single_click_activate) |
1089 | { |
1090 | GtkListItemManagerItem *item; |
1091 | |
1092 | g_return_if_fail (GTK_IS_LIST_ITEM_MANAGER (self)); |
1093 | |
1094 | self->single_click_activate = single_click_activate; |
1095 | |
1096 | for (item = gtk_rb_tree_get_first (tree: self->items); |
1097 | item != NULL; |
1098 | item = gtk_rb_tree_node_get_next (node: item)) |
1099 | { |
1100 | if (item->widget) |
1101 | gtk_list_item_widget_set_single_click_activate (GTK_LIST_ITEM_WIDGET (item->widget), single_click_activate); |
1102 | } |
1103 | } |
1104 | |
1105 | gboolean |
1106 | gtk_list_item_manager_get_single_click_activate (GtkListItemManager *self) |
1107 | { |
1108 | g_return_val_if_fail (GTK_IS_LIST_ITEM_MANAGER (self), FALSE); |
1109 | |
1110 | return self->single_click_activate; |
1111 | } |
1112 | |
1113 | GtkListItemTracker * |
1114 | gtk_list_item_tracker_new (GtkListItemManager *self) |
1115 | { |
1116 | GtkListItemTracker *tracker; |
1117 | |
1118 | g_return_val_if_fail (GTK_IS_LIST_ITEM_MANAGER (self), NULL); |
1119 | |
1120 | tracker = g_slice_new0 (GtkListItemTracker); |
1121 | |
1122 | tracker->position = GTK_INVALID_LIST_POSITION; |
1123 | |
1124 | self->trackers = g_slist_prepend (list: self->trackers, data: tracker); |
1125 | |
1126 | return tracker; |
1127 | } |
1128 | |
1129 | void |
1130 | gtk_list_item_tracker_free (GtkListItemManager *self, |
1131 | GtkListItemTracker *tracker) |
1132 | { |
1133 | gtk_list_item_tracker_unset_position (self, tracker); |
1134 | |
1135 | self->trackers = g_slist_remove (list: self->trackers, data: tracker); |
1136 | |
1137 | g_slice_free (GtkListItemTracker, tracker); |
1138 | |
1139 | gtk_list_item_manager_ensure_items (self, NULL, G_MAXUINT); |
1140 | |
1141 | gtk_widget_queue_resize (widget: self->widget); |
1142 | } |
1143 | |
1144 | void |
1145 | gtk_list_item_tracker_set_position (GtkListItemManager *self, |
1146 | GtkListItemTracker *tracker, |
1147 | guint position, |
1148 | guint n_before, |
1149 | guint n_after) |
1150 | { |
1151 | GtkListItemManagerItem *item; |
1152 | guint n_items; |
1153 | |
1154 | gtk_list_item_tracker_unset_position (self, tracker); |
1155 | |
1156 | if (self->model == NULL) |
1157 | return; |
1158 | |
1159 | n_items = g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self->model)); |
1160 | if (position >= n_items) |
1161 | position = n_items - 1; /* for n_items == 0 this underflows to GTK_INVALID_LIST_POSITION */ |
1162 | |
1163 | tracker->position = position; |
1164 | tracker->n_before = n_before; |
1165 | tracker->n_after = n_after; |
1166 | |
1167 | gtk_list_item_manager_ensure_items (self, NULL, G_MAXUINT); |
1168 | |
1169 | item = gtk_list_item_manager_get_nth (self, position, NULL); |
1170 | if (item) |
1171 | tracker->widget = GTK_LIST_ITEM_WIDGET (item->widget); |
1172 | |
1173 | gtk_widget_queue_resize (widget: self->widget); |
1174 | } |
1175 | |
1176 | guint |
1177 | gtk_list_item_tracker_get_position (GtkListItemManager *self, |
1178 | GtkListItemTracker *tracker) |
1179 | { |
1180 | return tracker->position; |
1181 | } |
1182 | |