1 | /* |
2 | * Copyright (c) 2013 Red Hat, Inc. |
3 | * |
4 | * This program is free software; you can redistribute it and/or modify |
5 | * it under the terms of the GNU Lesser General Public License as published by |
6 | * the Free Software Foundation; either version 2 of the License, or (at your |
7 | * option) any later version. |
8 | * |
9 | * This program is distributed in the hope that it will be useful, but |
10 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
11 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public |
12 | * License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Lesser General Public License |
15 | * along with this program; if not, write to the Free Software Foundation, |
16 | * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
17 | * |
18 | */ |
19 | |
20 | #include "config.h" |
21 | |
22 | #include "gtkstackswitcher.h" |
23 | |
24 | #include "gtkboxlayout.h" |
25 | #include "gtkdropcontrollermotion.h" |
26 | #include "gtkimage.h" |
27 | #include "gtkintl.h" |
28 | #include "gtklabel.h" |
29 | #include "gtkorientable.h" |
30 | #include "gtkprivate.h" |
31 | #include "gtkselectionmodel.h" |
32 | #include "gtktogglebutton.h" |
33 | #include "gtktypebuiltins.h" |
34 | #include "gtkwidgetprivate.h" |
35 | |
36 | /** |
37 | * GtkStackSwitcher: |
38 | * |
39 | * The `GtkStackSwitcher` shows a row of buttons to switch between `GtkStack` |
40 | * pages. |
41 | * |
42 | * ![An example GtkStackSwitcher](stackswitcher.png) |
43 | * |
44 | * It acts as a controller for the associated `GtkStack`. |
45 | * |
46 | * All the content for the buttons comes from the properties of the stacks |
47 | * [class@Gtk.StackPage] objects; the button visibility in a `GtkStackSwitcher` |
48 | * widget is controlled by the visibility of the child in the `GtkStack`. |
49 | * |
50 | * It is possible to associate multiple `GtkStackSwitcher` widgets |
51 | * with the same `GtkStack` widget. |
52 | * |
53 | * # CSS nodes |
54 | * |
55 | * `GtkStackSwitcher` has a single CSS node named stackswitcher and |
56 | * style class .stack-switcher. |
57 | * |
58 | * When circumstances require it, `GtkStackSwitcher` adds the |
59 | * .needs-attention style class to the widgets representing the |
60 | * stack pages. |
61 | * |
62 | * # Accessibility |
63 | * |
64 | * `GtkStackSwitcher` uses the %GTK_ACCESSIBLE_ROLE_TAB_LIST role |
65 | * and uses the %GTK_ACCESSIBLE_ROLE_TAB for its buttons. |
66 | * |
67 | * # Orientable |
68 | * |
69 | * Since GTK 4.4, `GtkStackSwitcher` implements `GtkOrientable` allowing |
70 | * the stack switcher to be made vertical with |
71 | * `gtk_orientable_set_orientation()`. |
72 | */ |
73 | |
74 | #define TIMEOUT_EXPAND 500 |
75 | |
76 | typedef struct _GtkStackSwitcherClass GtkStackSwitcherClass; |
77 | |
78 | struct _GtkStackSwitcher |
79 | { |
80 | GtkWidget parent_instance; |
81 | |
82 | GtkStack *stack; |
83 | GtkSelectionModel *pages; |
84 | GHashTable *buttons; |
85 | }; |
86 | |
87 | struct _GtkStackSwitcherClass |
88 | { |
89 | GtkWidgetClass parent_class; |
90 | }; |
91 | |
92 | enum { |
93 | PROP_0, |
94 | PROP_STACK, |
95 | PROP_ORIENTATION |
96 | }; |
97 | |
98 | G_DEFINE_TYPE_WITH_CODE (GtkStackSwitcher, gtk_stack_switcher, GTK_TYPE_WIDGET, |
99 | G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)) |
100 | |
101 | static void |
102 | gtk_stack_switcher_init (GtkStackSwitcher *switcher) |
103 | { |
104 | switcher->buttons = g_hash_table_new_full (hash_func: g_direct_hash, key_equal_func: g_direct_equal, key_destroy_func: g_object_unref, NULL); |
105 | |
106 | gtk_widget_add_css_class (GTK_WIDGET (switcher), css_class: "linked" ); |
107 | } |
108 | |
109 | static void |
110 | on_button_toggled (GtkWidget *button, |
111 | GParamSpec *pspec, |
112 | GtkStackSwitcher *self) |
113 | { |
114 | gboolean active; |
115 | guint index; |
116 | |
117 | active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)); |
118 | index = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (button), "child-index" )); |
119 | |
120 | if (active) |
121 | { |
122 | gtk_selection_model_select_item (model: self->pages, position: index, TRUE); |
123 | } |
124 | else |
125 | { |
126 | gboolean selected = gtk_selection_model_is_selected (model: self->pages, position: index); |
127 | gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), is_active: selected); |
128 | } |
129 | } |
130 | |
131 | static void |
132 | rebuild_child (GtkWidget *self, |
133 | const char *icon_name, |
134 | const char *title, |
135 | gboolean use_underline) |
136 | { |
137 | GtkWidget *button_child; |
138 | |
139 | button_child = NULL; |
140 | |
141 | if (icon_name != NULL) |
142 | { |
143 | button_child = gtk_image_new_from_icon_name (icon_name); |
144 | if (title != NULL) |
145 | gtk_widget_set_tooltip_text (GTK_WIDGET (self), text: title); |
146 | |
147 | gtk_widget_remove_css_class (widget: self, css_class: "text-button" ); |
148 | gtk_widget_add_css_class (widget: self, css_class: "image-button" ); |
149 | } |
150 | else if (title != NULL) |
151 | { |
152 | button_child = gtk_label_new (str: title); |
153 | gtk_label_set_use_underline (GTK_LABEL (button_child), setting: use_underline); |
154 | |
155 | gtk_widget_set_tooltip_text (GTK_WIDGET (self), NULL); |
156 | |
157 | gtk_widget_remove_css_class (widget: self, css_class: "image-button" ); |
158 | gtk_widget_add_css_class (widget: self, css_class: "text-button" ); |
159 | } |
160 | |
161 | if (button_child) |
162 | { |
163 | gtk_widget_set_halign (GTK_WIDGET (button_child), align: GTK_ALIGN_CENTER); |
164 | gtk_button_set_child (GTK_BUTTON (self), child: button_child); |
165 | } |
166 | |
167 | gtk_accessible_update_property (self: GTK_ACCESSIBLE (ptr: self), |
168 | first_property: GTK_ACCESSIBLE_PROPERTY_LABEL, title, |
169 | -1); |
170 | } |
171 | |
172 | static void |
173 | update_button (GtkStackSwitcher *self, |
174 | GtkStackPage *page, |
175 | GtkWidget *button) |
176 | { |
177 | char *title; |
178 | char *icon_name; |
179 | gboolean needs_attention; |
180 | gboolean visible; |
181 | gboolean use_underline; |
182 | |
183 | g_object_get (object: page, |
184 | first_property_name: "title" , &title, |
185 | "icon-name" , &icon_name, |
186 | "needs-attention" , &needs_attention, |
187 | "visible" , &visible, |
188 | "use-underline" , &use_underline, |
189 | NULL); |
190 | |
191 | rebuild_child (self: button, icon_name, title, use_underline); |
192 | |
193 | gtk_widget_set_visible (widget: button, visible: visible && (title != NULL || icon_name != NULL)); |
194 | |
195 | if (needs_attention) |
196 | gtk_widget_add_css_class (widget: button, css_class: "needs-attention" ); |
197 | else |
198 | gtk_widget_remove_css_class (widget: button, css_class: "needs-attention" ); |
199 | |
200 | g_free (mem: title); |
201 | g_free (mem: icon_name); |
202 | } |
203 | |
204 | static void |
205 | on_page_updated (GtkStackPage *page, |
206 | GParamSpec *pspec, |
207 | GtkStackSwitcher *self) |
208 | { |
209 | GtkWidget *button; |
210 | |
211 | button = g_hash_table_lookup (hash_table: self->buttons, key: page); |
212 | update_button (self, page, button); |
213 | } |
214 | |
215 | static gboolean |
216 | gtk_stack_switcher_switch_timeout (gpointer data) |
217 | { |
218 | GtkWidget *button = data; |
219 | |
220 | g_object_steal_data (G_OBJECT (button), key: "-gtk-switch-timer" ); |
221 | |
222 | if (button) |
223 | gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE); |
224 | |
225 | return G_SOURCE_REMOVE; |
226 | } |
227 | |
228 | static void |
229 | clear_timer (gpointer data) |
230 | { |
231 | if (data) |
232 | g_source_remove (GPOINTER_TO_UINT (data)); |
233 | } |
234 | |
235 | static void |
236 | gtk_stack_switcher_drag_enter (GtkDropControllerMotion *motion, |
237 | double x, |
238 | double y, |
239 | gpointer unused) |
240 | { |
241 | GtkWidget *button = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (motion)); |
242 | |
243 | if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button))) |
244 | { |
245 | guint switch_timer = g_timeout_add (TIMEOUT_EXPAND, |
246 | function: gtk_stack_switcher_switch_timeout, |
247 | data: button); |
248 | gdk_source_set_static_name_by_id (tag: switch_timer, name: "[gtk] gtk_stack_switcher_switch_timeout" ); |
249 | g_object_set_data_full (G_OBJECT (button), key: "-gtk-switch-timer" , GUINT_TO_POINTER (switch_timer), destroy: clear_timer); |
250 | } |
251 | } |
252 | |
253 | static void |
254 | gtk_stack_switcher_drag_leave (GtkDropControllerMotion *motion, |
255 | gpointer unused) |
256 | { |
257 | GtkWidget *button = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (motion)); |
258 | guint switch_timer; |
259 | |
260 | switch_timer = GPOINTER_TO_UINT (g_object_steal_data (G_OBJECT (button), "-gtk-switch-timer" )); |
261 | if (switch_timer) |
262 | g_source_remove (tag: switch_timer); |
263 | } |
264 | |
265 | static void |
266 | add_child (guint position, |
267 | GtkStackSwitcher *self) |
268 | { |
269 | GtkWidget *button; |
270 | gboolean selected; |
271 | GtkStackPage *page; |
272 | GtkEventController *controller; |
273 | |
274 | button = g_object_new (GTK_TYPE_TOGGLE_BUTTON, |
275 | first_property_name: "accessible-role" , GTK_ACCESSIBLE_ROLE_TAB, |
276 | "hexpand" , TRUE, |
277 | "vexpand" , TRUE, |
278 | NULL); |
279 | gtk_widget_set_focus_on_click (widget: button, FALSE); |
280 | |
281 | controller = gtk_drop_controller_motion_new (); |
282 | g_signal_connect (controller, "enter" , G_CALLBACK (gtk_stack_switcher_drag_enter), NULL); |
283 | g_signal_connect (controller, "leave" , G_CALLBACK (gtk_stack_switcher_drag_leave), NULL); |
284 | gtk_widget_add_controller (widget: button, controller); |
285 | |
286 | page = g_list_model_get_item (list: G_LIST_MODEL (ptr: self->pages), position); |
287 | update_button (self, page, button); |
288 | |
289 | gtk_widget_set_parent (widget: button, GTK_WIDGET (self)); |
290 | |
291 | g_object_set_data (G_OBJECT (button), key: "child-index" , GUINT_TO_POINTER (position)); |
292 | selected = gtk_selection_model_is_selected (model: self->pages, position); |
293 | gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), is_active: selected); |
294 | |
295 | gtk_accessible_update_state (self: GTK_ACCESSIBLE (ptr: button), |
296 | first_state: GTK_ACCESSIBLE_STATE_SELECTED, selected, |
297 | -1); |
298 | |
299 | gtk_accessible_update_relation (self: GTK_ACCESSIBLE (ptr: button), |
300 | first_relation: GTK_ACCESSIBLE_RELATION_CONTROLS, page, NULL, |
301 | -1); |
302 | |
303 | g_signal_connect (button, "notify::active" , G_CALLBACK (on_button_toggled), self); |
304 | g_signal_connect (page, "notify" , G_CALLBACK (on_page_updated), self); |
305 | |
306 | g_hash_table_insert (hash_table: self->buttons, g_object_ref (page), value: button); |
307 | |
308 | g_object_unref (object: page); |
309 | } |
310 | |
311 | static void |
312 | populate_switcher (GtkStackSwitcher *self) |
313 | { |
314 | guint i; |
315 | |
316 | for (i = 0; i < g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self->pages)); i++) |
317 | add_child (position: i, self); |
318 | } |
319 | |
320 | static void |
321 | clear_switcher (GtkStackSwitcher *self) |
322 | { |
323 | GHashTableIter iter; |
324 | GtkWidget *page; |
325 | GtkWidget *button; |
326 | |
327 | g_hash_table_iter_init (iter: &iter, hash_table: self->buttons); |
328 | while (g_hash_table_iter_next (iter: &iter, key: (gpointer *)&page, value: (gpointer *)&button)) |
329 | { |
330 | gtk_widget_unparent (widget: button); |
331 | g_signal_handlers_disconnect_by_func (page, on_page_updated, self); |
332 | g_hash_table_iter_remove (iter: &iter); |
333 | } |
334 | } |
335 | |
336 | static void |
337 | items_changed_cb (GListModel *model, |
338 | guint position, |
339 | guint removed, |
340 | guint added, |
341 | GtkStackSwitcher *switcher) |
342 | { |
343 | clear_switcher (self: switcher); |
344 | populate_switcher (self: switcher); |
345 | } |
346 | |
347 | static void |
348 | selection_changed_cb (GtkSelectionModel *model, |
349 | guint position, |
350 | guint n_items, |
351 | GtkStackSwitcher *switcher) |
352 | { |
353 | guint i; |
354 | |
355 | for (i = position; i < position + n_items; i++) |
356 | { |
357 | GtkStackPage *page; |
358 | GtkWidget *button; |
359 | gboolean selected; |
360 | |
361 | page = g_list_model_get_item (list: G_LIST_MODEL (ptr: switcher->pages), position: i); |
362 | button = g_hash_table_lookup (hash_table: switcher->buttons, key: page); |
363 | if (button) |
364 | { |
365 | selected = gtk_selection_model_is_selected (model: switcher->pages, position: i); |
366 | gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), is_active: selected); |
367 | |
368 | gtk_accessible_update_state (self: GTK_ACCESSIBLE (ptr: button), |
369 | first_state: GTK_ACCESSIBLE_STATE_SELECTED, selected, |
370 | -1); |
371 | } |
372 | g_object_unref (object: page); |
373 | } |
374 | } |
375 | |
376 | static void |
377 | disconnect_stack_signals (GtkStackSwitcher *switcher) |
378 | { |
379 | g_signal_handlers_disconnect_by_func (switcher->pages, items_changed_cb, switcher); |
380 | g_signal_handlers_disconnect_by_func (switcher->pages, selection_changed_cb, switcher); |
381 | } |
382 | |
383 | static void |
384 | connect_stack_signals (GtkStackSwitcher *switcher) |
385 | { |
386 | g_signal_connect (switcher->pages, "items-changed" , G_CALLBACK (items_changed_cb), switcher); |
387 | g_signal_connect (switcher->pages, "selection-changed" , G_CALLBACK (selection_changed_cb), switcher); |
388 | } |
389 | |
390 | static void |
391 | set_stack (GtkStackSwitcher *switcher, |
392 | GtkStack *stack) |
393 | { |
394 | if (stack) |
395 | { |
396 | switcher->stack = g_object_ref (stack); |
397 | switcher->pages = gtk_stack_get_pages (stack); |
398 | populate_switcher (self: switcher); |
399 | connect_stack_signals (switcher); |
400 | } |
401 | } |
402 | |
403 | static void |
404 | unset_stack (GtkStackSwitcher *switcher) |
405 | { |
406 | if (switcher->stack) |
407 | { |
408 | disconnect_stack_signals (switcher); |
409 | clear_switcher (self: switcher); |
410 | g_clear_object (&switcher->stack); |
411 | g_clear_object (&switcher->pages); |
412 | } |
413 | } |
414 | |
415 | /** |
416 | * gtk_stack_switcher_set_stack: (attributes org.gtk.Method.set_property=stack) |
417 | * @switcher: a `GtkStackSwitcher` |
418 | * @stack: (nullable): a `GtkStack` |
419 | * |
420 | * Sets the stack to control. |
421 | */ |
422 | void |
423 | gtk_stack_switcher_set_stack (GtkStackSwitcher *switcher, |
424 | GtkStack *stack) |
425 | { |
426 | g_return_if_fail (GTK_IS_STACK_SWITCHER (switcher)); |
427 | g_return_if_fail (GTK_IS_STACK (stack) || stack == NULL); |
428 | |
429 | if (switcher->stack == stack) |
430 | return; |
431 | |
432 | unset_stack (switcher); |
433 | set_stack (switcher, stack); |
434 | |
435 | gtk_widget_queue_resize (GTK_WIDGET (switcher)); |
436 | |
437 | g_object_notify (G_OBJECT (switcher), property_name: "stack" ); |
438 | } |
439 | |
440 | /** |
441 | * gtk_stack_switcher_get_stack: (attributes org.gtk.Method.get_property=stack) |
442 | * @switcher: a `GtkStackSwitcher` |
443 | * |
444 | * Retrieves the stack. |
445 | * |
446 | * Returns: (nullable) (transfer none): the stack |
447 | */ |
448 | GtkStack * |
449 | gtk_stack_switcher_get_stack (GtkStackSwitcher *switcher) |
450 | { |
451 | g_return_val_if_fail (GTK_IS_STACK_SWITCHER (switcher), NULL); |
452 | |
453 | return switcher->stack; |
454 | } |
455 | |
456 | static void |
457 | gtk_stack_switcher_get_property (GObject *object, |
458 | guint prop_id, |
459 | GValue *value, |
460 | GParamSpec *pspec) |
461 | { |
462 | GtkStackSwitcher *switcher = GTK_STACK_SWITCHER (object); |
463 | GtkLayoutManager *box_layout = gtk_widget_get_layout_manager (GTK_WIDGET (switcher)); |
464 | |
465 | switch (prop_id) |
466 | { |
467 | case PROP_ORIENTATION: |
468 | g_value_set_enum (value, v_enum: gtk_orientable_get_orientation (GTK_ORIENTABLE (box_layout))); |
469 | break; |
470 | |
471 | case PROP_STACK: |
472 | g_value_set_object (value, v_object: switcher->stack); |
473 | break; |
474 | |
475 | default: |
476 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
477 | break; |
478 | } |
479 | } |
480 | |
481 | static void |
482 | gtk_stack_switcher_set_property (GObject *object, |
483 | guint prop_id, |
484 | const GValue *value, |
485 | GParamSpec *pspec) |
486 | { |
487 | GtkStackSwitcher *switcher = GTK_STACK_SWITCHER (object); |
488 | GtkLayoutManager *box_layout = gtk_widget_get_layout_manager (GTK_WIDGET (switcher)); |
489 | |
490 | switch (prop_id) |
491 | { |
492 | case PROP_ORIENTATION: |
493 | { |
494 | GtkOrientation orientation = g_value_get_enum (value); |
495 | if (gtk_orientable_get_orientation (GTK_ORIENTABLE (box_layout)) != orientation) |
496 | { |
497 | gtk_orientable_set_orientation (GTK_ORIENTABLE (box_layout), orientation); |
498 | gtk_widget_update_orientation (GTK_WIDGET (switcher), orientation); |
499 | g_object_notify_by_pspec (object, pspec); |
500 | } |
501 | } |
502 | break; |
503 | |
504 | case PROP_STACK: |
505 | gtk_stack_switcher_set_stack (switcher, stack: g_value_get_object (value)); |
506 | break; |
507 | |
508 | default: |
509 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
510 | break; |
511 | } |
512 | } |
513 | |
514 | static void |
515 | gtk_stack_switcher_dispose (GObject *object) |
516 | { |
517 | GtkStackSwitcher *switcher = GTK_STACK_SWITCHER (object); |
518 | |
519 | unset_stack (switcher); |
520 | |
521 | G_OBJECT_CLASS (gtk_stack_switcher_parent_class)->dispose (object); |
522 | } |
523 | |
524 | static void |
525 | gtk_stack_switcher_finalize (GObject *object) |
526 | { |
527 | GtkStackSwitcher *switcher = GTK_STACK_SWITCHER (object); |
528 | |
529 | g_hash_table_destroy (hash_table: switcher->buttons); |
530 | |
531 | G_OBJECT_CLASS (gtk_stack_switcher_parent_class)->finalize (object); |
532 | } |
533 | |
534 | static void |
535 | gtk_stack_switcher_class_init (GtkStackSwitcherClass *class) |
536 | { |
537 | GObjectClass *object_class = G_OBJECT_CLASS (class); |
538 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); |
539 | |
540 | object_class->get_property = gtk_stack_switcher_get_property; |
541 | object_class->set_property = gtk_stack_switcher_set_property; |
542 | object_class->dispose = gtk_stack_switcher_dispose; |
543 | object_class->finalize = gtk_stack_switcher_finalize; |
544 | |
545 | /** |
546 | * GtkStackSwitcher:stack: (attributes org.gtk.Property.get=gtk_stack_switcher_get_stack org.gtk.Property.set=gtk_stack_switcher_set_stack) |
547 | * |
548 | * The stack. |
549 | */ |
550 | g_object_class_install_property (oclass: object_class, |
551 | property_id: PROP_STACK, |
552 | pspec: g_param_spec_object (name: "stack" , |
553 | P_("Stack" ), |
554 | P_("Stack" ), |
555 | GTK_TYPE_STACK, |
556 | GTK_PARAM_READWRITE | |
557 | G_PARAM_CONSTRUCT)); |
558 | |
559 | g_object_class_override_property (oclass: object_class, property_id: PROP_ORIENTATION, name: "orientation" ); |
560 | |
561 | gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BOX_LAYOUT); |
562 | gtk_widget_class_set_css_name (widget_class, I_("stackswitcher" )); |
563 | gtk_widget_class_set_accessible_role (widget_class, accessible_role: GTK_ACCESSIBLE_ROLE_TAB_LIST); |
564 | } |
565 | |
566 | /** |
567 | * gtk_stack_switcher_new: |
568 | * |
569 | * Create a new `GtkStackSwitcher`. |
570 | * |
571 | * Returns: a new `GtkStackSwitcher`. |
572 | */ |
573 | GtkWidget * |
574 | gtk_stack_switcher_new (void) |
575 | { |
576 | return g_object_new (GTK_TYPE_STACK_SWITCHER, NULL); |
577 | } |
578 | |