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 | |
158 | struct _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 *; |
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 | |
183 | typedef struct _GtkModelButtonClass GtkModelButtonClass; |
184 | |
185 | struct _GtkModelButtonClass |
186 | { |
187 | GtkWidgetClass parent_class; |
188 | |
189 | void (* clicked) (GtkModelButton *button); |
190 | }; |
191 | |
192 | static void gtk_model_button_actionable_iface_init (GtkActionableInterface *iface); |
193 | |
194 | G_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 | |
197 | GType |
198 | gtk_button_role_get_type (void) |
199 | { |
200 | static gsize gtk_button_role_type; |
201 | |
202 | if (g_once_init_enter (>k_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 (>k_button_role_type, type); |
216 | } |
217 | |
218 | return gtk_button_role_type; |
219 | } |
220 | |
221 | enum |
222 | { |
223 | PROP_0, |
224 | PROP_ROLE, |
225 | PROP_ICON, |
226 | PROP_TEXT, |
227 | PROP_USE_MARKUP, |
228 | PROP_ACTIVE, |
229 | , |
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 | |
241 | enum |
242 | { |
243 | SIGNAL_CLICKED, |
244 | LAST_SIGNAL |
245 | }; |
246 | |
247 | static GParamSpec *properties[LAST_PROP] = { NULL, }; |
248 | static guint signals[LAST_SIGNAL] = { 0 }; |
249 | |
250 | static void |
251 | gtk_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 | |
262 | static void |
263 | gtk_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 | |
274 | static const char * |
275 | gtk_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 | |
282 | static GVariant * |
283 | gtk_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 | |
290 | static void |
291 | gtk_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 | |
299 | static void |
300 | update_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 | |
335 | static void |
336 | update_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 | |
387 | static void |
388 | update_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 | |
407 | static GtkStateFlags |
408 | get_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 | |
424 | static void |
425 | update_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 | |
447 | static void |
448 | gtk_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 | |
460 | static void |
461 | gtk_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 | |
469 | static void |
470 | gtk_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 | |
481 | static void |
482 | update_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 | |
558 | static void |
559 | update_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 | |
591 | static void |
592 | gtk_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 | |
620 | static void |
621 | update_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 | |
644 | static void |
645 | gtk_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 | |
669 | static void |
670 | gtk_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 | |
684 | static void |
685 | gtk_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 | |
697 | static void |
698 | gtk_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 | |
714 | static void |
715 | (GtkModelButton *button, |
716 | const char *) |
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 | |
730 | static void |
731 | gtk_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 | |
776 | static void |
777 | gtk_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 | |
800 | static void |
801 | update_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 | |
862 | static void |
863 | gtk_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 | |
874 | static void |
875 | gtk_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 | |
938 | static void |
939 | gtk_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 | |
1006 | static void |
1007 | gtk_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 | |
1016 | static void |
1017 | (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 | |
1026 | static void |
1027 | gtk_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 *; |
1036 | GtkWidget *; |
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 | |
1057 | static gboolean |
1058 | toggle_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 | |
1071 | static void |
1072 | gtk_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 | |
1091 | static gboolean |
1092 | gtk_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 *; |
1118 | GtkWidget *; |
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 | |
1137 | static void |
1138 | gtk_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 | |
1324 | static gboolean |
1325 | (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 * = 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 | |
1356 | static void |
1357 | start_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 | |
1370 | static void |
1371 | stop_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 | |
1380 | static void |
1381 | pointer_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 | |
1413 | static void |
1414 | motion_cb (GtkEventController *controller, |
1415 | double x, |
1416 | double y, |
1417 | gpointer data) |
1418 | { |
1419 | start_open (GTK_MODEL_BUTTON (data)); |
1420 | } |
1421 | |
1422 | static void |
1423 | focus_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 | |
1436 | static void |
1437 | gesture_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 | |
1452 | static void |
1453 | emit_clicked (GtkModelButton *button) |
1454 | { |
1455 | g_signal_emit (instance: button, signal_id: signals[SIGNAL_CLICKED], detail: 0); |
1456 | } |
1457 | |
1458 | static void |
1459 | gtk_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 | */ |
1505 | GtkWidget * |
1506 | gtk_model_button_new (void) |
1507 | { |
1508 | return g_object_new (GTK_TYPE_MODEL_BUTTON, NULL); |
1509 | } |
1510 | |