1/*
2 * Copyright © 2014 Red Hat, Inc.
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 of the licence, 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 * Author: Matthias Clasen
18 */
19
20#include "config.h"
21
22#include "gtkmodelbuttonprivate.h"
23
24#include "gtkactionhelperprivate.h"
25#include "gtkboxlayout.h"
26#include "gtkgestureclick.h"
27#include "gtkwidgetprivate.h"
28#include "gtkmenutrackeritemprivate.h"
29#include "gtkimage.h"
30#include "gtklabel.h"
31#include "gtkbox.h"
32#include "gtktypebuiltins.h"
33#include "gtkstack.h"
34#include "gtkpopovermenuprivate.h"
35#include "gtkintl.h"
36#include "gtkcssnodeprivate.h"
37#include "gtkcsstypesprivate.h"
38#include "gtkbuiltiniconprivate.h"
39#include "gtksizegroup.h"
40#include "gtkactionable.h"
41#include "gtkeventcontrollermotion.h"
42#include "gtkeventcontrollerkey.h"
43#include "gtkeventcontrollerfocus.h"
44#include "gtknative.h"
45#include "gtkshortcuttrigger.h"
46#include "gtkshortcutcontroller.h"
47#include "gtkshortcut.h"
48#include "gtkaccessibleprivate.h"
49#include "gtkprivate.h"
50
51/*< private >
52 * GtkModelButton:
53 *
54 * GtkModelButton is a button class that can use a GAction as its model.
55 * In contrast to GtkToggleButton or GtkCheckButton, which can also
56 * be backed by a GAction via the GtkActionable:action-name property,
57 * GtkModelButton will adapt its appearance according to the kind of
58 * action it is backed by, and appear either as a plain, check or
59 * radio button.
60 *
61 * Model buttons are used when popovers from a menu model with
62 * gtk_popover_menu_new_from_model(); they can also be used manually in
63 * a GtkPopoverMenu.
64 *
65 * When the action is specified via the GtkActionable:action-name
66 * and GtkActionable:action-target properties, the role of the button
67 * (i.e. whether it is a plain, check or radio button) is determined by
68 * the type of the action and doesn't have to be explicitly specified
69 * with the GtkModelButton:role property.
70 *
71 * The content of the button is specified by the GtkModelButton:text
72 * and GtkModelButton:icon properties.
73 *
74 * The appearance of model buttons can be influenced with the
75 * GtkModelButton:iconic property.
76 *
77 * Model buttons have built-in support for submenus in GtkPopoverMenu.
78 * To make a GtkModelButton that opens a submenu when activated, set
79 * the GtkModelButton:menu-name property. To make a button that goes
80 * back to the parent menu, you should set the GtkModelButton:inverted
81 * property to place the submenu indicator at the opposite side.
82 *
83 * # Example
84 *
85 * |[
86 * <object class="GtkPopoverMenu">
87 * <child>
88 * <object class="GtkBox">
89 * <property name="visible">True</property>
90 * <property name="margin-start">10</property>
91 * <property name="margin-end">10</property>
92 * <property name="margin-top">10</property>
93 * <property name="margin-bottom">10</property>
94 * <child>
95 * <object class="GtkModelButton">
96 * <property name="visible">True</property>
97 * <property name="action-name">view.cut</property>
98 * <property name="text" translatable="yes">Cut</property>
99 * </object>
100 * </child>
101 * <child>
102 * <object class="GtkModelButton">
103 * <property name="visible">True</property>
104 * <property name="action-name">view.copy</property>
105 * <property name="text" translatable="yes">Copy</property>
106 * </object>
107 * </child>
108 * <child>
109 * <object class="GtkModelButton">
110 * <property name="visible">True</property>
111 * <property name="action-name">view.paste</property>
112 * <property name="text" translatable="yes">Paste</property>
113 * </object>
114 * </child>
115 * </object>
116 * </child>
117 * </object>
118 * ]|
119 *
120 * # CSS nodes
121 *
122 * |[<!-- language="plain" -->
123 * modelbutton
124 * ├── <child>
125 * ╰── check
126 * ]|
127 *
128 * |[<!-- language="plain" -->
129 * modelbutton
130 * ├── <child>
131 * ╰── radio
132 * ]|
133 *
134 * |[<!-- language="plain" -->
135 * modelbutton
136 * ├── <child>
137 * ╰── arrow
138 * ]|
139 *
140 * GtkModelButton has a main CSS node with name modelbutton, and a subnode,
141 * which will have the name check, radio or arrow, depending on the role
142 * of the button and whether it has a menu name set.
143 *
144 * The subnode is positioned before or after the content nodes and gets the
145 * .left or .right style class, depending on where it is located.
146 *
147 * |[<!-- language="plain" -->
148 * button.model
149 * ├── <child>
150 * ╰── check
151 * ]|
152 *
153 * Iconic model buttons (see GtkModelButton:iconic) change the name of
154 * their main node to button and add a .model style class to it. The indicator
155 * subnode is invisible in this case.
156 */
157
158struct _GtkModelButton
159{
160 GtkWidget parent_instance;
161
162 GtkWidget *box;
163 GtkWidget *image;
164 GtkWidget *label;
165 GtkWidget *accel_label;
166 GtkWidget *start_box;
167 GtkWidget *start_indicator;
168 GtkWidget *end_indicator;
169 GtkWidget *popover;
170 GtkActionHelper *action_helper;
171 char *menu_name;
172 GtkButtonRole role;
173 GtkSizeGroup *indicators;
174 char *accel;
175 guint open_timeout;
176 GtkEventController *controller;
177
178 guint active : 1;
179 guint iconic : 1;
180 guint keep_open : 1;
181};
182
183typedef struct _GtkModelButtonClass GtkModelButtonClass;
184
185struct _GtkModelButtonClass
186{
187 GtkWidgetClass parent_class;
188
189 void (* clicked) (GtkModelButton *button);
190};
191
192static void gtk_model_button_actionable_iface_init (GtkActionableInterface *iface);
193
194G_DEFINE_TYPE_WITH_CODE (GtkModelButton, gtk_model_button, GTK_TYPE_WIDGET,
195 G_IMPLEMENT_INTERFACE (GTK_TYPE_ACTIONABLE, gtk_model_button_actionable_iface_init))
196
197GType
198gtk_button_role_get_type (void)
199{
200 static gsize gtk_button_role_type;
201
202 if (g_once_init_enter (&gtk_button_role_type))
203 {
204 static const GEnumValue values[] = {
205 { GTK_BUTTON_ROLE_NORMAL, "GTK_BUTTON_ROLE_NORMAL", "normal" },
206 { GTK_BUTTON_ROLE_CHECK, "GTK_BUTTON_ROLE_CHECK", "check" },
207 { GTK_BUTTON_ROLE_RADIO, "GTK_BUTTON_ROLE_RADIO", "radio" },
208 { GTK_BUTTON_ROLE_TITLE, "GTK_BUTTON_ROLE_RADIO", "title" },
209 { 0, NULL, NULL }
210 };
211 GType type;
212
213 type = g_enum_register_static (I_("GtkButtonRole"), const_static_values: values);
214
215 g_once_init_leave (&gtk_button_role_type, type);
216 }
217
218 return gtk_button_role_type;
219}
220
221enum
222{
223 PROP_0,
224 PROP_ROLE,
225 PROP_ICON,
226 PROP_TEXT,
227 PROP_USE_MARKUP,
228 PROP_ACTIVE,
229 PROP_MENU_NAME,
230 PROP_POPOVER,
231 PROP_ICONIC,
232 PROP_ACCEL,
233 PROP_INDICATOR_SIZE_GROUP,
234
235 /* actionable properties */
236 PROP_ACTION_NAME,
237 PROP_ACTION_TARGET,
238 LAST_PROP = PROP_ACTION_NAME
239};
240
241enum
242{
243 SIGNAL_CLICKED,
244 LAST_SIGNAL
245};
246
247static GParamSpec *properties[LAST_PROP] = { NULL, };
248static guint signals[LAST_SIGNAL] = { 0 };
249
250static void
251gtk_model_button_set_action_name (GtkActionable *actionable,
252 const char *action_name)
253{
254 GtkModelButton *self = GTK_MODEL_BUTTON (actionable);
255
256 if (!self->action_helper)
257 self->action_helper = gtk_action_helper_new (widget: actionable);
258
259 gtk_action_helper_set_action_name (helper: self->action_helper, action_name);
260}
261
262static void
263gtk_model_button_set_action_target_value (GtkActionable *actionable,
264 GVariant *action_target)
265{
266 GtkModelButton *self = GTK_MODEL_BUTTON (actionable);
267
268 if (!self->action_helper)
269 self->action_helper = gtk_action_helper_new (widget: actionable);
270
271 gtk_action_helper_set_action_target_value (helper: self->action_helper, action_target);
272}
273
274static const char *
275gtk_model_button_get_action_name (GtkActionable *actionable)
276{
277 GtkModelButton *self = GTK_MODEL_BUTTON (actionable);
278
279 return gtk_action_helper_get_action_name (helper: self->action_helper);
280}
281
282static GVariant *
283gtk_model_button_get_action_target_value (GtkActionable *actionable)
284{
285 GtkModelButton *self = GTK_MODEL_BUTTON (actionable);
286
287 return gtk_action_helper_get_action_target_value (helper: self->action_helper);
288}
289
290static void
291gtk_model_button_actionable_iface_init (GtkActionableInterface *iface)
292{
293 iface->get_action_name = gtk_model_button_get_action_name;
294 iface->set_action_name = gtk_model_button_set_action_name;
295 iface->get_action_target_value = gtk_model_button_get_action_target_value;
296 iface->set_action_target_value = gtk_model_button_set_action_target_value;
297}
298
299static void
300update_at_context (GtkModelButton *button)
301{
302 GtkAccessibleRole role;
303 GtkATContext *context;
304 gboolean was_realized;
305
306 context = gtk_accessible_get_at_context (self: GTK_ACCESSIBLE (ptr: button));
307 if (context == NULL)
308 return;
309
310 was_realized = gtk_at_context_is_realized (self: context);
311
312 gtk_at_context_unrealize (self: context);
313
314 switch (button->role)
315 {
316 default:
317 case GTK_BUTTON_ROLE_NORMAL:
318 case GTK_BUTTON_ROLE_TITLE:
319 role = GTK_ACCESSIBLE_ROLE_MENU_ITEM;
320 break;
321 case GTK_BUTTON_ROLE_CHECK:
322 role = GTK_ACCESSIBLE_ROLE_MENU_ITEM_CHECKBOX;
323 break;
324 case GTK_BUTTON_ROLE_RADIO:
325 role = GTK_ACCESSIBLE_ROLE_MENU_ITEM_RADIO;
326 break;
327 }
328
329 gtk_at_context_set_accessible_role (self: context, role);
330
331 if (was_realized)
332 gtk_at_context_realize (self: context);
333}
334
335static void
336update_node_ordering (GtkModelButton *button)
337{
338 GtkWidget *child;
339
340 if (gtk_widget_get_direction (GTK_WIDGET (button)) == GTK_TEXT_DIR_LTR)
341 {
342 if (button->start_indicator)
343 {
344 gtk_widget_add_css_class (widget: button->start_indicator, css_class: "left");
345 gtk_widget_remove_css_class (widget: button->start_indicator, css_class: "right");
346 }
347
348 if (button->end_indicator)
349 {
350 gtk_widget_add_css_class (widget: button->end_indicator, css_class: "right");
351 gtk_widget_remove_css_class (widget: button->end_indicator, css_class: "left");
352 }
353
354 child = gtk_widget_get_first_child (GTK_WIDGET (button));
355 if (button->start_indicator && child != button->start_box)
356 gtk_widget_insert_before (widget: button->start_box, GTK_WIDGET (button), next_sibling: child);
357
358 child = gtk_widget_get_last_child (GTK_WIDGET (button));
359 if (button->end_indicator && child != button->end_indicator)
360 gtk_widget_insert_after (widget: button->end_indicator, GTK_WIDGET (button), previous_sibling: child);
361 }
362 else
363 {
364 if (button->start_indicator)
365 {
366 gtk_widget_add_css_class (widget: button->start_indicator, css_class: "right");
367 gtk_widget_remove_css_class (widget: button->start_indicator, css_class: "left");
368 }
369
370 if (button->end_indicator)
371 {
372 gtk_widget_add_css_class (widget: button->end_indicator, css_class: "left");
373 gtk_widget_remove_css_class (widget: button->end_indicator, css_class: "right");
374
375 }
376
377 child = gtk_widget_get_first_child (GTK_WIDGET (button));
378 if (button->end_indicator && child != button->end_indicator)
379 gtk_widget_insert_before (widget: button->end_indicator, GTK_WIDGET (button), next_sibling: child);
380
381 child = gtk_widget_get_last_child (GTK_WIDGET (button));
382 if (button->end_indicator && child != button->end_indicator)
383 gtk_widget_insert_after (widget: button->end_indicator, GTK_WIDGET (button), previous_sibling: child);
384 }
385}
386
387static void
388update_end_indicator (GtkModelButton *self)
389{
390 const gboolean is_ltr = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_LTR;
391
392 if (!self->end_indicator)
393 return;
394
395 if (is_ltr)
396 {
397 gtk_widget_add_css_class (widget: self->end_indicator, css_class: "right");
398 gtk_widget_remove_css_class (widget: self->end_indicator, css_class: "left");
399 }
400 else
401 {
402 gtk_widget_add_css_class (widget: self->end_indicator, css_class: "left");
403 gtk_widget_remove_css_class (widget: self->end_indicator, css_class: "right");
404 }
405}
406
407static GtkStateFlags
408get_start_indicator_state (GtkModelButton *self)
409{
410 GtkStateFlags state = gtk_widget_get_state_flags (GTK_WIDGET (self));
411
412 if (self->role == GTK_BUTTON_ROLE_CHECK ||
413 self->role == GTK_BUTTON_ROLE_RADIO)
414 {
415 if (self->active)
416 state |= GTK_STATE_FLAG_CHECKED;
417 else
418 state &= ~GTK_STATE_FLAG_CHECKED;
419 }
420
421 return state;
422}
423
424static void
425update_start_indicator (GtkModelButton *self)
426{
427 const gboolean is_ltr = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_LTR;
428
429 if (!self->start_indicator)
430 return;
431
432 gtk_widget_set_state_flags (widget: self->start_indicator, flags: get_start_indicator_state (self), TRUE);
433
434 if (is_ltr)
435 {
436 gtk_widget_add_css_class (widget: self->start_indicator, css_class: "left");
437 gtk_widget_remove_css_class (widget: self->start_indicator, css_class: "right");
438 }
439 else
440 {
441 gtk_widget_add_css_class (widget: self->start_indicator, css_class: "right");
442 gtk_widget_remove_css_class (widget: self->start_indicator, css_class: "left");
443 }
444
445}
446
447static void
448gtk_model_button_update_state (GtkModelButton *self)
449{
450 GtkStateFlags indicator_state;
451
452 update_start_indicator (self);
453 update_end_indicator (self);
454
455 indicator_state = get_start_indicator_state (self);
456 if (self->iconic)
457 gtk_widget_set_state_flags (GTK_WIDGET (self), flags: indicator_state, TRUE);
458}
459
460static void
461gtk_model_button_state_flags_changed (GtkWidget *widget,
462 GtkStateFlags previous_flags)
463{
464 gtk_model_button_update_state (GTK_MODEL_BUTTON (widget));
465
466 GTK_WIDGET_CLASS (gtk_model_button_parent_class)->state_flags_changed (widget, previous_flags);
467}
468
469static void
470gtk_model_button_direction_changed (GtkWidget *widget,
471 GtkTextDirection previous_dir)
472{
473 GtkModelButton *button = GTK_MODEL_BUTTON (widget);
474
475 gtk_model_button_update_state (self: button);
476 update_node_ordering (button);
477
478 GTK_WIDGET_CLASS (gtk_model_button_parent_class)->direction_changed (widget, previous_dir);
479}
480
481static void
482update_node_name (GtkModelButton *self)
483{
484 const char *start_name;
485 const char *end_name;
486
487 switch (self->role)
488 {
489 case GTK_BUTTON_ROLE_TITLE:
490 start_name = "arrow";
491 end_name = "";
492 break;
493 case GTK_BUTTON_ROLE_NORMAL:
494 start_name = NULL;
495 if (self->menu_name || self->popover)
496 end_name = "arrow";
497 else
498 end_name = NULL;
499 break;
500
501 case GTK_BUTTON_ROLE_CHECK:
502 start_name = "check";
503 end_name = NULL;
504 break;
505
506 case GTK_BUTTON_ROLE_RADIO:
507 start_name = "radio";
508 end_name = NULL;
509 break;
510
511 default:
512 g_assert_not_reached ();
513 }
514
515 if (self->iconic)
516 {
517 start_name = NULL;
518 end_name = NULL;
519 }
520
521 if (start_name && !self->start_indicator)
522 {
523 self->start_indicator = gtk_builtin_icon_new (css_name: start_name);
524 gtk_widget_set_halign (widget: self->start_indicator, align: GTK_ALIGN_CENTER);
525 gtk_widget_set_valign (widget: self->start_indicator, align: GTK_ALIGN_CENTER);
526 update_start_indicator (self);
527
528 gtk_box_append (GTK_BOX (self->start_box), child: self->start_indicator);
529 }
530 else if (start_name)
531 {
532 gtk_css_node_set_name (cssnode: gtk_widget_get_css_node (widget: self->start_indicator), name: g_quark_from_static_string (string: start_name));
533 }
534 else if (self->start_indicator)
535 {
536 gtk_box_remove (GTK_BOX (self->start_box), child: self->start_indicator);
537 self->start_indicator = NULL;
538 }
539
540 if (end_name && !self->end_indicator)
541 {
542 self->end_indicator = gtk_builtin_icon_new (css_name: end_name);
543 gtk_widget_set_halign (widget: self->end_indicator, align: GTK_ALIGN_CENTER);
544 gtk_widget_set_valign (widget: self->end_indicator, align: GTK_ALIGN_CENTER);
545 gtk_widget_set_parent (widget: self->end_indicator, GTK_WIDGET (self));
546 update_end_indicator (self);
547 }
548 else if (end_name)
549 {
550 gtk_css_node_set_name (cssnode: gtk_widget_get_css_node (widget: self->end_indicator), name: g_quark_from_static_string (string: end_name));
551 }
552 else
553 {
554 g_clear_pointer (&self->end_indicator, gtk_widget_unparent);
555 }
556}
557
558static void
559update_accessible_properties (GtkModelButton *button)
560{
561 if (button->menu_name || button->popover)
562 gtk_accessible_update_property (self: GTK_ACCESSIBLE (ptr: button),
563 first_property: GTK_ACCESSIBLE_PROPERTY_HAS_POPUP, TRUE,
564 -1);
565 else
566 gtk_accessible_reset_property (self: GTK_ACCESSIBLE (ptr: button),
567 property: GTK_ACCESSIBLE_PROPERTY_HAS_POPUP);
568
569 if (button->popover)
570 gtk_accessible_update_relation (self: GTK_ACCESSIBLE (ptr: button),
571 first_relation: GTK_ACCESSIBLE_RELATION_CONTROLS, button->popover, NULL,
572 -1);
573 else
574 gtk_accessible_reset_relation (self: GTK_ACCESSIBLE (ptr: button),
575 relation: GTK_ACCESSIBLE_RELATION_CONTROLS);
576
577 if (button->role == GTK_BUTTON_ROLE_CHECK ||
578 button->role == GTK_BUTTON_ROLE_RADIO)
579 gtk_accessible_update_state (self: GTK_ACCESSIBLE (ptr: button),
580 first_state: GTK_ACCESSIBLE_STATE_CHECKED, button->active,
581 -1);
582 else
583 gtk_accessible_reset_state (self: GTK_ACCESSIBLE (ptr: button),
584 state: GTK_ACCESSIBLE_STATE_CHECKED);
585
586 gtk_accessible_update_relation (self: GTK_ACCESSIBLE (ptr: button),
587 first_relation: GTK_ACCESSIBLE_RELATION_LABELLED_BY, button->label, NULL,
588 -1);
589}
590
591static void
592gtk_model_button_set_role (GtkModelButton *self,
593 GtkButtonRole role)
594{
595 if (role == self->role)
596 return;
597
598 self->role = role;
599
600 if (role == GTK_BUTTON_ROLE_TITLE)
601 {
602 gtk_widget_add_css_class (GTK_WIDGET (self), css_class: "title");
603 gtk_widget_set_halign (widget: self->label, align: GTK_ALIGN_CENTER);
604 }
605 else
606 {
607 gtk_widget_remove_css_class (GTK_WIDGET (self), css_class: "title");
608 gtk_widget_set_halign (widget: self->label, align: GTK_ALIGN_START);
609 }
610
611 update_node_name (self);
612 gtk_model_button_update_state (self);
613
614 update_at_context (button: self);
615 update_accessible_properties (button: self);
616
617 g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_ROLE]);
618}
619
620static void
621update_visibility (GtkModelButton *self)
622{
623 gboolean has_icon;
624 gboolean has_text;
625
626 has_icon = self->image && gtk_image_get_storage_type (GTK_IMAGE (self->image)) != GTK_IMAGE_EMPTY;
627 has_text = gtk_label_get_text (GTK_LABEL (self->label))[0] != '\0';
628
629 gtk_widget_set_visible (widget: self->label, visible: has_text && (!self->iconic || !has_icon));
630 gtk_widget_set_hexpand (widget: self->label,
631 expand: gtk_widget_get_visible (widget: self->label) && !has_icon);
632
633 if (self->accel_label)
634 gtk_widget_set_visible (widget: self->accel_label, visible: has_text && (!self->iconic || !has_icon));
635
636 if (self->image)
637 {
638 gtk_widget_set_visible (widget: self->image, visible: has_icon && (self->iconic || !has_text));
639 gtk_widget_set_hexpand (widget: self->image,
640 expand: has_icon && (!has_text || !gtk_widget_get_visible (widget: self->label)));
641 }
642}
643
644static void
645gtk_model_button_set_icon (GtkModelButton *self,
646 GIcon *icon)
647{
648 if (!self->image && icon)
649 {
650 self->image = g_object_new (GTK_TYPE_IMAGE,
651 first_property_name: "accessible-role", GTK_ACCESSIBLE_ROLE_PRESENTATION,
652 "gicon", icon,
653 NULL);
654 gtk_widget_insert_before (widget: self->image, GTK_WIDGET (self), next_sibling: self->label);
655 }
656 else if (self->image && !icon)
657 {
658 g_clear_pointer (&self->image, gtk_widget_unparent);
659 }
660 else if (icon)
661 {
662 gtk_image_set_from_gicon (GTK_IMAGE (self->image), icon);
663 }
664
665 update_visibility (self);
666 g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_ICON]);
667}
668
669static void
670gtk_model_button_set_text (GtkModelButton *button,
671 const char *text)
672{
673 gtk_label_set_text_with_mnemonic (GTK_LABEL (button->label),
674 str: text ? text : "");
675 update_visibility (self: button);
676
677 gtk_accessible_update_relation (self: GTK_ACCESSIBLE (ptr: button),
678 first_relation: GTK_ACCESSIBLE_RELATION_LABELLED_BY, button->label, NULL,
679 -1);
680
681 g_object_notify_by_pspec (G_OBJECT (button), pspec: properties[PROP_TEXT]);
682}
683
684static void
685gtk_model_button_set_use_markup (GtkModelButton *button,
686 gboolean use_markup)
687{
688 use_markup = !!use_markup;
689 if (gtk_label_get_use_markup (GTK_LABEL (button->label)) == use_markup)
690 return;
691
692 gtk_label_set_use_markup (GTK_LABEL (button->label), setting: use_markup);
693 update_visibility (self: button);
694 g_object_notify_by_pspec (G_OBJECT (button), pspec: properties[PROP_USE_MARKUP]);
695}
696
697static void
698gtk_model_button_set_active (GtkModelButton *button,
699 gboolean active)
700{
701 active = !!active;
702 if (button->active == active)
703 return;
704
705 button->active = active;
706
707 update_accessible_properties (button);
708
709 gtk_model_button_update_state (self: button);
710 gtk_widget_queue_draw (GTK_WIDGET (button));
711 g_object_notify_by_pspec (G_OBJECT (button), pspec: properties[PROP_ACTIVE]);
712}
713
714static void
715gtk_model_button_set_menu_name (GtkModelButton *button,
716 const char *menu_name)
717{
718 g_free (mem: button->menu_name);
719 button->menu_name = g_strdup (str: menu_name);
720
721 update_node_name (self: button);
722 gtk_model_button_update_state (self: button);
723
724 update_accessible_properties (button);
725
726 gtk_widget_queue_resize (GTK_WIDGET (button));
727 g_object_notify_by_pspec (G_OBJECT (button), pspec: properties[PROP_MENU_NAME]);
728}
729
730static void
731gtk_model_button_set_iconic (GtkModelButton *self,
732 gboolean iconic)
733{
734 GtkWidget *widget = GTK_WIDGET (self);
735 GtkCssNode *widget_node;
736
737 iconic = !!iconic;
738 if (self->iconic == iconic)
739 return;
740
741 self->iconic = iconic;
742
743 widget_node = gtk_widget_get_css_node (widget);
744 if (iconic)
745 {
746 gtk_widget_hide (widget: self->start_box);
747 gtk_css_node_set_name (cssnode: widget_node, name: g_quark_from_static_string (string: "button"));
748 gtk_widget_add_css_class (widget, css_class: "model");
749 gtk_widget_add_css_class (widget, css_class: "image-button");
750 gtk_widget_remove_css_class (widget, css_class: "flat");
751 }
752 else
753 {
754 gtk_widget_show (widget: self->start_box);
755 gtk_css_node_set_name (cssnode: widget_node, name: g_quark_from_static_string (string: "modelbutton"));
756 gtk_widget_remove_css_class (widget, css_class: "model");
757 gtk_widget_remove_css_class (widget, css_class: "image-button");
758 gtk_widget_add_css_class (widget, css_class: "flat");
759 }
760
761 if (!iconic)
762 {
763 if (self->start_indicator)
764 {
765 gtk_box_remove (GTK_BOX (self->start_box), child: self->start_indicator);
766 self->start_indicator = NULL;
767 }
768 g_clear_pointer (&self->end_indicator, gtk_widget_unparent);
769 }
770
771 update_node_name (self);
772 update_visibility (self);
773 g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_ICONIC]);
774}
775
776static void
777gtk_model_button_set_popover (GtkModelButton *button,
778 GtkWidget *popover)
779{
780 if (button->popover)
781 gtk_widget_unparent (widget: button->popover);
782
783 button->popover = popover;
784
785 if (button->popover)
786 {
787 gtk_widget_set_parent (widget: button->popover, GTK_WIDGET (button));
788 gtk_popover_set_position (GTK_POPOVER (button->popover), position: GTK_POS_RIGHT);
789 }
790
791 update_accessible_properties (button);
792
793 update_node_name (self: button);
794 gtk_model_button_update_state (self: button);
795
796 gtk_widget_queue_resize (GTK_WIDGET (button));
797 g_object_notify_by_pspec (G_OBJECT (button), pspec: properties[PROP_POPOVER]);
798}
799
800static void
801update_accel (GtkModelButton *self,
802 const char *accel)
803{
804 if (accel)
805 {
806 guint key;
807 GdkModifierType mods;
808 char *str;
809
810 if (!self->accel_label)
811 {
812 self->accel_label = g_object_new (GTK_TYPE_LABEL,
813 first_property_name: "css-name", "accelerator",
814 NULL);
815 gtk_widget_insert_before (widget: self->accel_label, GTK_WIDGET (self), NULL);
816 }
817
818 gtk_accelerator_parse (accelerator: accel, accelerator_key: &key, accelerator_mods: &mods);
819 str = gtk_accelerator_get_label (accelerator_key: key, accelerator_mods: mods);
820 gtk_label_set_label (GTK_LABEL (self->accel_label), str);
821 g_free (mem: str);
822
823 if (GTK_IS_POPOVER (gtk_widget_get_native (GTK_WIDGET (self))))
824 {
825 GtkShortcutTrigger *trigger;
826 GtkShortcutAction *action;
827
828 if (self->controller)
829 {
830 while (g_list_model_get_n_items (list: G_LIST_MODEL (ptr: self->controller)) > 0)
831 {
832 GtkShortcut *shortcut = g_list_model_get_item (list: G_LIST_MODEL (ptr: self->controller), position: 0);
833 gtk_shortcut_controller_remove_shortcut (GTK_SHORTCUT_CONTROLLER (self->controller),
834 shortcut);
835 g_object_unref (object: shortcut);
836 }
837 }
838 else
839 {
840 self->controller = gtk_shortcut_controller_new ();
841 gtk_shortcut_controller_set_scope (GTK_SHORTCUT_CONTROLLER (self->controller), scope: GTK_SHORTCUT_SCOPE_MANAGED);
842 gtk_widget_add_controller (GTK_WIDGET (self), controller: self->controller);
843 }
844
845 trigger = gtk_keyval_trigger_new (keyval: key, modifiers: mods);
846 action = gtk_signal_action_new (signal_name: "clicked");
847 gtk_shortcut_controller_add_shortcut (GTK_SHORTCUT_CONTROLLER (self->controller),
848 shortcut: gtk_shortcut_new (trigger, action));
849 }
850 }
851 else
852 {
853 g_clear_pointer (&self->accel_label, gtk_widget_unparent);
854 if (self->controller)
855 {
856 gtk_widget_remove_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (self->controller));
857 g_clear_object (&self->controller);
858 }
859 }
860}
861
862static void
863gtk_model_button_set_accel (GtkModelButton *button,
864 const char *accel)
865{
866 g_free (mem: button->accel);
867 button->accel = g_strdup (str: accel);
868 update_accel (self: button, accel: button->accel);
869 update_visibility (self: button);
870
871 g_object_notify_by_pspec (G_OBJECT (button), pspec: properties[PROP_ACCEL]);
872}
873
874static void
875gtk_model_button_get_property (GObject *object,
876 guint prop_id,
877 GValue *value,
878 GParamSpec *pspec)
879{
880 GtkModelButton *self = GTK_MODEL_BUTTON (object);
881
882 switch (prop_id)
883 {
884 case PROP_ROLE:
885 g_value_set_enum (value, v_enum: self->role);
886 break;
887
888 case PROP_ICON:
889 g_value_set_object (value, v_object: self->image ? gtk_image_get_gicon (GTK_IMAGE (self->image)) : NULL);
890 break;
891
892 case PROP_TEXT:
893 g_value_set_string (value, v_string: gtk_label_get_text (GTK_LABEL (self->label)));
894 break;
895
896 case PROP_USE_MARKUP:
897 g_value_set_boolean (value, v_boolean: gtk_label_get_use_markup (GTK_LABEL (self->label)));
898 break;
899
900 case PROP_ACTIVE:
901 g_value_set_boolean (value, v_boolean: self->active);
902 break;
903
904 case PROP_MENU_NAME:
905 g_value_set_string (value, v_string: self->menu_name);
906 break;
907
908 case PROP_POPOVER:
909 g_value_set_object (value, v_object: self->popover);
910 break;
911
912 case PROP_ICONIC:
913 g_value_set_boolean (value, v_boolean: self->iconic);
914 break;
915
916 case PROP_ACCEL:
917 g_value_set_string (value, v_string: self->accel);
918 break;
919
920 case PROP_INDICATOR_SIZE_GROUP:
921 g_value_set_object (value, v_object: self->indicators);
922 break;
923
924 case PROP_ACTION_NAME:
925 g_value_set_string (value, v_string: gtk_action_helper_get_action_name (helper: self->action_helper));
926 break;
927
928 case PROP_ACTION_TARGET:
929 g_value_set_variant (value, variant: gtk_action_helper_get_action_target_value (helper: self->action_helper));
930 break;
931
932 default:
933 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
934 break;
935 }
936}
937
938static void
939gtk_model_button_set_property (GObject *object,
940 guint prop_id,
941 const GValue *value,
942 GParamSpec *pspec)
943{
944 GtkModelButton *button = GTK_MODEL_BUTTON (object);
945
946 switch (prop_id)
947 {
948 case PROP_ROLE:
949 gtk_model_button_set_role (self: button, role: g_value_get_enum (value));
950 break;
951
952 case PROP_ICON:
953 gtk_model_button_set_icon (self: button, icon: g_value_get_object (value));
954 break;
955
956 case PROP_TEXT:
957 gtk_model_button_set_text (button, text: g_value_get_string (value));
958 break;
959
960 case PROP_USE_MARKUP:
961 gtk_model_button_set_use_markup (button, use_markup: g_value_get_boolean (value));
962 break;
963
964 case PROP_ACTIVE:
965 gtk_model_button_set_active (button, active: g_value_get_boolean (value));
966 break;
967
968 case PROP_MENU_NAME:
969 gtk_model_button_set_menu_name (button, menu_name: g_value_get_string (value));
970 break;
971
972 case PROP_POPOVER:
973 gtk_model_button_set_popover (button, popover: (GtkWidget *)g_value_get_object (value));
974 break;
975
976 case PROP_ICONIC:
977 gtk_model_button_set_iconic (self: button, iconic: g_value_get_boolean (value));
978 break;
979
980 case PROP_ACCEL:
981 gtk_model_button_set_accel (button, accel: g_value_get_string (value));
982 break;
983
984 case PROP_INDICATOR_SIZE_GROUP:
985 if (button->indicators)
986 gtk_size_group_remove_widget (size_group: button->indicators, widget: button->start_box);
987 button->indicators = GTK_SIZE_GROUP (g_value_get_object (value));
988 if (button->indicators)
989 gtk_size_group_add_widget (size_group: button->indicators, widget: button->start_box);
990 break;
991
992 case PROP_ACTION_NAME:
993 gtk_model_button_set_action_name (GTK_ACTIONABLE (button), action_name: g_value_get_string (value));
994 break;
995
996 case PROP_ACTION_TARGET:
997 gtk_model_button_set_action_target_value (GTK_ACTIONABLE (button), action_target: g_value_get_variant (value));
998 break;
999
1000 default:
1001 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
1002 break;
1003 }
1004}
1005
1006static void
1007gtk_model_button_dispose (GObject *object)
1008{
1009 GtkModelButton *model_button = GTK_MODEL_BUTTON (object);
1010
1011 g_clear_pointer (&model_button->menu_name, g_free);
1012
1013 G_OBJECT_CLASS (gtk_model_button_parent_class)->dispose (object);
1014}
1015
1016static void
1017switch_menu (GtkModelButton *button)
1018{
1019 GtkWidget *stack;
1020
1021 stack = gtk_widget_get_ancestor (GTK_WIDGET (button), GTK_TYPE_STACK);
1022 if (stack != NULL)
1023 gtk_stack_set_visible_child_name (GTK_STACK (stack), name: button->menu_name);
1024}
1025
1026static void
1027gtk_model_button_clicked (GtkModelButton *self)
1028{
1029 if (self->menu_name != NULL)
1030 {
1031 switch_menu (button: self);
1032 }
1033 else if (self->popover != NULL)
1034 {
1035 GtkPopoverMenu *menu;
1036 GtkWidget *submenu;
1037
1038 menu = (GtkPopoverMenu *)gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_POPOVER_MENU);
1039 submenu = self->popover;
1040 gtk_popover_popup (GTK_POPOVER (submenu));
1041 gtk_popover_menu_set_open_submenu (menu, submenu);
1042 gtk_popover_menu_set_parent_menu (GTK_POPOVER_MENU (submenu), GTK_WIDGET (menu));
1043 }
1044 else if (!self->keep_open)
1045 {
1046 GtkWidget *popover;
1047
1048 popover = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_POPOVER);
1049 if (popover)
1050 gtk_popover_popdown (GTK_POPOVER (popover));
1051 }
1052
1053 if (self->action_helper)
1054 gtk_action_helper_activate (helper: self->action_helper);
1055}
1056
1057static gboolean
1058toggle_cb (GtkWidget *widget,
1059 GVariant *args,
1060 gpointer user_data)
1061{
1062 GtkModelButton *self = GTK_MODEL_BUTTON (widget);
1063
1064 self->keep_open = self->role != GTK_BUTTON_ROLE_NORMAL;
1065 g_signal_emit (instance: widget, signal_id: signals[SIGNAL_CLICKED], detail: 0);
1066 self->keep_open = FALSE;
1067
1068 return TRUE;
1069}
1070
1071static void
1072gtk_model_button_finalize (GObject *object)
1073{
1074 GtkModelButton *button = GTK_MODEL_BUTTON (object);
1075
1076 g_clear_pointer (&button->image, gtk_widget_unparent);
1077 g_clear_pointer (&button->label, gtk_widget_unparent);
1078 g_clear_pointer (&button->start_box, gtk_widget_unparent);
1079 g_clear_pointer (&button->accel_label, gtk_widget_unparent);
1080 g_clear_pointer (&button->end_indicator, gtk_widget_unparent);
1081 g_clear_object (&button->action_helper);
1082 g_free (mem: button->accel);
1083 g_clear_pointer (&button->popover, gtk_widget_unparent);
1084
1085 if (button->open_timeout)
1086 g_source_remove (tag: button->open_timeout);
1087
1088 G_OBJECT_CLASS (gtk_model_button_parent_class)->finalize (object);
1089}
1090
1091static gboolean
1092gtk_model_button_focus (GtkWidget *widget,
1093 GtkDirectionType direction)
1094{
1095 GtkModelButton *button = GTK_MODEL_BUTTON (widget);
1096
1097 if (gtk_widget_is_focus (widget))
1098 {
1099 if (direction == GTK_DIR_LEFT &&
1100 button->role == GTK_BUTTON_ROLE_TITLE &&
1101 button->menu_name != NULL)
1102 {
1103 switch_menu (button);
1104 return TRUE;
1105 }
1106 else if (direction == GTK_DIR_RIGHT &&
1107 button->role == GTK_BUTTON_ROLE_NORMAL &&
1108 button->menu_name != NULL)
1109 {
1110 switch_menu (button);
1111 return TRUE;
1112 }
1113 else if (direction == GTK_DIR_RIGHT &&
1114 button->role == GTK_BUTTON_ROLE_NORMAL &&
1115 button->popover != NULL)
1116 {
1117 GtkPopoverMenu *menu;
1118 GtkWidget *submenu;
1119
1120 menu = GTK_POPOVER_MENU (gtk_widget_get_ancestor (GTK_WIDGET (button), GTK_TYPE_POPOVER_MENU));
1121 submenu = button->popover;
1122 gtk_popover_popup (GTK_POPOVER (submenu));
1123 gtk_popover_menu_set_open_submenu (menu, submenu);
1124 gtk_popover_menu_set_parent_menu (GTK_POPOVER_MENU (submenu), GTK_WIDGET (menu));
1125 return TRUE;
1126 }
1127 }
1128 else
1129 {
1130 gtk_widget_grab_focus (widget);
1131 return TRUE;
1132 }
1133
1134 return FALSE;
1135}
1136
1137static void
1138gtk_model_button_class_init (GtkModelButtonClass *class)
1139{
1140 GObjectClass *object_class = G_OBJECT_CLASS (class);
1141 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
1142 GtkShortcutAction *action;
1143 guint activate_keyvals[] = {
1144 GDK_KEY_Return, GDK_KEY_ISO_Enter, GDK_KEY_KP_Enter
1145 };
1146 guint toggle_keyvals[] = {
1147 GDK_KEY_space, GDK_KEY_KP_Space
1148 };
1149 int i;
1150
1151 object_class->dispose = gtk_model_button_dispose;
1152 object_class->finalize = gtk_model_button_finalize;
1153 object_class->get_property = gtk_model_button_get_property;
1154 object_class->set_property = gtk_model_button_set_property;
1155
1156 widget_class->state_flags_changed = gtk_model_button_state_flags_changed;
1157 widget_class->direction_changed = gtk_model_button_direction_changed;
1158 widget_class->focus = gtk_model_button_focus;
1159
1160 class->clicked = gtk_model_button_clicked;
1161
1162 /**
1163 * GtkModelButton:role:
1164 *
1165 * Specifies whether the button is a plain, check or radio button.
1166 * When GtkActionable:action-name is set, the role will be determined
1167 * from the action and does not have to be set explicitly.
1168 */
1169 properties[PROP_ROLE] =
1170 g_param_spec_enum (name: "role",
1171 P_("Role"),
1172 P_("The role of this button"),
1173 GTK_TYPE_BUTTON_ROLE,
1174 default_value: GTK_BUTTON_ROLE_NORMAL,
1175 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
1176
1177 /**
1178 * GtkModelButton:icon:
1179 *
1180 * A GIcon that will be used if iconic appearance for the button is
1181 * desired.
1182 */
1183 properties[PROP_ICON] =
1184 g_param_spec_object (name: "icon",
1185 P_("Icon"),
1186 P_("The icon"),
1187 G_TYPE_ICON,
1188 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
1189
1190 /**
1191 * GtkModelButton:text:
1192 *
1193 * The label for the button.
1194 */
1195 properties[PROP_TEXT] =
1196 g_param_spec_string (name: "text",
1197 P_("Text"),
1198 P_("The text"),
1199 default_value: "",
1200 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
1201
1202 /**
1203 * GtkModelButton:use-markup:
1204 *
1205 * If %TRUE, XML tags in the text of the button are interpreted as by
1206 * pango_parse_markup() to format the enclosed spans of text. If %FALSE, the
1207 * text will be displayed verbatim.
1208 */
1209 properties[PROP_USE_MARKUP] =
1210 g_param_spec_boolean (name: "use-markup",
1211 P_("Use markup"),
1212 P_("The text of the button includes XML markup. See pango_parse_markup()"),
1213 FALSE,
1214 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
1215
1216 /**
1217 * GtkModelButton:active:
1218 *
1219 * The state of the button. This is reflecting the state of the associated
1220 * GAction.
1221 */
1222 properties[PROP_ACTIVE] =
1223 g_param_spec_boolean (name: "active",
1224 P_("Active"),
1225 P_("Active"),
1226 FALSE,
1227 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
1228
1229 /**
1230 * GtkModelButton:menu-name:
1231 *
1232 * The name of a submenu to open when the button is activated. * If this is set, the button should not have an action associated with it.
1233 */
1234 properties[PROP_MENU_NAME] =
1235 g_param_spec_string (name: "menu-name",
1236 P_("Menu name"),
1237 P_("The name of the menu to open"),
1238 NULL,
1239 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
1240
1241 properties[PROP_POPOVER] =
1242 g_param_spec_object (name: "popover",
1243 P_("Popover"),
1244 P_("Popover to open"),
1245 GTK_TYPE_POPOVER,
1246 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
1247
1248 /**
1249 * GtkModelButton:iconic:
1250 *
1251 * If this property is set, the button will show an icon if one is set.
1252 * If no icon is set, the text will be used. This is typically used for
1253 * horizontal sections of linked buttons.
1254 */
1255 properties[PROP_ICONIC] =
1256 g_param_spec_boolean (name: "iconic",
1257 P_("Iconic"),
1258 P_("Whether to prefer the icon over text"),
1259 FALSE,
1260 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
1261
1262 /**
1263 * GtkModelButton:indicator-size-group:
1264 *
1265 * Containers like GtkPopoverMenu can provide a size group
1266 * in this property to align the checks and radios of all
1267 * the model buttons in a menu.
1268 */
1269 properties[PROP_INDICATOR_SIZE_GROUP] =
1270 g_param_spec_object (name: "indicator-size-group",
1271 P_("Size group"),
1272 P_("Size group for checks and radios"),
1273 GTK_TYPE_SIZE_GROUP,
1274 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
1275 properties[PROP_ACCEL] =
1276 g_param_spec_string (name: "accel",
1277 P_("Accel"),
1278 P_("The accelerator"),
1279 NULL,
1280 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
1281 g_object_class_install_properties (oclass: object_class, n_pspecs: LAST_PROP, pspecs: properties);
1282
1283 g_object_class_override_property (oclass: object_class, property_id: PROP_ACTION_NAME, name: "action-name");
1284 g_object_class_override_property (oclass: object_class, property_id: PROP_ACTION_TARGET, name: "action-target");
1285
1286 signals[SIGNAL_CLICKED] = g_signal_new (I_("clicked"),
1287 G_OBJECT_CLASS_TYPE (object_class),
1288 signal_flags: G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
1289 G_STRUCT_OFFSET (GtkModelButtonClass, clicked),
1290 NULL, NULL,
1291 NULL,
1292 G_TYPE_NONE, n_params: 0);
1293
1294 gtk_widget_class_set_activate_signal (widget_class, signal_id: signals[SIGNAL_CLICKED]);
1295 gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BOX_LAYOUT);
1296 gtk_widget_class_set_css_name (widget_class, I_("modelbutton"));
1297 gtk_widget_class_set_accessible_role (widget_class, accessible_role: GTK_ACCESSIBLE_ROLE_MENU_ITEM);
1298
1299 action = gtk_signal_action_new (signal_name: "clicked");
1300 for (i = 0; i < G_N_ELEMENTS (activate_keyvals); i++)
1301 {
1302 GtkShortcut *shortcut;
1303
1304 shortcut = gtk_shortcut_new (trigger: gtk_keyval_trigger_new (keyval: activate_keyvals[i], modifiers: 0),
1305 g_object_ref (action));
1306 gtk_widget_class_add_shortcut (widget_class, shortcut);
1307 g_object_unref (object: shortcut);
1308 }
1309 g_object_unref (object: action);
1310
1311 action = gtk_callback_action_new (callback: toggle_cb, NULL, NULL);
1312 for (i = 0; i < G_N_ELEMENTS (toggle_keyvals); i++)
1313 {
1314 GtkShortcut *shortcut;
1315
1316 shortcut = gtk_shortcut_new (trigger: gtk_keyval_trigger_new (keyval: toggle_keyvals[i], modifiers: 0),
1317 g_object_ref (action));
1318 gtk_widget_class_add_shortcut (widget_class, shortcut);
1319 g_object_unref (object: shortcut);
1320 }
1321 g_object_unref (object: action);
1322}
1323
1324static gboolean
1325open_submenu (gpointer data)
1326{
1327 GtkModelButton *button = data;
1328 GtkPopover *popover;
1329
1330 popover = (GtkPopover*)gtk_widget_get_ancestor (GTK_WIDGET (button), GTK_TYPE_POPOVER);
1331
1332 if (GTK_IS_POPOVER_MENU (popover))
1333 {
1334 gtk_popover_menu_set_active_item (GTK_POPOVER_MENU (popover), GTK_WIDGET (button));
1335
1336 if (button->popover)
1337 {
1338 GtkWidget *submenu = button->popover;
1339
1340 if (gtk_popover_menu_get_open_submenu (GTK_POPOVER_MENU (popover)) != submenu)
1341 gtk_popover_menu_close_submenus (GTK_POPOVER_MENU (popover));
1342
1343 gtk_popover_popup (GTK_POPOVER (submenu));
1344 gtk_popover_menu_set_open_submenu (GTK_POPOVER_MENU (popover), submenu);
1345 gtk_popover_menu_set_parent_menu (GTK_POPOVER_MENU (submenu), GTK_WIDGET (popover));
1346 }
1347 }
1348
1349 button->open_timeout = 0;
1350
1351 return G_SOURCE_REMOVE;
1352}
1353
1354#define OPEN_TIMEOUT 80
1355
1356static void
1357start_open (GtkModelButton *button)
1358{
1359 if (button->open_timeout)
1360 g_source_remove (tag: button->open_timeout);
1361
1362 if (button->popover &&
1363 gtk_widget_get_visible (widget: button->popover))
1364 return;
1365
1366 button->open_timeout = g_timeout_add (OPEN_TIMEOUT, function: open_submenu, data: button);
1367 gdk_source_set_static_name_by_id (tag: button->open_timeout, name: "[gtk] open_submenu");
1368}
1369
1370static void
1371stop_open (GtkModelButton *button)
1372{
1373 if (button->open_timeout)
1374 {
1375 g_source_remove (tag: button->open_timeout);
1376 button->open_timeout = 0;
1377 }
1378}
1379
1380static void
1381pointer_cb (GObject *object,
1382 GParamSpec *pspec,
1383 gpointer data)
1384{
1385 GtkWidget *target = GTK_WIDGET (data);
1386 GtkWidget *popover;
1387 gboolean contains;
1388
1389 contains = gtk_event_controller_motion_contains_pointer (GTK_EVENT_CONTROLLER_MOTION (object));
1390
1391 popover = gtk_widget_get_ancestor (widget: target, GTK_TYPE_POPOVER_MENU);
1392
1393 if (contains)
1394 {
1395 if (popover)
1396 {
1397 if (gtk_popover_menu_get_open_submenu (GTK_POPOVER_MENU (popover)) != NULL)
1398 start_open (GTK_MODEL_BUTTON (target));
1399 else
1400 open_submenu (data: target);
1401 }
1402 }
1403 else
1404 {
1405 GtkModelButton *button = data;
1406
1407 stop_open (button);
1408 if (popover)
1409 gtk_popover_menu_set_active_item (GTK_POPOVER_MENU (popover), NULL);
1410 }
1411}
1412
1413static void
1414motion_cb (GtkEventController *controller,
1415 double x,
1416 double y,
1417 gpointer data)
1418{
1419 start_open (GTK_MODEL_BUTTON (data));
1420}
1421
1422static void
1423focus_in_cb (GtkEventController *controller,
1424 gpointer data)
1425{
1426 GtkWidget *target;
1427 GtkWidget *popover;
1428
1429 target = gtk_event_controller_get_widget (controller);
1430 popover = gtk_widget_get_ancestor (widget: target, GTK_TYPE_POPOVER_MENU);
1431
1432 if (popover)
1433 gtk_popover_menu_set_active_item (GTK_POPOVER_MENU (popover), item: target);
1434}
1435
1436static void
1437gesture_pressed (GtkGestureClick *gesture,
1438 guint n_press,
1439 double x,
1440 double y,
1441 GtkWidget *widget)
1442{
1443 GdkEventSequence *sequence;
1444
1445 if (gtk_widget_get_focus_on_click (widget) && !gtk_widget_has_focus (widget))
1446 gtk_widget_grab_focus (widget);
1447
1448 sequence = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
1449 gtk_gesture_set_sequence_state (GTK_GESTURE (gesture), sequence, state: GTK_EVENT_SEQUENCE_CLAIMED);
1450}
1451
1452static void
1453emit_clicked (GtkModelButton *button)
1454{
1455 g_signal_emit (instance: button, signal_id: signals[SIGNAL_CLICKED], detail: 0);
1456}
1457
1458static void
1459gtk_model_button_init (GtkModelButton *self)
1460{
1461 GtkEventController *controller;
1462 GtkGesture *gesture;
1463
1464 gtk_widget_set_focusable (GTK_WIDGET (self), TRUE);
1465
1466 self->role = GTK_BUTTON_ROLE_NORMAL;
1467 self->label = gtk_label_new (str: "");
1468 gtk_widget_set_halign (widget: self->label, align: GTK_ALIGN_START);
1469 gtk_widget_set_parent (widget: self->label, GTK_WIDGET (self));
1470
1471 self->start_box = gtk_box_new (orientation: GTK_ORIENTATION_HORIZONTAL, spacing: 0);
1472 gtk_widget_insert_after (widget: self->start_box, GTK_WIDGET (self), NULL);
1473 update_node_ordering (button: self);
1474
1475 gtk_widget_add_css_class (GTK_WIDGET (self), css_class: "flat");
1476
1477 controller = gtk_event_controller_motion_new ();
1478 g_signal_connect (controller, "notify::contains-pointer", G_CALLBACK (pointer_cb), self);
1479 g_signal_connect (controller, "motion", G_CALLBACK (motion_cb), self);
1480 gtk_widget_add_controller (GTK_WIDGET (self), controller);
1481
1482 controller = gtk_event_controller_focus_new ();
1483 gtk_event_controller_set_propagation_limit (controller, limit: GTK_LIMIT_NONE);
1484 g_signal_connect (controller, "enter", G_CALLBACK (focus_in_cb), NULL);
1485 gtk_widget_add_controller (GTK_WIDGET (self), controller);
1486
1487 gesture = gtk_gesture_click_new ();
1488 gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (gesture), FALSE);
1489 gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (gesture), TRUE);
1490 gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), GDK_BUTTON_PRIMARY);
1491 g_signal_connect (gesture, "pressed", G_CALLBACK (gesture_pressed), self);
1492 g_signal_connect_swapped (gesture, "released", G_CALLBACK (emit_clicked), self);
1493 g_signal_connect_swapped (gesture, "unpaired-release", G_CALLBACK (emit_clicked), self);
1494 gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (gesture), phase: GTK_PHASE_CAPTURE);
1495 gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (gesture));
1496}
1497
1498/**
1499 * gtk_model_button_new:
1500 *
1501 * Creates a new GtkModelButton.
1502 *
1503 * Returns: the newly created GtkModelButton widget
1504 */
1505GtkWidget *
1506gtk_model_button_new (void)
1507{
1508 return g_object_new (GTK_TYPE_MODEL_BUTTON, NULL);
1509}
1510

source code of gtk/gtk/gtkmodelbutton.c