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
78struct _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
93enum
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
104G_DEFINE_TYPE (GtkTreeExpander, gtk_tree_expander, GTK_TYPE_WIDGET)
105
106static GParamSpec *properties[N_PROPS] = { NULL, };
107
108static void
109gtk_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
126static void
127gtk_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
139static void
140gtk_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
150static void
151gtk_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
269static void
270gtk_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
305static gboolean
306gtk_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
340static gboolean
341gtk_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
351static void
352gtk_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
362static void
363gtk_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
383static void
384gtk_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
415static void
416gtk_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
443static void
444gtk_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
456static void
457gtk_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
469static void
470gtk_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
482static gboolean
483expand_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
497static gboolean
498expand_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
512static void
513gtk_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
658static gboolean
659gtk_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
673static void
674gtk_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
690static void
691gtk_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
701static void
702gtk_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 **/
722GtkWidget *
723gtk_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 **/
737GtkWidget *
738gtk_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 */
752void
753gtk_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 */
795gpointer
796gtk_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 */
814GtkTreeListRow *
815gtk_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 */
829void
830gtk_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 */
870gboolean
871gtk_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 */
887void
888gtk_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

source code of gtk/gtk/gtktreeexpander.c