1 | /* |
2 | * Copyright © 2019 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 "gtktreeexpander.h" |
23 | |
24 | #include "gtkaccessible.h" |
25 | #include "gtkboxlayout.h" |
26 | #include "gtkbuiltiniconprivate.h" |
27 | #include "gtkdropcontrollermotion.h" |
28 | #include "gtkenums.h" |
29 | #include "gtkgestureclick.h" |
30 | #include "gtkintl.h" |
31 | #include "gtktreelistmodel.h" |
32 | #include "gtkprivate.h" |
33 | |
34 | /** |
35 | * GtkTreeExpander: |
36 | * |
37 | * `GtkTreeExpander` is a widget that provides an expander for a list. |
38 | * |
39 | * It is typically placed as a bottommost child into a `GtkListView` |
40 | * to allow users to expand and collapse children in a list with a |
41 | * [class@Gtk.TreeListModel]. `GtkTreeExpander` provides the common UI |
42 | * elements, gestures and keybindings for this purpose. |
43 | * |
44 | * On top of this, the "listitem.expand", "listitem.collapse" and |
45 | * "listitem.toggle-expand" actions are provided to allow adding custom |
46 | * UI for managing expanded state. |
47 | * |
48 | * The `GtkTreeListModel` must be set to not be passthrough. Then it |
49 | * will provide [class@Gtk.TreeListRow] items which can be set via |
50 | * [method@Gtk.TreeExpander.set_list_row] on the expander. |
51 | * The expander will then watch that row item automatically. |
52 | * [method@Gtk.TreeExpander.set_child] sets the widget that displays |
53 | * the actual row contents. |
54 | * |
55 | * # CSS nodes |
56 | * |
57 | * ``` |
58 | * treeexpander |
59 | * ├── [indent]* |
60 | * ├── [expander] |
61 | * ╰── <child> |
62 | * ``` |
63 | * |
64 | * `GtkTreeExpander` has zero or one CSS nodes with the name "expander" that |
65 | * should display the expander icon. The node will be `:checked` when it |
66 | * is expanded. If the node is not expandable, an "indent" node will be |
67 | * displayed instead. |
68 | * |
69 | * For every level of depth, another "indent" node is prepended. |
70 | * |
71 | * # Accessibility |
72 | * |
73 | * `GtkTreeExpander` uses the %GTK_ACCESSIBLE_ROLE_GROUP role. The expander icon |
74 | * is represented as a %GTK_ACCESSIBLE_ROLE_BUTTON, labelled by the expander's |
75 | * child, and toggling it will change the %GTK_ACCESSIBLE_STATE_EXPANDED state. |
76 | */ |
77 | |
78 | struct _GtkTreeExpander |
79 | { |
80 | GtkWidget parent_instance; |
81 | |
82 | GtkTreeListRow *list_row; |
83 | GtkWidget *child; |
84 | |
85 | GtkWidget *expander_icon; |
86 | guint notify_handler; |
87 | |
88 | gboolean indent_for_icon; |
89 | |
90 | guint expand_timer; |
91 | }; |
92 | |
93 | enum |
94 | { |
95 | PROP_0, |
96 | PROP_CHILD, |
97 | PROP_ITEM, |
98 | PROP_LIST_ROW, |
99 | PROP_INDENT_FOR_ICON, |
100 | |
101 | N_PROPS |
102 | }; |
103 | |
104 | G_DEFINE_TYPE (GtkTreeExpander, gtk_tree_expander, GTK_TYPE_WIDGET) |
105 | |
106 | static GParamSpec *properties[N_PROPS] = { NULL, }; |
107 | |
108 | static void |
109 | gtk_tree_expander_click_gesture_pressed (GtkGestureClick *gesture, |
110 | int n_press, |
111 | double x, |
112 | double y, |
113 | gpointer unused) |
114 | { |
115 | GtkWidget *widget = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture)); |
116 | |
117 | gtk_widget_activate_action (widget, name: "listitem.toggle-expand" , NULL); |
118 | |
119 | gtk_widget_set_state_flags (widget, |
120 | flags: GTK_STATE_FLAG_ACTIVE, |
121 | FALSE); |
122 | |
123 | gtk_gesture_set_state (GTK_GESTURE (gesture), state: GTK_EVENT_SEQUENCE_CLAIMED); |
124 | } |
125 | |
126 | static void |
127 | gtk_tree_expander_click_gesture_released (GtkGestureClick *gesture, |
128 | int n_press, |
129 | double x, |
130 | double y, |
131 | gpointer unused) |
132 | { |
133 | gtk_widget_unset_state_flags (widget: gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture)), |
134 | flags: GTK_STATE_FLAG_ACTIVE); |
135 | |
136 | gtk_gesture_set_state (GTK_GESTURE (gesture), state: GTK_EVENT_SEQUENCE_CLAIMED); |
137 | } |
138 | |
139 | static void |
140 | gtk_tree_expander_click_gesture_canceled (GtkGestureClick *gesture, |
141 | GdkEventSequence *sequence, |
142 | gpointer unused) |
143 | { |
144 | gtk_widget_unset_state_flags (widget: gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (gesture)), |
145 | flags: GTK_STATE_FLAG_ACTIVE); |
146 | |
147 | gtk_gesture_set_state (GTK_GESTURE (gesture), state: GTK_EVENT_SEQUENCE_CLAIMED); |
148 | } |
149 | |
150 | static void |
151 | gtk_tree_expander_update_for_list_row (GtkTreeExpander *self) |
152 | { |
153 | if (self->list_row == NULL) |
154 | { |
155 | GtkWidget *child; |
156 | |
157 | for (child = gtk_widget_get_first_child (GTK_WIDGET (self)); |
158 | child != self->child; |
159 | child = gtk_widget_get_first_child (GTK_WIDGET (self))) |
160 | { |
161 | gtk_widget_unparent (widget: child); |
162 | } |
163 | self->expander_icon = NULL; |
164 | |
165 | gtk_accessible_reset_state (self: GTK_ACCESSIBLE (ptr: self), state: GTK_ACCESSIBLE_STATE_EXPANDED); |
166 | } |
167 | else |
168 | { |
169 | GtkWidget *child; |
170 | guint i, depth; |
171 | |
172 | depth = gtk_tree_list_row_get_depth (self: self->list_row); |
173 | if (gtk_tree_list_row_is_expandable (self: self->list_row)) |
174 | { |
175 | if (self->expander_icon == NULL) |
176 | { |
177 | GtkGesture *gesture; |
178 | |
179 | self->expander_icon = |
180 | g_object_new (GTK_TYPE_BUILTIN_ICON, |
181 | first_property_name: "css-name" , "expander" , |
182 | "accessible-role" , GTK_ACCESSIBLE_ROLE_BUTTON, |
183 | NULL); |
184 | |
185 | gesture = gtk_gesture_click_new (); |
186 | gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (gesture), |
187 | phase: GTK_PHASE_BUBBLE); |
188 | gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (gesture), |
189 | FALSE); |
190 | gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), |
191 | GDK_BUTTON_PRIMARY); |
192 | g_signal_connect (gesture, "pressed" , |
193 | G_CALLBACK (gtk_tree_expander_click_gesture_pressed), NULL); |
194 | g_signal_connect (gesture, "released" , |
195 | G_CALLBACK (gtk_tree_expander_click_gesture_released), NULL); |
196 | g_signal_connect (gesture, "cancel" , |
197 | G_CALLBACK (gtk_tree_expander_click_gesture_canceled), NULL); |
198 | gtk_widget_add_controller (widget: self->expander_icon, GTK_EVENT_CONTROLLER (gesture)); |
199 | |
200 | gtk_widget_insert_before (widget: self->expander_icon, |
201 | GTK_WIDGET (self), |
202 | next_sibling: self->child); |
203 | |
204 | gtk_accessible_update_property (self: GTK_ACCESSIBLE (ptr: self->expander_icon), |
205 | first_property: GTK_ACCESSIBLE_PROPERTY_LABEL, _("Expand" ), |
206 | -1); |
207 | } |
208 | |
209 | if (gtk_tree_list_row_get_expanded (self: self->list_row)) |
210 | { |
211 | gtk_widget_set_state_flags (widget: self->expander_icon, flags: GTK_STATE_FLAG_CHECKED, FALSE); |
212 | gtk_accessible_update_state (self: GTK_ACCESSIBLE (ptr: self), |
213 | first_state: GTK_ACCESSIBLE_STATE_EXPANDED, TRUE, |
214 | -1); |
215 | } |
216 | else |
217 | { |
218 | gtk_widget_unset_state_flags (widget: self->expander_icon, flags: GTK_STATE_FLAG_CHECKED); |
219 | gtk_accessible_update_state (self: GTK_ACCESSIBLE (ptr: self), |
220 | first_state: GTK_ACCESSIBLE_STATE_EXPANDED, FALSE, |
221 | -1); |
222 | } |
223 | |
224 | child = gtk_widget_get_prev_sibling (widget: self->expander_icon); |
225 | } |
226 | else |
227 | { |
228 | g_clear_pointer (&self->expander_icon, gtk_widget_unparent); |
229 | |
230 | if (self->indent_for_icon) |
231 | depth++; |
232 | |
233 | if (self->child) |
234 | child = gtk_widget_get_prev_sibling (widget: self->child); |
235 | else |
236 | child = gtk_widget_get_last_child (GTK_WIDGET (self)); |
237 | } |
238 | |
239 | for (i = 0; i < depth; i++) |
240 | { |
241 | if (child) |
242 | child = gtk_widget_get_prev_sibling (widget: child); |
243 | else |
244 | { |
245 | GtkWidget *indent = |
246 | g_object_new (GTK_TYPE_BUILTIN_ICON, |
247 | first_property_name: "css-name" , "indent" , |
248 | "accessible-role" , GTK_ACCESSIBLE_ROLE_PRESENTATION, |
249 | NULL); |
250 | |
251 | gtk_widget_insert_after (widget: indent, GTK_WIDGET (self), NULL); |
252 | } |
253 | } |
254 | |
255 | /* The level property is >= 1 */ |
256 | gtk_accessible_update_property (self: GTK_ACCESSIBLE (ptr: self), |
257 | first_property: GTK_ACCESSIBLE_PROPERTY_LEVEL, depth + 1, |
258 | -1); |
259 | |
260 | while (child) |
261 | { |
262 | GtkWidget *prev = gtk_widget_get_prev_sibling (widget: child); |
263 | gtk_widget_unparent (widget: child); |
264 | child = prev; |
265 | } |
266 | } |
267 | } |
268 | |
269 | static void |
270 | gtk_tree_expander_list_row_notify_cb (GtkTreeListRow *list_row, |
271 | GParamSpec *pspec, |
272 | GtkTreeExpander *self) |
273 | { |
274 | if (pspec->name == g_intern_static_string (string: "expanded" )) |
275 | { |
276 | if (self->expander_icon) |
277 | { |
278 | if (gtk_tree_list_row_get_expanded (self: list_row)) |
279 | { |
280 | gtk_widget_set_state_flags (widget: self->expander_icon, flags: GTK_STATE_FLAG_CHECKED, FALSE); |
281 | gtk_accessible_update_state (self: GTK_ACCESSIBLE (ptr: self->expander_icon), |
282 | first_state: GTK_ACCESSIBLE_STATE_EXPANDED, TRUE, |
283 | -1); |
284 | } |
285 | else |
286 | { |
287 | gtk_widget_unset_state_flags (widget: self->expander_icon, flags: GTK_STATE_FLAG_CHECKED); |
288 | gtk_accessible_update_state (self: GTK_ACCESSIBLE (ptr: self->expander_icon), |
289 | first_state: GTK_ACCESSIBLE_STATE_EXPANDED, FALSE, |
290 | -1); |
291 | } |
292 | } |
293 | } |
294 | else if (pspec->name == g_intern_static_string (string: "item" )) |
295 | { |
296 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_ITEM]); |
297 | } |
298 | else |
299 | { |
300 | /* can this happen other than when destroying the row? */ |
301 | gtk_tree_expander_update_for_list_row (self); |
302 | } |
303 | } |
304 | |
305 | static gboolean |
306 | gtk_tree_expander_focus (GtkWidget *widget, |
307 | GtkDirectionType direction) |
308 | { |
309 | GtkTreeExpander *self = GTK_TREE_EXPANDER (ptr: widget); |
310 | |
311 | /* The idea of this function is the following: |
312 | * 1. If any child can take focus, do not ever attempt |
313 | * to take focus. |
314 | * 2. Otherwise, if this item is selectable or activatable, |
315 | * allow focusing this widget. |
316 | * |
317 | * This makes sure every item in a list is focusable for |
318 | * activation and selection handling, but no useless widgets |
319 | * get focused and moving focus is as fast as possible. |
320 | */ |
321 | if (self->child) |
322 | { |
323 | if (gtk_widget_get_focus_child (widget)) |
324 | return FALSE; |
325 | if (gtk_widget_child_focus (widget: self->child, direction)) |
326 | return TRUE; |
327 | } |
328 | |
329 | if (gtk_widget_is_focus (widget)) |
330 | return FALSE; |
331 | |
332 | if (!gtk_widget_get_focusable (widget)) |
333 | return FALSE; |
334 | |
335 | gtk_widget_grab_focus (widget); |
336 | |
337 | return TRUE; |
338 | } |
339 | |
340 | static gboolean |
341 | gtk_tree_expander_grab_focus (GtkWidget *widget) |
342 | { |
343 | GtkTreeExpander *self = GTK_TREE_EXPANDER (ptr: widget); |
344 | |
345 | if (self->child && gtk_widget_grab_focus (widget: self->child)) |
346 | return TRUE; |
347 | |
348 | return GTK_WIDGET_CLASS (gtk_tree_expander_parent_class)->grab_focus (widget); |
349 | } |
350 | |
351 | static void |
352 | gtk_tree_expander_clear_list_row (GtkTreeExpander *self) |
353 | { |
354 | if (self->list_row == NULL) |
355 | return; |
356 | |
357 | g_signal_handler_disconnect (instance: self->list_row, handler_id: self->notify_handler); |
358 | self->notify_handler = 0; |
359 | g_clear_object (&self->list_row); |
360 | } |
361 | |
362 | static void |
363 | gtk_tree_expander_dispose (GObject *object) |
364 | { |
365 | GtkTreeExpander *self = GTK_TREE_EXPANDER (ptr: object); |
366 | |
367 | if (self->expand_timer) |
368 | { |
369 | g_source_remove (tag: self->expand_timer); |
370 | self->expand_timer = 0; |
371 | } |
372 | |
373 | gtk_tree_expander_clear_list_row (self); |
374 | gtk_tree_expander_update_for_list_row (self); |
375 | |
376 | g_clear_pointer (&self->child, gtk_widget_unparent); |
377 | |
378 | g_assert (self->expander_icon == NULL); |
379 | |
380 | G_OBJECT_CLASS (gtk_tree_expander_parent_class)->dispose (object); |
381 | } |
382 | |
383 | static void |
384 | gtk_tree_expander_get_property (GObject *object, |
385 | guint property_id, |
386 | GValue *value, |
387 | GParamSpec *pspec) |
388 | { |
389 | GtkTreeExpander *self = GTK_TREE_EXPANDER (ptr: object); |
390 | |
391 | switch (property_id) |
392 | { |
393 | case PROP_CHILD: |
394 | g_value_set_object (value, v_object: self->child); |
395 | break; |
396 | |
397 | case PROP_ITEM: |
398 | g_value_take_object (value, v_object: gtk_tree_expander_get_item (self)); |
399 | break; |
400 | |
401 | case PROP_LIST_ROW: |
402 | g_value_set_object (value, v_object: self->list_row); |
403 | break; |
404 | |
405 | case PROP_INDENT_FOR_ICON: |
406 | g_value_set_boolean (value, v_boolean: gtk_tree_expander_get_indent_for_icon (self)); |
407 | break; |
408 | |
409 | default: |
410 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); |
411 | break; |
412 | } |
413 | } |
414 | |
415 | static void |
416 | gtk_tree_expander_set_property (GObject *object, |
417 | guint property_id, |
418 | const GValue *value, |
419 | GParamSpec *pspec) |
420 | { |
421 | GtkTreeExpander *self = GTK_TREE_EXPANDER (ptr: object); |
422 | |
423 | switch (property_id) |
424 | { |
425 | case PROP_CHILD: |
426 | gtk_tree_expander_set_child (self, child: g_value_get_object (value)); |
427 | break; |
428 | |
429 | case PROP_LIST_ROW: |
430 | gtk_tree_expander_set_list_row (self, list_row: g_value_get_object (value)); |
431 | break; |
432 | |
433 | case PROP_INDENT_FOR_ICON: |
434 | gtk_tree_expander_set_indent_for_icon (self, indent_for_icon: g_value_get_boolean (value)); |
435 | break; |
436 | |
437 | default: |
438 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); |
439 | break; |
440 | } |
441 | } |
442 | |
443 | static void |
444 | gtk_tree_expander_expand (GtkWidget *widget, |
445 | const char *action_name, |
446 | GVariant *parameter) |
447 | { |
448 | GtkTreeExpander *self = GTK_TREE_EXPANDER (ptr: widget); |
449 | |
450 | if (self->list_row == NULL) |
451 | return; |
452 | |
453 | gtk_tree_list_row_set_expanded (self: self->list_row, TRUE); |
454 | } |
455 | |
456 | static void |
457 | gtk_tree_expander_collapse (GtkWidget *widget, |
458 | const char *action_name, |
459 | GVariant *parameter) |
460 | { |
461 | GtkTreeExpander *self = GTK_TREE_EXPANDER (ptr: widget); |
462 | |
463 | if (self->list_row == NULL) |
464 | return; |
465 | |
466 | gtk_tree_list_row_set_expanded (self: self->list_row, FALSE); |
467 | } |
468 | |
469 | static void |
470 | gtk_tree_expander_toggle_expand (GtkWidget *widget, |
471 | const char *action_name, |
472 | GVariant *parameter) |
473 | { |
474 | GtkTreeExpander *self = GTK_TREE_EXPANDER (ptr: widget); |
475 | |
476 | if (self->list_row == NULL) |
477 | return; |
478 | |
479 | gtk_tree_list_row_set_expanded (self: self->list_row, expanded: !gtk_tree_list_row_get_expanded (self: self->list_row)); |
480 | } |
481 | |
482 | static gboolean |
483 | expand_collapse_right (GtkWidget *widget, |
484 | GVariant *args, |
485 | gpointer unused) |
486 | { |
487 | GtkTreeExpander *self = GTK_TREE_EXPANDER (ptr: widget); |
488 | |
489 | if (self->list_row == NULL) |
490 | return FALSE; |
491 | |
492 | gtk_tree_list_row_set_expanded (self: self->list_row, expanded: gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL); |
493 | |
494 | return TRUE; |
495 | } |
496 | |
497 | static gboolean |
498 | expand_collapse_left (GtkWidget *widget, |
499 | GVariant *args, |
500 | gpointer unused) |
501 | { |
502 | GtkTreeExpander *self = GTK_TREE_EXPANDER (ptr: widget); |
503 | |
504 | if (self->list_row == NULL) |
505 | return FALSE; |
506 | |
507 | gtk_tree_list_row_set_expanded (self: self->list_row, expanded: gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL); |
508 | |
509 | return TRUE; |
510 | } |
511 | |
512 | static void |
513 | gtk_tree_expander_class_init (GtkTreeExpanderClass *klass) |
514 | { |
515 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); |
516 | GObjectClass *gobject_class = G_OBJECT_CLASS (klass); |
517 | |
518 | widget_class->focus = gtk_tree_expander_focus; |
519 | widget_class->grab_focus = gtk_tree_expander_grab_focus; |
520 | |
521 | gobject_class->dispose = gtk_tree_expander_dispose; |
522 | gobject_class->get_property = gtk_tree_expander_get_property; |
523 | gobject_class->set_property = gtk_tree_expander_set_property; |
524 | |
525 | /** |
526 | * GtkTreeExpander:child: (attributes org.gtk.Property.get=gtk_tree_expander_get_child org.gtk.Property.set=gtk_tree_expander_set_child) |
527 | * |
528 | * The child widget with the actual contents. |
529 | */ |
530 | properties[PROP_CHILD] = |
531 | g_param_spec_object (name: "child" , |
532 | P_("Child" ), |
533 | P_("The child widget with the actual contents" ), |
534 | GTK_TYPE_WIDGET, |
535 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); |
536 | |
537 | /** |
538 | * GtkTreeExpander:item: (attributes org.gtk.Property.get=gtk_tree_expander_get_item) |
539 | * |
540 | * The item held by this expander's row. |
541 | */ |
542 | properties[PROP_ITEM] = |
543 | g_param_spec_object (name: "item" , |
544 | P_("Item" ), |
545 | P_("The item held by this expander's row" ), |
546 | G_TYPE_OBJECT, |
547 | flags: G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); |
548 | |
549 | /** |
550 | * GtkTreeExpander:list-row: (attributes org.gtk.Property.get=gtk_tree_expander_get_list_row org.gtk.Property.set=gtk_tree_expander_set_list_row) |
551 | * |
552 | * The list row to track for expander state. |
553 | */ |
554 | properties[PROP_LIST_ROW] = |
555 | g_param_spec_object (name: "list-row" , |
556 | P_("List row" ), |
557 | P_("The list row to track for expander state" ), |
558 | GTK_TYPE_TREE_LIST_ROW, |
559 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); |
560 | |
561 | /** |
562 | * GtkTreeExpander:indent-for-icon: (attributes org.gtk.Property.get=gtk_tree_expander_get_indent_for_icon org.gtk.Property.set=gtk_tree_expander_set_indent_for_icon) |
563 | * |
564 | * TreeExpander indents the child by the width of an expander-icon if it is not expandable. |
565 | * |
566 | * Since: 4.6 |
567 | */ |
568 | properties[PROP_INDENT_FOR_ICON] = |
569 | g_param_spec_boolean (name: "indent-for-icon" , |
570 | P_ ("Indent without expander" ), |
571 | P_ ("If the TreeExpander should indent the child if no expander-icon is shown" ), |
572 | TRUE, |
573 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); |
574 | |
575 | g_object_class_install_properties (oclass: gobject_class, n_pspecs: N_PROPS, pspecs: properties); |
576 | |
577 | /** |
578 | * GtkTreeExpander|listitem.expand: |
579 | * |
580 | * Expands the expander if it can be expanded. |
581 | */ |
582 | gtk_widget_class_install_action (widget_class, |
583 | action_name: "listitem.expand" , |
584 | NULL, |
585 | activate: gtk_tree_expander_expand); |
586 | |
587 | /** |
588 | * GtkTreeExpander|listitem.collapse: |
589 | * |
590 | * Collapses the expander. |
591 | */ |
592 | gtk_widget_class_install_action (widget_class, |
593 | action_name: "listitem.collapse" , |
594 | NULL, |
595 | activate: gtk_tree_expander_collapse); |
596 | |
597 | /** |
598 | * GtkTreeExpander|listitem.toggle-expand: |
599 | * |
600 | * Tries to expand the expander if it was collapsed or collapses it if |
601 | * it was expanded. |
602 | */ |
603 | gtk_widget_class_install_action (widget_class, |
604 | action_name: "listitem.toggle-expand" , |
605 | NULL, |
606 | activate: gtk_tree_expander_toggle_expand); |
607 | |
608 | gtk_widget_class_add_binding_action (widget_class, GDK_KEY_plus, mods: 0, |
609 | action_name: "listitem.expand" , NULL); |
610 | gtk_widget_class_add_binding_action (widget_class, GDK_KEY_KP_Add, mods: 0, |
611 | action_name: "listitem.expand" , NULL); |
612 | gtk_widget_class_add_binding_action (widget_class, GDK_KEY_asterisk, mods: 0, |
613 | action_name: "listitem.expand" , NULL); |
614 | gtk_widget_class_add_binding_action (widget_class, GDK_KEY_KP_Multiply, mods: 0, |
615 | action_name: "listitem.expand" , NULL); |
616 | gtk_widget_class_add_binding_action (widget_class, GDK_KEY_minus, mods: 0, |
617 | action_name: "listitem.collapse" , NULL); |
618 | gtk_widget_class_add_binding_action (widget_class, GDK_KEY_KP_Subtract, mods: 0, |
619 | action_name: "listitem.collapse" , NULL); |
620 | gtk_widget_class_add_binding_action (widget_class, GDK_KEY_slash, mods: 0, |
621 | action_name: "listitem.collapse" , NULL); |
622 | gtk_widget_class_add_binding_action (widget_class, GDK_KEY_KP_Divide, mods: 0, |
623 | action_name: "listitem.collapse" , NULL); |
624 | |
625 | gtk_widget_class_add_binding (widget_class, GDK_KEY_Right, mods: GDK_SHIFT_MASK, |
626 | callback: expand_collapse_right, NULL); |
627 | gtk_widget_class_add_binding (widget_class, GDK_KEY_KP_Right, mods: GDK_SHIFT_MASK, |
628 | callback: expand_collapse_right, NULL); |
629 | gtk_widget_class_add_binding (widget_class, GDK_KEY_Right, mods: GDK_CONTROL_MASK | GDK_SHIFT_MASK, |
630 | callback: expand_collapse_right, NULL); |
631 | gtk_widget_class_add_binding (widget_class, GDK_KEY_KP_Right, mods: GDK_CONTROL_MASK | GDK_SHIFT_MASK, |
632 | callback: expand_collapse_right, NULL); |
633 | gtk_widget_class_add_binding (widget_class, GDK_KEY_Left, mods: GDK_SHIFT_MASK, |
634 | callback: expand_collapse_left, NULL); |
635 | gtk_widget_class_add_binding (widget_class, GDK_KEY_KP_Left, mods: GDK_SHIFT_MASK, |
636 | callback: expand_collapse_left, NULL); |
637 | gtk_widget_class_add_binding (widget_class, GDK_KEY_Left, mods: GDK_CONTROL_MASK | GDK_SHIFT_MASK, |
638 | callback: expand_collapse_left, NULL); |
639 | gtk_widget_class_add_binding (widget_class, GDK_KEY_KP_Left, mods: GDK_CONTROL_MASK | GDK_SHIFT_MASK, |
640 | callback: expand_collapse_left, NULL); |
641 | |
642 | gtk_widget_class_add_binding_action (widget_class, GDK_KEY_space, mods: GDK_CONTROL_MASK, |
643 | action_name: "listitem.toggle-expand" , NULL); |
644 | gtk_widget_class_add_binding_action (widget_class, GDK_KEY_KP_Space, mods: GDK_CONTROL_MASK, |
645 | action_name: "listitem.toggle-expand" , NULL); |
646 | |
647 | #if 0 |
648 | /* These can't be implements yet. */ |
649 | gtk_widget_class_add_binding (widget_class, GDK_KEY_BackSpace, 0, go_to_parent_row, NULL, NULL); |
650 | gtk_widget_class_add_binding (widget_class, GDK_KEY_BackSpace, GDK_CONTROL_MASK, go_to_parent_row, NULL, NULL); |
651 | #endif |
652 | |
653 | gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BOX_LAYOUT); |
654 | gtk_widget_class_set_css_name (widget_class, I_("treeexpander" )); |
655 | gtk_widget_class_set_accessible_role (widget_class, accessible_role: GTK_ACCESSIBLE_ROLE_GROUP); |
656 | } |
657 | |
658 | static gboolean |
659 | gtk_tree_expander_expand_timeout (gpointer data) |
660 | { |
661 | GtkTreeExpander *self = GTK_TREE_EXPANDER (ptr: data); |
662 | |
663 | if (self->list_row != NULL) |
664 | gtk_tree_list_row_set_expanded (self: self->list_row, TRUE); |
665 | |
666 | self->expand_timer = 0; |
667 | |
668 | return G_SOURCE_REMOVE; |
669 | } |
670 | |
671 | #define TIMEOUT_EXPAND 500 |
672 | |
673 | static void |
674 | gtk_tree_expander_drag_enter (GtkDropControllerMotion *motion, |
675 | double x, |
676 | double y, |
677 | GtkTreeExpander *self) |
678 | { |
679 | if (self->list_row == NULL) |
680 | return; |
681 | |
682 | if (!gtk_tree_list_row_get_expanded (self: self->list_row) && |
683 | !self->expand_timer) |
684 | { |
685 | self->expand_timer = g_timeout_add (TIMEOUT_EXPAND, function: (GSourceFunc) gtk_tree_expander_expand_timeout, data: self); |
686 | gdk_source_set_static_name_by_id (tag: self->expand_timer, name: "[gtk] gtk_tree_expander_expand_timeout" ); |
687 | } |
688 | } |
689 | |
690 | static void |
691 | gtk_tree_expander_drag_leave (GtkDropControllerMotion *motion, |
692 | GtkTreeExpander *self) |
693 | { |
694 | if (self->expand_timer) |
695 | { |
696 | g_source_remove (tag: self->expand_timer); |
697 | self->expand_timer = 0; |
698 | } |
699 | } |
700 | |
701 | static void |
702 | gtk_tree_expander_init (GtkTreeExpander *self) |
703 | { |
704 | GtkEventController *controller; |
705 | |
706 | gtk_widget_set_focusable (GTK_WIDGET (self), TRUE); |
707 | self->indent_for_icon = TRUE; |
708 | |
709 | controller = gtk_drop_controller_motion_new (); |
710 | g_signal_connect (controller, "enter" , G_CALLBACK (gtk_tree_expander_drag_enter), self); |
711 | g_signal_connect (controller, "leave" , G_CALLBACK (gtk_tree_expander_drag_leave), self); |
712 | gtk_widget_add_controller (GTK_WIDGET (self), controller); |
713 | } |
714 | |
715 | /** |
716 | * gtk_tree_expander_new: |
717 | * |
718 | * Creates a new `GtkTreeExpander` |
719 | * |
720 | * Returns: a new `GtkTreeExpander` |
721 | **/ |
722 | GtkWidget * |
723 | gtk_tree_expander_new (void) |
724 | { |
725 | return g_object_new (GTK_TYPE_TREE_EXPANDER, |
726 | NULL); |
727 | } |
728 | |
729 | /** |
730 | * gtk_tree_expander_get_child: (attributes org.gtk.Method.get_property=child) |
731 | * @self: a `GtkTreeExpander` |
732 | * |
733 | * Gets the child widget displayed by @self. |
734 | * |
735 | * Returns: (nullable) (transfer none): The child displayed by @self |
736 | **/ |
737 | GtkWidget * |
738 | gtk_tree_expander_get_child (GtkTreeExpander *self) |
739 | { |
740 | g_return_val_if_fail (GTK_IS_TREE_EXPANDER (self), NULL); |
741 | |
742 | return self->child; |
743 | } |
744 | |
745 | /** |
746 | * gtk_tree_expander_set_child: (attributes org.gtk.Method.set_property=child) |
747 | * @self: a `GtkTreeExpander` |
748 | * @child: (nullable): a `GtkWidget` |
749 | * |
750 | * Sets the content widget to display. |
751 | */ |
752 | void |
753 | gtk_tree_expander_set_child (GtkTreeExpander *self, |
754 | GtkWidget *child) |
755 | { |
756 | g_return_if_fail (GTK_IS_TREE_EXPANDER (self)); |
757 | g_return_if_fail (child == NULL || GTK_IS_WIDGET (child)); |
758 | |
759 | if (self->child == child) |
760 | return; |
761 | |
762 | g_clear_pointer (&self->child, gtk_widget_unparent); |
763 | |
764 | if (child) |
765 | { |
766 | self->child = child; |
767 | gtk_widget_set_parent (widget: child, GTK_WIDGET (self)); |
768 | |
769 | gtk_accessible_update_relation (self: GTK_ACCESSIBLE (ptr: self), |
770 | first_relation: GTK_ACCESSIBLE_RELATION_LABELLED_BY, self->child, NULL, |
771 | -1); |
772 | } |
773 | else |
774 | { |
775 | gtk_accessible_reset_relation (self: GTK_ACCESSIBLE (ptr: self), relation: GTK_ACCESSIBLE_RELATION_LABELLED_BY); |
776 | } |
777 | |
778 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_CHILD]); |
779 | } |
780 | |
781 | /** |
782 | * gtk_tree_expander_get_item: (attributes org.gtk.Method.get_property=item) |
783 | * @self: a `GtkTreeExpander` |
784 | * |
785 | * Forwards the item set on the `GtkTreeListRow` that @self is managing. |
786 | * |
787 | * This call is essentially equivalent to calling: |
788 | * |
789 | * ```c |
790 | * gtk_tree_list_row_get_item (gtk_tree_expander_get_list_row (@self)); |
791 | * ``` |
792 | * |
793 | * Returns: (nullable) (transfer full) (type GObject): The item of the row |
794 | */ |
795 | gpointer |
796 | gtk_tree_expander_get_item (GtkTreeExpander *self) |
797 | { |
798 | g_return_val_if_fail (GTK_IS_TREE_EXPANDER (self), NULL); |
799 | |
800 | if (self->list_row == NULL) |
801 | return NULL; |
802 | |
803 | return gtk_tree_list_row_get_item (self: self->list_row); |
804 | } |
805 | |
806 | /** |
807 | * gtk_tree_expander_get_list_row: (attributes org.gtk.Method.get_property=list-row) |
808 | * @self: a `GtkTreeExpander` |
809 | * |
810 | * Gets the list row managed by @self. |
811 | * |
812 | * Returns: (nullable) (transfer none): The list row displayed by @self |
813 | */ |
814 | GtkTreeListRow * |
815 | gtk_tree_expander_get_list_row (GtkTreeExpander *self) |
816 | { |
817 | g_return_val_if_fail (GTK_IS_TREE_EXPANDER (self), NULL); |
818 | |
819 | return self->list_row; |
820 | } |
821 | |
822 | /** |
823 | * gtk_tree_expander_set_list_row: (attributes org.gtk.Method.set_property=list-row) |
824 | * @self: a `GtkTreeExpander` widget |
825 | * @list_row: (nullable): a `GtkTreeListRow` |
826 | * |
827 | * Sets the tree list row that this expander should manage. |
828 | */ |
829 | void |
830 | gtk_tree_expander_set_list_row (GtkTreeExpander *self, |
831 | GtkTreeListRow *list_row) |
832 | { |
833 | g_return_if_fail (GTK_IS_TREE_EXPANDER (self)); |
834 | g_return_if_fail (list_row == NULL || GTK_IS_TREE_LIST_ROW (list_row)); |
835 | |
836 | if (self->list_row == list_row) |
837 | return; |
838 | |
839 | g_object_freeze_notify (G_OBJECT (self)); |
840 | |
841 | gtk_tree_expander_clear_list_row (self); |
842 | |
843 | if (list_row) |
844 | { |
845 | self->list_row = g_object_ref (list_row); |
846 | self->notify_handler = g_signal_connect (list_row, |
847 | "notify" , |
848 | G_CALLBACK (gtk_tree_expander_list_row_notify_cb), |
849 | self); |
850 | } |
851 | |
852 | gtk_tree_expander_update_for_list_row (self); |
853 | |
854 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_LIST_ROW]); |
855 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_ITEM]); |
856 | |
857 | g_object_thaw_notify (G_OBJECT (self)); |
858 | } |
859 | |
860 | /** |
861 | * gtk_tree_expander_get_indent_for_icon: (attributes org.gtk.Method.get_property=indent-for-icon) |
862 | * @self: a `GtkTreeExpander` |
863 | * |
864 | * TreeExpander indents the child by the width of an expander-icon if it is not expandable. |
865 | * |
866 | * Returns: TRUE if the child should be indented when not expandable. Otherwise FALSE. |
867 | * |
868 | * Since: 4.6 |
869 | */ |
870 | gboolean |
871 | gtk_tree_expander_get_indent_for_icon (GtkTreeExpander *self) |
872 | { |
873 | g_return_val_if_fail (GTK_IS_TREE_EXPANDER (self), FALSE); |
874 | |
875 | return self->indent_for_icon; |
876 | } |
877 | |
878 | /** |
879 | * gtk_tree_expander_set_indent_for_icon: (attributes org.gtk.Method.set_property=indent-for-icon) |
880 | * @self: a `GtkTreeExpander` widget |
881 | * @indent_for_icon: TRUE if the child should be indented without expander. Otherwise FALSE. |
882 | * |
883 | * Sets if the TreeExpander should indent the child by the width of an expander-icon when it is not expandable. |
884 | * |
885 | * Since: 4.6 |
886 | */ |
887 | void |
888 | gtk_tree_expander_set_indent_for_icon (GtkTreeExpander *self, |
889 | gboolean indent_for_icon) |
890 | { |
891 | g_return_if_fail (GTK_IS_TREE_EXPANDER (self)); |
892 | |
893 | if (indent_for_icon == self->indent_for_icon) |
894 | return; |
895 | |
896 | self->indent_for_icon = indent_for_icon; |
897 | |
898 | gtk_tree_expander_update_for_list_row (self); |
899 | |
900 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_INDENT_FOR_ICON]); |
901 | } |
902 | |