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 "gtklistviewprivate.h" |
23 | |
24 | #include "gtkbitset.h" |
25 | #include "gtkintl.h" |
26 | #include "gtklistbaseprivate.h" |
27 | #include "gtklistitemmanagerprivate.h" |
28 | #include "gtkmain.h" |
29 | #include "gtkprivate.h" |
30 | #include "gtkrbtreeprivate.h" |
31 | #include "gtkwidgetprivate.h" |
32 | #include "gtkmultiselection.h" |
33 | |
34 | /* Maximum number of list items created by the listview. |
35 | * For debugging, you can set this to G_MAXUINT to ensure |
36 | * there's always a list item for every row. |
37 | */ |
38 | #define GTK_LIST_VIEW_MAX_LIST_ITEMS 200 |
39 | |
40 | /* Extra items to keep above + below every tracker */ |
41 | #define 2 |
42 | |
43 | /** |
44 | * GtkListView: |
45 | * |
46 | * `GtkListView` presents a large dynamic list of items. |
47 | * |
48 | * `GtkListView` uses its factory to generate one row widget for each visible |
49 | * item and shows them in a linear display, either vertically or horizontally. |
50 | * |
51 | * The [property@Gtk.ListView:show-separators] property offers a simple way to |
52 | * display separators between the rows. |
53 | * |
54 | * `GtkListView` allows the user to select items according to the selection |
55 | * characteristics of the model. For models that allow multiple selected items, |
56 | * it is possible to turn on _rubberband selection_, using |
57 | * [property@Gtk.ListView:enable-rubberband]. |
58 | * |
59 | * If you need multiple columns with headers, see [class@Gtk.ColumnView]. |
60 | * |
61 | * To learn more about the list widget framework, see the |
62 | * [overview](section-list-widget.html). |
63 | * |
64 | * An example of using `GtkListView`: |
65 | * ```c |
66 | * static void |
67 | * setup_listitem_cb (GtkListItemFactory *factory, |
68 | * GtkListItem *list_item) |
69 | * { |
70 | * GtkWidget *image; |
71 | * |
72 | * image = gtk_image_new (); |
73 | * gtk_image_set_icon_size (GTK_IMAGE (image), GTK_ICON_SIZE_LARGE); |
74 | * gtk_list_item_set_child (list_item, image); |
75 | * } |
76 | * |
77 | * static void |
78 | * bind_listitem_cb (GtkListItemFactory *factory, |
79 | * GtkListItem *list_item) |
80 | * { |
81 | * GtkWidget *image; |
82 | * GAppInfo *app_info; |
83 | * |
84 | * image = gtk_list_item_get_child (list_item); |
85 | * app_info = gtk_list_item_get_item (list_item); |
86 | * gtk_image_set_from_gicon (GTK_IMAGE (image), g_app_info_get_icon (app_info)); |
87 | * } |
88 | * |
89 | * static void |
90 | * activate_cb (GtkListView *list, |
91 | * guint position, |
92 | * gpointer unused) |
93 | * { |
94 | * GAppInfo *app_info; |
95 | * |
96 | * app_info = g_list_model_get_item (G_LIST_MODEL (gtk_list_view_get_model (list)), position); |
97 | * g_app_info_launch (app_info, NULL, NULL, NULL); |
98 | * g_object_unref (app_info); |
99 | * } |
100 | * |
101 | * ... |
102 | * |
103 | * model = create_application_list (); |
104 | * |
105 | * factory = gtk_signal_list_item_factory_new (); |
106 | * g_signal_connect (factory, "setup", G_CALLBACK (setup_listitem_cb), NULL); |
107 | * g_signal_connect (factory, "bind", G_CALLBACK (bind_listitem_cb), NULL); |
108 | * |
109 | * list = gtk_list_view_new (GTK_SELECTION_MODEL (gtk_single_selection_new (model)), factory); |
110 | * |
111 | * g_signal_connect (list, "activate", G_CALLBACK (activate_cb), NULL); |
112 | * |
113 | * gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), list); |
114 | * ``` |
115 | * |
116 | * # CSS nodes |
117 | * |
118 | * ``` |
119 | * listview[.separators][.rich-list][.navigation-sidebar][.data-table] |
120 | * ├── row[.activatable] |
121 | * │ |
122 | * ├── row[.activatable] |
123 | * │ |
124 | * ┊ |
125 | * ╰── [rubberband] |
126 | * ``` |
127 | * |
128 | * `GtkListView` uses a single CSS node named `listview`. It may carry the |
129 | * `.separators` style class, when [property@Gtk.ListView:show-separators] |
130 | * property is set. Each child widget uses a single CSS node named `row`. |
131 | * If the [property@Gtk.ListItem:activatable] property is set, the |
132 | * corresponding row will have the `.activatable` style class. For |
133 | * rubberband selection, a node with name `rubberband` is used. |
134 | * |
135 | * The main listview node may also carry style classes to select |
136 | * the style of [list presentation](ListContainers.html#list-styles): |
137 | * .rich-list, .navigation-sidebar or .data-table. |
138 | * |
139 | * # Accessibility |
140 | * |
141 | * `GtkListView` uses the %GTK_ACCESSIBLE_ROLE_LIST role, and the list |
142 | * items use the %GTK_ACCESSIBLE_ROLE_LIST_ITEM role. |
143 | */ |
144 | |
145 | typedef struct _ListRow ListRow; |
146 | typedef struct _ListRowAugment ListRowAugment; |
147 | |
148 | struct _ListRow |
149 | { |
150 | GtkListItemManagerItem parent; |
151 | guint height; /* per row */ |
152 | }; |
153 | |
154 | struct _ListRowAugment |
155 | { |
156 | GtkListItemManagerItemAugment parent; |
157 | guint height; /* total */ |
158 | }; |
159 | |
160 | enum |
161 | { |
162 | PROP_0, |
163 | PROP_FACTORY, |
164 | PROP_MODEL, |
165 | PROP_SHOW_SEPARATORS, |
166 | PROP_SINGLE_CLICK_ACTIVATE, |
167 | PROP_ENABLE_RUBBERBAND, |
168 | |
169 | N_PROPS |
170 | }; |
171 | |
172 | enum { |
173 | ACTIVATE, |
174 | LAST_SIGNAL |
175 | }; |
176 | |
177 | G_DEFINE_TYPE (GtkListView, gtk_list_view, GTK_TYPE_LIST_BASE) |
178 | |
179 | static GParamSpec *properties[N_PROPS] = { NULL, }; |
180 | static guint signals[LAST_SIGNAL] = { 0 }; |
181 | |
182 | static void G_GNUC_UNUSED |
183 | dump (GtkListView *self) |
184 | { |
185 | ListRow *row; |
186 | guint n_widgets, n_list_rows; |
187 | |
188 | n_widgets = 0; |
189 | n_list_rows = 0; |
190 | //g_print ("ANCHOR: %u - %u\n", self->anchor_start, self->anchor_end); |
191 | for (row = gtk_list_item_manager_get_first (self: self->item_manager); |
192 | row; |
193 | row = gtk_rb_tree_node_get_next (node: row)) |
194 | { |
195 | if (row->parent.widget) |
196 | n_widgets++; |
197 | n_list_rows++; |
198 | g_print (format: " %4u%s (%upx)\n" , row->parent.n_items, row->parent.widget ? " (widget)" : "" , row->height); |
199 | } |
200 | |
201 | g_print (format: " => %u widgets in %u list rows\n" , n_widgets, n_list_rows); |
202 | } |
203 | |
204 | static void |
205 | list_row_augment (GtkRbTree *tree, |
206 | gpointer node_augment, |
207 | gpointer node, |
208 | gpointer left, |
209 | gpointer right) |
210 | { |
211 | ListRow *row = node; |
212 | ListRowAugment *aug = node_augment; |
213 | |
214 | gtk_list_item_manager_augment_node (tree, node_augment, node, left, right); |
215 | |
216 | aug->height = row->height * row->parent.n_items; |
217 | |
218 | if (left) |
219 | { |
220 | ListRowAugment *left_aug = gtk_rb_tree_get_augment (tree, node: left); |
221 | |
222 | aug->height += left_aug->height; |
223 | } |
224 | |
225 | if (right) |
226 | { |
227 | ListRowAugment *right_aug = gtk_rb_tree_get_augment (tree, node: right); |
228 | |
229 | aug->height += right_aug->height; |
230 | } |
231 | } |
232 | |
233 | static ListRow * |
234 | gtk_list_view_get_row_at_y (GtkListView *self, |
235 | int y, |
236 | int *offset) |
237 | { |
238 | ListRow *row, *tmp; |
239 | |
240 | row = gtk_list_item_manager_get_root (self: self->item_manager); |
241 | |
242 | while (row) |
243 | { |
244 | tmp = gtk_rb_tree_node_get_left (node: row); |
245 | if (tmp) |
246 | { |
247 | ListRowAugment *aug = gtk_list_item_manager_get_item_augment (self: self->item_manager, item: tmp); |
248 | if (y < aug->height) |
249 | { |
250 | row = tmp; |
251 | continue; |
252 | } |
253 | y -= aug->height; |
254 | } |
255 | |
256 | if (y < row->height * row->parent.n_items) |
257 | break; |
258 | y -= row->height * row->parent.n_items; |
259 | |
260 | row = gtk_rb_tree_node_get_right (node: row); |
261 | } |
262 | |
263 | if (offset) |
264 | *offset = row ? y : 0; |
265 | |
266 | return row; |
267 | } |
268 | |
269 | static int |
270 | list_row_get_y (GtkListView *self, |
271 | ListRow *row) |
272 | { |
273 | ListRow *parent, *left; |
274 | int y; |
275 | |
276 | left = gtk_rb_tree_node_get_left (node: row); |
277 | if (left) |
278 | { |
279 | ListRowAugment *aug = gtk_list_item_manager_get_item_augment (self: self->item_manager, item: left); |
280 | y = aug->height; |
281 | } |
282 | else |
283 | y = 0; |
284 | |
285 | for (parent = gtk_rb_tree_node_get_parent (node: row); |
286 | parent != NULL; |
287 | parent = gtk_rb_tree_node_get_parent (node: row)) |
288 | { |
289 | left = gtk_rb_tree_node_get_left (node: parent); |
290 | |
291 | if (left != row) |
292 | { |
293 | if (left) |
294 | { |
295 | ListRowAugment *aug = gtk_list_item_manager_get_item_augment (self: self->item_manager, item: left); |
296 | y += aug->height; |
297 | } |
298 | y += parent->height * parent->parent.n_items; |
299 | } |
300 | |
301 | row = parent; |
302 | } |
303 | |
304 | return y ; |
305 | } |
306 | |
307 | static int |
308 | gtk_list_view_get_list_height (GtkListView *self) |
309 | { |
310 | ListRow *row; |
311 | ListRowAugment *aug; |
312 | |
313 | row = gtk_list_item_manager_get_root (self: self->item_manager); |
314 | if (row == NULL) |
315 | return 0; |
316 | |
317 | aug = gtk_list_item_manager_get_item_augment (self: self->item_manager, item: row); |
318 | return aug->height; |
319 | } |
320 | |
321 | static gboolean |
322 | gtk_list_view_get_allocation_along (GtkListBase *base, |
323 | guint pos, |
324 | int *offset, |
325 | int *size) |
326 | { |
327 | GtkListView *self = GTK_LIST_VIEW (base); |
328 | ListRow *row; |
329 | guint skip; |
330 | int y; |
331 | |
332 | row = gtk_list_item_manager_get_nth (self: self->item_manager, position: pos, offset: &skip); |
333 | if (row == NULL) |
334 | { |
335 | if (offset) |
336 | *offset = 0; |
337 | if (size) |
338 | *size = 0; |
339 | return FALSE; |
340 | } |
341 | |
342 | y = list_row_get_y (self, row); |
343 | y += skip * row->height; |
344 | |
345 | if (offset) |
346 | *offset = y; |
347 | if (size) |
348 | *size = row->height; |
349 | |
350 | return TRUE; |
351 | } |
352 | |
353 | static gboolean |
354 | gtk_list_view_get_allocation_across (GtkListBase *base, |
355 | guint pos, |
356 | int *offset, |
357 | int *size) |
358 | { |
359 | GtkListView *self = GTK_LIST_VIEW (base); |
360 | |
361 | if (offset) |
362 | *offset = 0; |
363 | if (size) |
364 | *size = self->list_width; |
365 | |
366 | return TRUE; |
367 | } |
368 | |
369 | static GtkBitset * |
370 | gtk_list_view_get_items_in_rect (GtkListBase *base, |
371 | const cairo_rectangle_int_t *rect) |
372 | { |
373 | GtkListView *self = GTK_LIST_VIEW (base); |
374 | guint first, last, n_items; |
375 | GtkBitset *result; |
376 | ListRow *row; |
377 | |
378 | result = gtk_bitset_new_empty (); |
379 | |
380 | if (rect->y >= gtk_list_view_get_list_height (self)) |
381 | return result; |
382 | |
383 | n_items = gtk_list_base_get_n_items (self: base); |
384 | if (n_items == 0) |
385 | return result; |
386 | |
387 | row = gtk_list_view_get_row_at_y (self, y: rect->y, NULL); |
388 | if (row) |
389 | first = gtk_list_item_manager_get_item_position (self: self->item_manager, item: row); |
390 | else |
391 | first = rect->y < 0 ? 0 : n_items - 1; |
392 | row = gtk_list_view_get_row_at_y (self, y: rect->y + rect->height, NULL); |
393 | if (row) |
394 | last = gtk_list_item_manager_get_item_position (self: self->item_manager, item: row); |
395 | else |
396 | last = rect->y + rect->height < 0 ? 0 : n_items - 1; |
397 | |
398 | gtk_bitset_add_range_closed (self: result, first, last); |
399 | return result; |
400 | } |
401 | |
402 | static guint |
403 | gtk_list_view_move_focus_along (GtkListBase *base, |
404 | guint pos, |
405 | int steps) |
406 | { |
407 | if (steps < 0) |
408 | return pos - MIN (pos, -steps); |
409 | else |
410 | { |
411 | pos += MIN (gtk_list_base_get_n_items (base) - pos - 1, steps); |
412 | } |
413 | |
414 | return pos; |
415 | } |
416 | |
417 | static gboolean |
418 | gtk_list_view_get_position_from_allocation (GtkListBase *base, |
419 | int across, |
420 | int along, |
421 | guint *pos, |
422 | cairo_rectangle_int_t *area) |
423 | { |
424 | GtkListView *self = GTK_LIST_VIEW (base); |
425 | ListRow *row; |
426 | int remaining; |
427 | |
428 | if (across >= self->list_width) |
429 | return FALSE; |
430 | |
431 | along = CLAMP (along, 0, gtk_list_view_get_list_height (self) - 1); |
432 | |
433 | row = gtk_list_view_get_row_at_y (self, y: along, offset: &remaining); |
434 | if (row == NULL) |
435 | return FALSE; |
436 | |
437 | *pos = gtk_list_item_manager_get_item_position (self: self->item_manager, item: row); |
438 | g_assert (remaining < row->height * row->parent.n_items); |
439 | *pos += remaining / row->height; |
440 | |
441 | if (area) |
442 | { |
443 | area->x = 0; |
444 | area->width = self->list_width; |
445 | area->y = along - remaining % row->height; |
446 | area->height = row->height; |
447 | } |
448 | |
449 | return TRUE; |
450 | } |
451 | |
452 | static guint |
453 | gtk_list_view_move_focus_across (GtkListBase *base, |
454 | guint pos, |
455 | int steps) |
456 | { |
457 | return pos; |
458 | } |
459 | |
460 | static int |
461 | compare_ints (gconstpointer first, |
462 | gconstpointer second) |
463 | { |
464 | return *(int *) first - *(int *) second; |
465 | } |
466 | |
467 | static guint |
468 | gtk_list_view_get_unknown_row_height (GtkListView *self, |
469 | GArray *heights) |
470 | { |
471 | g_return_val_if_fail (heights->len > 0, 0); |
472 | |
473 | /* return the median and hope rows are generally uniform with few outliers */ |
474 | g_array_sort (array: heights, compare_func: compare_ints); |
475 | |
476 | return g_array_index (heights, int, heights->len / 2); |
477 | } |
478 | |
479 | static void |
480 | gtk_list_view_measure_across (GtkWidget *widget, |
481 | GtkOrientation orientation, |
482 | int for_size, |
483 | int *minimum, |
484 | int *natural) |
485 | { |
486 | GtkListView *self = GTK_LIST_VIEW (widget); |
487 | ListRow *row; |
488 | int min, nat, child_min, child_nat; |
489 | /* XXX: Figure out how to split a given height into per-row heights. |
490 | * Good luck! */ |
491 | for_size = -1; |
492 | |
493 | min = 0; |
494 | nat = 0; |
495 | |
496 | for (row = gtk_list_item_manager_get_first (self: self->item_manager); |
497 | row != NULL; |
498 | row = gtk_rb_tree_node_get_next (node: row)) |
499 | { |
500 | /* ignore unavailable rows */ |
501 | if (row->parent.widget == NULL) |
502 | continue; |
503 | |
504 | gtk_widget_measure (widget: row->parent.widget, |
505 | orientation, for_size, |
506 | minimum: &child_min, natural: &child_nat, NULL, NULL); |
507 | min = MAX (min, child_min); |
508 | nat = MAX (nat, child_nat); |
509 | } |
510 | |
511 | *minimum = min; |
512 | *natural = nat; |
513 | } |
514 | |
515 | static void |
516 | gtk_list_view_measure_list (GtkWidget *widget, |
517 | GtkOrientation orientation, |
518 | int for_size, |
519 | int *minimum, |
520 | int *natural) |
521 | { |
522 | GtkListView *self = GTK_LIST_VIEW (widget); |
523 | ListRow *row; |
524 | int min, nat, child_min, child_nat; |
525 | GArray *min_heights, *nat_heights; |
526 | guint n_unknown; |
527 | |
528 | min_heights = g_array_new (FALSE, FALSE, element_size: sizeof (int)); |
529 | nat_heights = g_array_new (FALSE, FALSE, element_size: sizeof (int)); |
530 | n_unknown = 0; |
531 | min = 0; |
532 | nat = 0; |
533 | |
534 | for (row = gtk_list_item_manager_get_first (self: self->item_manager); |
535 | row != NULL; |
536 | row = gtk_rb_tree_node_get_next (node: row)) |
537 | { |
538 | if (row->parent.widget) |
539 | { |
540 | gtk_widget_measure (widget: row->parent.widget, |
541 | orientation, for_size, |
542 | minimum: &child_min, natural: &child_nat, NULL, NULL); |
543 | g_array_append_val (min_heights, child_min); |
544 | g_array_append_val (nat_heights, child_nat); |
545 | min += child_min; |
546 | nat += child_nat; |
547 | } |
548 | else |
549 | { |
550 | n_unknown += row->parent.n_items; |
551 | } |
552 | } |
553 | |
554 | if (n_unknown) |
555 | { |
556 | min += n_unknown * gtk_list_view_get_unknown_row_height (self, heights: min_heights); |
557 | nat += n_unknown * gtk_list_view_get_unknown_row_height (self, heights: nat_heights); |
558 | } |
559 | g_array_free (array: min_heights, TRUE); |
560 | g_array_free (array: nat_heights, TRUE); |
561 | |
562 | *minimum = min; |
563 | *natural = nat; |
564 | } |
565 | |
566 | static void |
567 | gtk_list_view_measure (GtkWidget *widget, |
568 | GtkOrientation orientation, |
569 | int for_size, |
570 | int *minimum, |
571 | int *natural, |
572 | int *minimum_baseline, |
573 | int *natural_baseline) |
574 | { |
575 | GtkListView *self = GTK_LIST_VIEW (widget); |
576 | |
577 | if (orientation == gtk_list_base_get_orientation (GTK_LIST_BASE (self))) |
578 | gtk_list_view_measure_list (widget, orientation, for_size, minimum, natural); |
579 | else |
580 | gtk_list_view_measure_across (widget, orientation, for_size, minimum, natural); |
581 | } |
582 | |
583 | static void |
584 | gtk_list_view_size_allocate (GtkWidget *widget, |
585 | int width, |
586 | int height, |
587 | int baseline) |
588 | { |
589 | GtkListView *self = GTK_LIST_VIEW (widget); |
590 | ListRow *row; |
591 | GArray *heights; |
592 | int min, nat, row_height; |
593 | int x, y; |
594 | GtkOrientation orientation, opposite_orientation; |
595 | GtkScrollablePolicy scroll_policy, opposite_scroll_policy; |
596 | |
597 | orientation = gtk_list_base_get_orientation (GTK_LIST_BASE (self)); |
598 | opposite_orientation = OPPOSITE_ORIENTATION (orientation); |
599 | scroll_policy = gtk_list_base_get_scroll_policy (GTK_LIST_BASE (self), orientation); |
600 | opposite_scroll_policy = gtk_list_base_get_scroll_policy (GTK_LIST_BASE (self), orientation: opposite_orientation); |
601 | |
602 | /* step 0: exit early if list is empty */ |
603 | if (gtk_list_item_manager_get_root (self: self->item_manager) == NULL) |
604 | return; |
605 | |
606 | /* step 1: determine width of the list */ |
607 | gtk_widget_measure (widget, orientation: opposite_orientation, |
608 | for_size: -1, |
609 | minimum: &min, natural: &nat, NULL, NULL); |
610 | self->list_width = orientation == GTK_ORIENTATION_VERTICAL ? width : height; |
611 | if (opposite_scroll_policy == GTK_SCROLL_MINIMUM) |
612 | self->list_width = MAX (min, self->list_width); |
613 | else |
614 | self->list_width = MAX (nat, self->list_width); |
615 | |
616 | /* step 2: determine height of known list items */ |
617 | heights = g_array_new (FALSE, FALSE, element_size: sizeof (int)); |
618 | |
619 | for (row = gtk_list_item_manager_get_first (self: self->item_manager); |
620 | row != NULL; |
621 | row = gtk_rb_tree_node_get_next (node: row)) |
622 | { |
623 | if (row->parent.widget == NULL) |
624 | continue; |
625 | |
626 | gtk_widget_measure (widget: row->parent.widget, orientation, |
627 | for_size: self->list_width, |
628 | minimum: &min, natural: &nat, NULL, NULL); |
629 | if (scroll_policy == GTK_SCROLL_MINIMUM) |
630 | row_height = min; |
631 | else |
632 | row_height = nat; |
633 | if (row->height != row_height) |
634 | { |
635 | row->height = row_height; |
636 | gtk_rb_tree_node_mark_dirty (node: row); |
637 | } |
638 | g_array_append_val (heights, row_height); |
639 | } |
640 | |
641 | /* step 3: determine height of unknown items */ |
642 | row_height = gtk_list_view_get_unknown_row_height (self, heights); |
643 | g_array_free (array: heights, TRUE); |
644 | |
645 | for (row = gtk_list_item_manager_get_first (self: self->item_manager); |
646 | row != NULL; |
647 | row = gtk_rb_tree_node_get_next (node: row)) |
648 | { |
649 | if (row->parent.widget) |
650 | continue; |
651 | |
652 | if (row->height != row_height) |
653 | { |
654 | row->height = row_height; |
655 | gtk_rb_tree_node_mark_dirty (node: row); |
656 | } |
657 | } |
658 | |
659 | /* step 3: update the adjustments */ |
660 | gtk_list_base_update_adjustments (GTK_LIST_BASE (self), |
661 | total_across: self->list_width, |
662 | total_along: gtk_list_view_get_list_height (self), |
663 | page_across: gtk_widget_get_size (widget, orientation: opposite_orientation), |
664 | page_along: gtk_widget_get_size (widget, orientation), |
665 | across: &x, along: &y); |
666 | x = -x; |
667 | y = -y; |
668 | |
669 | /* step 4: actually allocate the widgets */ |
670 | |
671 | for (row = gtk_list_item_manager_get_first (self: self->item_manager); |
672 | row != NULL; |
673 | row = gtk_rb_tree_node_get_next (node: row)) |
674 | { |
675 | if (row->parent.widget) |
676 | { |
677 | gtk_list_base_size_allocate_child (GTK_LIST_BASE (self), |
678 | child: row->parent.widget, |
679 | x, |
680 | y, |
681 | width: self->list_width, |
682 | height: row->height); |
683 | } |
684 | |
685 | y += row->height * row->parent.n_items; |
686 | } |
687 | |
688 | gtk_list_base_allocate_rubberband (GTK_LIST_BASE (self)); |
689 | } |
690 | |
691 | static void |
692 | gtk_list_view_dispose (GObject *object) |
693 | { |
694 | GtkListView *self = GTK_LIST_VIEW (object); |
695 | |
696 | self->item_manager = NULL; |
697 | |
698 | G_OBJECT_CLASS (gtk_list_view_parent_class)->dispose (object); |
699 | } |
700 | |
701 | static void |
702 | gtk_list_view_get_property (GObject *object, |
703 | guint property_id, |
704 | GValue *value, |
705 | GParamSpec *pspec) |
706 | { |
707 | GtkListView *self = GTK_LIST_VIEW (object); |
708 | |
709 | switch (property_id) |
710 | { |
711 | case PROP_FACTORY: |
712 | g_value_set_object (value, v_object: gtk_list_item_manager_get_factory (self: self->item_manager)); |
713 | break; |
714 | |
715 | case PROP_MODEL: |
716 | g_value_set_object (value, v_object: gtk_list_base_get_model (GTK_LIST_BASE (self))); |
717 | break; |
718 | |
719 | case PROP_SHOW_SEPARATORS: |
720 | g_value_set_boolean (value, v_boolean: self->show_separators); |
721 | break; |
722 | |
723 | case PROP_SINGLE_CLICK_ACTIVATE: |
724 | g_value_set_boolean (value, v_boolean: gtk_list_item_manager_get_single_click_activate (self: self->item_manager)); |
725 | break; |
726 | |
727 | case PROP_ENABLE_RUBBERBAND: |
728 | g_value_set_boolean (value, v_boolean: gtk_list_base_get_enable_rubberband (GTK_LIST_BASE (self))); |
729 | break; |
730 | |
731 | default: |
732 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); |
733 | break; |
734 | } |
735 | } |
736 | |
737 | static void |
738 | gtk_list_view_set_property (GObject *object, |
739 | guint property_id, |
740 | const GValue *value, |
741 | GParamSpec *pspec) |
742 | { |
743 | GtkListView *self = GTK_LIST_VIEW (object); |
744 | |
745 | switch (property_id) |
746 | { |
747 | case PROP_FACTORY: |
748 | gtk_list_view_set_factory (self, factory: g_value_get_object (value)); |
749 | break; |
750 | |
751 | case PROP_MODEL: |
752 | gtk_list_view_set_model (self, model: g_value_get_object (value)); |
753 | break; |
754 | |
755 | case PROP_SHOW_SEPARATORS: |
756 | gtk_list_view_set_show_separators (self, show_separators: g_value_get_boolean (value)); |
757 | break; |
758 | |
759 | case PROP_SINGLE_CLICK_ACTIVATE: |
760 | gtk_list_view_set_single_click_activate (self, single_click_activate: g_value_get_boolean (value)); |
761 | break; |
762 | |
763 | case PROP_ENABLE_RUBBERBAND: |
764 | gtk_list_view_set_enable_rubberband (self, enable_rubberband: g_value_get_boolean (value)); |
765 | break; |
766 | |
767 | default: |
768 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); |
769 | break; |
770 | } |
771 | } |
772 | |
773 | static void |
774 | gtk_list_view_activate_item (GtkWidget *widget, |
775 | const char *action_name, |
776 | GVariant *parameter) |
777 | { |
778 | GtkListView *self = GTK_LIST_VIEW (widget); |
779 | guint pos; |
780 | |
781 | if (!g_variant_check_format_string (value: parameter, format_string: "u" , FALSE)) |
782 | return; |
783 | |
784 | g_variant_get (value: parameter, format_string: "u" , &pos); |
785 | if (pos >= gtk_list_base_get_n_items (GTK_LIST_BASE (self))) |
786 | return; |
787 | |
788 | g_signal_emit (instance: widget, signal_id: signals[ACTIVATE], detail: 0, pos); |
789 | } |
790 | |
791 | static void |
792 | gtk_list_view_class_init (GtkListViewClass *klass) |
793 | { |
794 | GtkListBaseClass *list_base_class = GTK_LIST_BASE_CLASS (klass); |
795 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); |
796 | GObjectClass *gobject_class = G_OBJECT_CLASS (klass); |
797 | |
798 | list_base_class->list_item_name = "row" ; |
799 | list_base_class->list_item_role = GTK_ACCESSIBLE_ROLE_LIST_ITEM; |
800 | list_base_class->list_item_size = sizeof (ListRow); |
801 | list_base_class->list_item_augment_size = sizeof (ListRowAugment); |
802 | list_base_class->list_item_augment_func = list_row_augment; |
803 | list_base_class->get_allocation_along = gtk_list_view_get_allocation_along; |
804 | list_base_class->get_allocation_across = gtk_list_view_get_allocation_across; |
805 | list_base_class->get_items_in_rect = gtk_list_view_get_items_in_rect; |
806 | list_base_class->get_position_from_allocation = gtk_list_view_get_position_from_allocation; |
807 | list_base_class->move_focus_along = gtk_list_view_move_focus_along; |
808 | list_base_class->move_focus_across = gtk_list_view_move_focus_across; |
809 | |
810 | widget_class->measure = gtk_list_view_measure; |
811 | widget_class->size_allocate = gtk_list_view_size_allocate; |
812 | |
813 | gobject_class->dispose = gtk_list_view_dispose; |
814 | gobject_class->get_property = gtk_list_view_get_property; |
815 | gobject_class->set_property = gtk_list_view_set_property; |
816 | |
817 | /** |
818 | * GtkListView:factory: (attributes org.gtk.Property.get=gtk_list_view_get_factory org.gtk.Property.set=gtk_list_view_set_factory) |
819 | * |
820 | * Factory for populating list items. |
821 | */ |
822 | properties[PROP_FACTORY] = |
823 | g_param_spec_object (name: "factory" , |
824 | P_("Factory" ), |
825 | P_("Factory for populating list items" ), |
826 | GTK_TYPE_LIST_ITEM_FACTORY, |
827 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); |
828 | |
829 | /** |
830 | * GtkListView:model: (attributes org.gtk.Property.get=gtk_list_view_get_model org.gtk.Property.set=gtk_list_view_set_model) |
831 | * |
832 | * Model for the items displayed. |
833 | */ |
834 | properties[PROP_MODEL] = |
835 | g_param_spec_object (name: "model" , |
836 | P_("Model" ), |
837 | P_("Model for the items displayed" ), |
838 | GTK_TYPE_SELECTION_MODEL, |
839 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); |
840 | |
841 | /** |
842 | * GtkListView:show-separators: (attributes org.gtk.Property.get=gtk_list_view_get_show_separators org.gtk.Property.set=gtk_list_view_set_show_separators) |
843 | * |
844 | * Show separators between rows. |
845 | */ |
846 | properties[PROP_SHOW_SEPARATORS] = |
847 | g_param_spec_boolean (name: "show-separators" , |
848 | P_("Show separators" ), |
849 | P_("Show separators between rows" ), |
850 | FALSE, |
851 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); |
852 | |
853 | /** |
854 | * GtkListView:single-click-activate: (attributes org.gtk.Property.get=gtk_list_view_get_single_click_activate org.gtk.Property.set=gtk_list_view_set_single_click_activate) |
855 | * |
856 | * Activate rows on single click and select them on hover. |
857 | */ |
858 | properties[PROP_SINGLE_CLICK_ACTIVATE] = |
859 | g_param_spec_boolean (name: "single-click-activate" , |
860 | P_("Single click activate" ), |
861 | P_("Activate rows on single click" ), |
862 | FALSE, |
863 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); |
864 | |
865 | /** |
866 | * GtkListView:enable-rubberband: (attributes org.gtk.Property.get=gtk_list_view_get_enable_rubberband org.gtk.Property.set=gtk_list_view_set_enable_rubberband) |
867 | * |
868 | * Allow rubberband selection. |
869 | */ |
870 | properties[PROP_ENABLE_RUBBERBAND] = |
871 | g_param_spec_boolean (name: "enable-rubberband" , |
872 | P_("Enable rubberband selection" ), |
873 | P_("Allow selecting items by dragging with the mouse" ), |
874 | FALSE, |
875 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); |
876 | |
877 | g_object_class_install_properties (oclass: gobject_class, n_pspecs: N_PROPS, pspecs: properties); |
878 | |
879 | /** |
880 | * GtkListView::activate: |
881 | * @self: The `GtkListView` |
882 | * @position: position of item to activate |
883 | * |
884 | * Emitted when a row has been activated by the user, |
885 | * usually via activating the GtkListView|list.activate-item action. |
886 | * |
887 | * This allows for a convenient way to handle activation in a listview. |
888 | * See [method@Gtk.ListItem.set_activatable] for details on how to use |
889 | * this signal. |
890 | */ |
891 | signals[ACTIVATE] = |
892 | g_signal_new (I_("activate" ), |
893 | G_TYPE_FROM_CLASS (gobject_class), |
894 | signal_flags: G_SIGNAL_RUN_LAST, |
895 | class_offset: 0, |
896 | NULL, NULL, |
897 | c_marshaller: g_cclosure_marshal_VOID__UINT, |
898 | G_TYPE_NONE, n_params: 1, |
899 | G_TYPE_UINT); |
900 | g_signal_set_va_marshaller (signal_id: signals[ACTIVATE], |
901 | G_TYPE_FROM_CLASS (gobject_class), |
902 | va_marshaller: g_cclosure_marshal_VOID__UINTv); |
903 | |
904 | /** |
905 | * GtkListView|list.activate-item: |
906 | * @position: position of item to activate |
907 | * |
908 | * Activates the item given in @position by emitting the |
909 | * [signal@Gtk.ListView::activate] signal. |
910 | */ |
911 | gtk_widget_class_install_action (widget_class, |
912 | action_name: "list.activate-item" , |
913 | parameter_type: "u" , |
914 | activate: gtk_list_view_activate_item); |
915 | |
916 | gtk_widget_class_set_css_name (widget_class, I_("listview" )); |
917 | gtk_widget_class_set_accessible_role (widget_class, accessible_role: GTK_ACCESSIBLE_ROLE_LIST); |
918 | } |
919 | |
920 | static void |
921 | gtk_list_view_init (GtkListView *self) |
922 | { |
923 | self->item_manager = gtk_list_base_get_manager (GTK_LIST_BASE (self)); |
924 | |
925 | gtk_list_base_set_anchor_max_widgets (GTK_LIST_BASE (self), |
926 | GTK_LIST_VIEW_MAX_LIST_ITEMS, |
927 | GTK_LIST_VIEW_EXTRA_ITEMS); |
928 | |
929 | gtk_widget_add_css_class (GTK_WIDGET (self), css_class: "view" ); |
930 | } |
931 | |
932 | /** |
933 | * gtk_list_view_new: |
934 | * @model: (nullable) (transfer full): the model to use |
935 | * @factory: (nullable) (transfer full): The factory to populate items with |
936 | * |
937 | * Creates a new `GtkListView` that uses the given @factory for |
938 | * mapping items to widgets. |
939 | * |
940 | * The function takes ownership of the |
941 | * arguments, so you can write code like |
942 | * ```c |
943 | * list_view = gtk_list_view_new (create_model (), |
944 | * gtk_builder_list_item_factory_new_from_resource ("/resource.ui")); |
945 | * ``` |
946 | * |
947 | * Returns: a new `GtkListView` using the given @model and @factory |
948 | */ |
949 | GtkWidget * |
950 | gtk_list_view_new (GtkSelectionModel *model, |
951 | GtkListItemFactory *factory) |
952 | { |
953 | GtkWidget *result; |
954 | |
955 | g_return_val_if_fail (model == NULL || GTK_IS_SELECTION_MODEL (model), NULL); |
956 | g_return_val_if_fail (factory == NULL || GTK_IS_LIST_ITEM_FACTORY (factory), NULL); |
957 | |
958 | result = g_object_new (GTK_TYPE_LIST_VIEW, |
959 | first_property_name: "model" , model, |
960 | "factory" , factory, |
961 | NULL); |
962 | |
963 | /* consume the references */ |
964 | g_clear_object (&model); |
965 | g_clear_object (&factory); |
966 | |
967 | return result; |
968 | } |
969 | |
970 | /** |
971 | * gtk_list_view_get_model: (attributes org.gtk.Method.get_property=model) |
972 | * @self: a `GtkListView` |
973 | * |
974 | * Gets the model that's currently used to read the items displayed. |
975 | * |
976 | * Returns: (nullable) (transfer none): The model in use |
977 | */ |
978 | GtkSelectionModel * |
979 | gtk_list_view_get_model (GtkListView *self) |
980 | { |
981 | g_return_val_if_fail (GTK_IS_LIST_VIEW (self), NULL); |
982 | |
983 | return gtk_list_base_get_model (GTK_LIST_BASE (self)); |
984 | } |
985 | |
986 | /** |
987 | * gtk_list_view_set_model: (attributes org.gtk.Method.set_property=model) |
988 | * @self: a `GtkListView` |
989 | * @model: (nullable) (transfer none): the model to use |
990 | * |
991 | * Sets the model to use. |
992 | * |
993 | * This must be a [iface@Gtk.SelectionModel] to use. |
994 | */ |
995 | void |
996 | gtk_list_view_set_model (GtkListView *self, |
997 | GtkSelectionModel *model) |
998 | { |
999 | g_return_if_fail (GTK_IS_LIST_VIEW (self)); |
1000 | g_return_if_fail (model == NULL || GTK_IS_SELECTION_MODEL (model)); |
1001 | |
1002 | if (!gtk_list_base_set_model (GTK_LIST_BASE (self), model)) |
1003 | return; |
1004 | |
1005 | gtk_accessible_update_property (self: GTK_ACCESSIBLE (ptr: self), |
1006 | first_property: GTK_ACCESSIBLE_PROPERTY_MULTI_SELECTABLE, GTK_IS_MULTI_SELECTION (ptr: model), |
1007 | -1); |
1008 | |
1009 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_MODEL]); |
1010 | } |
1011 | |
1012 | /** |
1013 | * gtk_list_view_get_factory: (attributes org.gtk.Method.get_property=factory) |
1014 | * @self: a `GtkListView` |
1015 | * |
1016 | * Gets the factory that's currently used to populate list items. |
1017 | * |
1018 | * Returns: (nullable) (transfer none): The factory in use |
1019 | */ |
1020 | GtkListItemFactory * |
1021 | gtk_list_view_get_factory (GtkListView *self) |
1022 | { |
1023 | g_return_val_if_fail (GTK_IS_LIST_VIEW (self), NULL); |
1024 | |
1025 | return gtk_list_item_manager_get_factory (self: self->item_manager); |
1026 | } |
1027 | |
1028 | /** |
1029 | * gtk_list_view_set_factory: (attributes org.gtk.Method.set_property=factory) |
1030 | * @self: a `GtkListView` |
1031 | * @factory: (nullable) (transfer none): the factory to use |
1032 | * |
1033 | * Sets the `GtkListItemFactory` to use for populating list items. |
1034 | */ |
1035 | void |
1036 | gtk_list_view_set_factory (GtkListView *self, |
1037 | GtkListItemFactory *factory) |
1038 | { |
1039 | g_return_if_fail (GTK_IS_LIST_VIEW (self)); |
1040 | g_return_if_fail (factory == NULL || GTK_IS_LIST_ITEM_FACTORY (factory)); |
1041 | |
1042 | if (factory == gtk_list_item_manager_get_factory (self: self->item_manager)) |
1043 | return; |
1044 | |
1045 | gtk_list_item_manager_set_factory (self: self->item_manager, factory); |
1046 | |
1047 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_FACTORY]); |
1048 | } |
1049 | |
1050 | /** |
1051 | * gtk_list_view_set_show_separators: (attributes org.gtk.Method.set_property=show-separators) |
1052 | * @self: a `GtkListView` |
1053 | * @show_separators: %TRUE to show separators |
1054 | * |
1055 | * Sets whether the list box should show separators |
1056 | * between rows. |
1057 | */ |
1058 | void |
1059 | gtk_list_view_set_show_separators (GtkListView *self, |
1060 | gboolean show_separators) |
1061 | { |
1062 | g_return_if_fail (GTK_IS_LIST_VIEW (self)); |
1063 | |
1064 | if (self->show_separators == show_separators) |
1065 | return; |
1066 | |
1067 | self->show_separators = show_separators; |
1068 | |
1069 | if (show_separators) |
1070 | gtk_widget_add_css_class (GTK_WIDGET (self), css_class: "separators" ); |
1071 | else |
1072 | gtk_widget_remove_css_class (GTK_WIDGET (self), css_class: "separators" ); |
1073 | |
1074 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_SHOW_SEPARATORS]); |
1075 | } |
1076 | |
1077 | /** |
1078 | * gtk_list_view_get_show_separators: (attributes org.gtk.Method.get_property=show-separators) |
1079 | * @self: a `GtkListView` |
1080 | * |
1081 | * Returns whether the list box should show separators |
1082 | * between rows. |
1083 | * |
1084 | * Returns: %TRUE if the list box shows separators |
1085 | */ |
1086 | gboolean |
1087 | gtk_list_view_get_show_separators (GtkListView *self) |
1088 | { |
1089 | g_return_val_if_fail (GTK_IS_LIST_VIEW (self), FALSE); |
1090 | |
1091 | return self->show_separators; |
1092 | } |
1093 | |
1094 | /** |
1095 | * gtk_list_view_set_single_click_activate: (attributes org.gtk.Method.set_property=single-click-activate) |
1096 | * @self: a `GtkListView` |
1097 | * @single_click_activate: %TRUE to activate items on single click |
1098 | * |
1099 | * Sets whether rows should be activated on single click and |
1100 | * selected on hover. |
1101 | */ |
1102 | void |
1103 | gtk_list_view_set_single_click_activate (GtkListView *self, |
1104 | gboolean single_click_activate) |
1105 | { |
1106 | g_return_if_fail (GTK_IS_LIST_VIEW (self)); |
1107 | |
1108 | if (single_click_activate == gtk_list_item_manager_get_single_click_activate (self: self->item_manager)) |
1109 | return; |
1110 | |
1111 | gtk_list_item_manager_set_single_click_activate (self: self->item_manager, single_click_activate); |
1112 | |
1113 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_SINGLE_CLICK_ACTIVATE]); |
1114 | } |
1115 | |
1116 | /** |
1117 | * gtk_list_view_get_single_click_activate: (attributes org.gtk.Method.set_property=single-click-activate) |
1118 | * @self: a `GtkListView` |
1119 | * |
1120 | * Returns whether rows will be activated on single click and |
1121 | * selected on hover. |
1122 | * |
1123 | * Returns: %TRUE if rows are activated on single click |
1124 | */ |
1125 | gboolean |
1126 | gtk_list_view_get_single_click_activate (GtkListView *self) |
1127 | { |
1128 | g_return_val_if_fail (GTK_IS_LIST_VIEW (self), FALSE); |
1129 | |
1130 | return gtk_list_item_manager_get_single_click_activate (self: self->item_manager); |
1131 | } |
1132 | |
1133 | /** |
1134 | * gtk_list_view_set_enable_rubberband: (attributes org.gtk.Method.set_property=enable-rubberband) |
1135 | * @self: a `GtkListView` |
1136 | * @enable_rubberband: %TRUE to enable rubberband selection |
1137 | * |
1138 | * Sets whether selections can be changed by dragging with the mouse. |
1139 | */ |
1140 | void |
1141 | gtk_list_view_set_enable_rubberband (GtkListView *self, |
1142 | gboolean enable_rubberband) |
1143 | { |
1144 | g_return_if_fail (GTK_IS_LIST_VIEW (self)); |
1145 | |
1146 | if (enable_rubberband == gtk_list_base_get_enable_rubberband (GTK_LIST_BASE (self))) |
1147 | return; |
1148 | |
1149 | gtk_list_base_set_enable_rubberband (GTK_LIST_BASE (self), enable: enable_rubberband); |
1150 | |
1151 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_ENABLE_RUBBERBAND]); |
1152 | } |
1153 | |
1154 | /** |
1155 | * gtk_list_view_get_enable_rubberband: (attributes org.gtk.Method.get_property=enable-rubberband) |
1156 | * @self: a `GtkListView` |
1157 | * |
1158 | * Returns whether rows can be selected by dragging with the mouse. |
1159 | * |
1160 | * Returns: %TRUE if rubberband selection is enabled |
1161 | */ |
1162 | gboolean |
1163 | gtk_list_view_get_enable_rubberband (GtkListView *self) |
1164 | { |
1165 | g_return_val_if_fail (GTK_IS_LIST_VIEW (self), FALSE); |
1166 | |
1167 | return gtk_list_base_get_enable_rubberband (GTK_LIST_BASE (self)); |
1168 | } |
1169 | |