1/*
2 * Copyright © 2013 Canonical Limited
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: Ryan Lortie <desrt@desrt.ca>
18 */
19
20#include "config.h"
21
22#include "gtkmenutrackeritemprivate.h"
23#include "gtkactionmuxerprivate.h"
24#include "gtkdebug.h"
25#include "gtkintl.h"
26
27#include <string.h>
28
29/*< private >
30 * GtkMenuTrackerItem:
31 *
32 * A GtkMenuTrackerItem is a small helper class used by GtkMenuTracker to
33 * represent menu items. It has one of three classes: normal item, separator,
34 * or submenu.
35 *
36 * If an item is one of the non-normal classes (submenu, separator), only the
37 * label of the item needs to be respected. Otherwise, all the properties
38 * of the item contribute to the item’s appearance and state.
39 *
40 * Implementing the appearance of the menu item is up to toolkits, and certain
41 * toolkits may choose to ignore certain properties, like icon or accel. The
42 * role of the item determines its accessibility role, along with its
43 * decoration if the GtkMenuTrackerItem::toggled property is true. As an
44 * example, if the item has the role %GTK_MENU_TRACKER_ITEM_ROLE_CHECK and
45 * GtkMenuTrackerItem::toggled is %FALSE, its accessible role should be that of
46 * a check menu item, and no decoration should be drawn. But if
47 * GtkMenuTrackerItem::toggled is %TRUE, a checkmark should be drawn.
48 *
49 * All properties except for the two class-determining properties,
50 * GtkMenuTrackerItem::is-separator and GtkMenuTrackerItem::has-submenu are
51 * allowed to change, so listen to the notify signals to update your item's
52 * appearance. When using a GObject library, this can conveniently be done
53 * with g_object_bind_property() and GBinding, and this is how this is
54 * implemented in GTK; the appearance side is implemented in GtkModelMenuItem.
55 *
56 * When an item is clicked, simply call gtk_menu_tracker_item_activated() in
57 * response. The GtkMenuTrackerItem will take care of everything related to
58 * activating the item and will itself update the state of all items in
59 * response.
60 *
61 * Submenus are a special case of menu item. When an item is a submenu, you
62 * should create a submenu for it with gtk_menu_tracker_new_item_for_submenu(),
63 * and apply the same menu tracking logic you would for a toplevel menu.
64 * Applications using submenus may want to lazily build their submenus in
65 * response to the user clicking on it, as building a submenu may be expensive.
66 *
67 * Thus, the submenu has two special controls -- the submenu’s visibility
68 * should be controlled by the GtkMenuTrackerItem::submenu-shown property,
69 * and if a user clicks on the submenu, do not immediately show the menu,
70 * but call gtk_menu_tracker_item_request_submenu_shown() and wait for the
71 * GtkMenuTrackerItem::submenu-shown property to update. If the user navigates,
72 * the application may want to be notified so it can cancel the expensive
73 * operation that it was using to build the submenu. Thus,
74 * gtk_menu_tracker_item_request_submenu_shown() takes a boolean parameter.
75 * Use %TRUE when the user wants to open the submenu, and %FALSE when the
76 * user wants to close the submenu.
77 */
78
79typedef GObjectClass GtkMenuTrackerItemClass;
80
81struct _GtkMenuTrackerItem
82{
83 GObject parent_instance;
84
85 GtkActionObservable *observable;
86 char *action_namespace;
87 char *action_and_target;
88 GMenuItem *item;
89 GtkMenuTrackerItemRole role : 4;
90 guint is_separator : 1;
91 guint can_activate : 1;
92 guint sensitive : 1;
93 guint toggled : 1;
94 guint submenu_shown : 1;
95 guint submenu_requested : 1;
96 guint hidden_when : 2;
97 guint is_visible : 1;
98};
99
100#define HIDDEN_NEVER 0
101#define HIDDEN_WHEN_MISSING 1
102#define HIDDEN_WHEN_DISABLED 2
103#define HIDDEN_WHEN_ALWAYS 3
104
105enum {
106 PROP_0,
107 PROP_IS_SEPARATOR,
108 PROP_LABEL,
109 PROP_USE_MARKUP,
110 PROP_ICON,
111 PROP_VERB_ICON,
112 PROP_SENSITIVE,
113 PROP_ROLE,
114 PROP_TOGGLED,
115 PROP_ACCEL,
116 PROP_SUBMENU_SHOWN,
117 PROP_IS_VISIBLE,
118 N_PROPS
119};
120
121static GParamSpec *gtk_menu_tracker_item_pspecs[N_PROPS];
122
123static void gtk_menu_tracker_item_init_observer_iface (GtkActionObserverInterface *iface);
124G_DEFINE_TYPE_WITH_CODE (GtkMenuTrackerItem, gtk_menu_tracker_item, G_TYPE_OBJECT,
125 G_IMPLEMENT_INTERFACE (GTK_TYPE_ACTION_OBSERVER, gtk_menu_tracker_item_init_observer_iface))
126
127GType
128gtk_menu_tracker_item_role_get_type (void)
129{
130 static gsize gtk_menu_tracker_item_role_type;
131
132 if (g_once_init_enter (&gtk_menu_tracker_item_role_type))
133 {
134 static const GEnumValue values[] = {
135 { GTK_MENU_TRACKER_ITEM_ROLE_NORMAL, "GTK_MENU_TRACKER_ITEM_ROLE_NORMAL", "normal" },
136 { GTK_MENU_TRACKER_ITEM_ROLE_CHECK, "GTK_MENU_TRACKER_ITEM_ROLE_CHECK", "check" },
137 { GTK_MENU_TRACKER_ITEM_ROLE_RADIO, "GTK_MENU_TRACKER_ITEM_ROLE_RADIO", "radio" },
138 { 0, NULL, NULL }
139 };
140 GType type;
141
142 type = g_enum_register_static (I_("GtkMenuTrackerItemRole"), const_static_values: values);
143
144 g_once_init_leave (&gtk_menu_tracker_item_role_type, type);
145 }
146
147 return gtk_menu_tracker_item_role_type;
148}
149
150static void
151gtk_menu_tracker_item_get_property (GObject *object,
152 guint prop_id,
153 GValue *value,
154 GParamSpec *pspec)
155{
156 GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (object);
157
158 switch (prop_id)
159 {
160 case PROP_IS_SEPARATOR:
161 g_value_set_boolean (value, v_boolean: gtk_menu_tracker_item_get_is_separator (self));
162 break;
163 case PROP_LABEL:
164 g_value_set_string (value, v_string: gtk_menu_tracker_item_get_label (self));
165 break;
166 case PROP_USE_MARKUP:
167 g_value_set_boolean (value, v_boolean: gtk_menu_tracker_item_get_use_markup (self));
168 break;
169 case PROP_ICON:
170 g_value_take_object (value, v_object: gtk_menu_tracker_item_get_icon (self));
171 break;
172 case PROP_VERB_ICON:
173 g_value_take_object (value, v_object: gtk_menu_tracker_item_get_verb_icon (self));
174 break;
175 case PROP_SENSITIVE:
176 g_value_set_boolean (value, v_boolean: gtk_menu_tracker_item_get_sensitive (self));
177 break;
178 case PROP_ROLE:
179 g_value_set_enum (value, v_enum: gtk_menu_tracker_item_get_role (self));
180 break;
181 case PROP_TOGGLED:
182 g_value_set_boolean (value, v_boolean: gtk_menu_tracker_item_get_toggled (self));
183 break;
184 case PROP_ACCEL:
185 g_value_set_string (value, v_string: gtk_menu_tracker_item_get_accel (self));
186 break;
187 case PROP_SUBMENU_SHOWN:
188 g_value_set_boolean (value, v_boolean: gtk_menu_tracker_item_get_submenu_shown (self));
189 break;
190 case PROP_IS_VISIBLE:
191 g_value_set_boolean (value, v_boolean: gtk_menu_tracker_item_get_is_visible (self));
192 break;
193 default:
194 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
195 break;
196 }
197}
198
199static void
200gtk_menu_tracker_item_finalize (GObject *object)
201{
202 GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (object);
203
204 g_free (mem: self->action_namespace);
205 g_free (mem: self->action_and_target);
206
207 if (self->observable)
208 g_object_unref (object: self->observable);
209
210 g_object_unref (object: self->item);
211
212 G_OBJECT_CLASS (gtk_menu_tracker_item_parent_class)->finalize (object);
213}
214
215static void
216gtk_menu_tracker_item_init (GtkMenuTrackerItem * self)
217{
218}
219
220static void
221gtk_menu_tracker_item_class_init (GtkMenuTrackerItemClass *class)
222{
223 class->get_property = gtk_menu_tracker_item_get_property;
224 class->finalize = gtk_menu_tracker_item_finalize;
225
226 gtk_menu_tracker_item_pspecs[PROP_IS_SEPARATOR] =
227 g_param_spec_boolean (name: "is-separator", nick: "", blurb: "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
228 gtk_menu_tracker_item_pspecs[PROP_LABEL] =
229 g_param_spec_string (name: "label", nick: "", blurb: "", NULL, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
230 gtk_menu_tracker_item_pspecs[PROP_USE_MARKUP] =
231 g_param_spec_boolean (name: "use-markup", nick: "", blurb: "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
232 gtk_menu_tracker_item_pspecs[PROP_ICON] =
233 g_param_spec_object (name: "icon", nick: "", blurb: "", G_TYPE_ICON, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
234 gtk_menu_tracker_item_pspecs[PROP_VERB_ICON] =
235 g_param_spec_object (name: "verb-icon", nick: "", blurb: "", G_TYPE_ICON, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
236 gtk_menu_tracker_item_pspecs[PROP_SENSITIVE] =
237 g_param_spec_boolean (name: "sensitive", nick: "", blurb: "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
238 gtk_menu_tracker_item_pspecs[PROP_ROLE] =
239 g_param_spec_enum (name: "role", nick: "", blurb: "",
240 GTK_TYPE_MENU_TRACKER_ITEM_ROLE, default_value: GTK_MENU_TRACKER_ITEM_ROLE_NORMAL,
241 G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
242 gtk_menu_tracker_item_pspecs[PROP_TOGGLED] =
243 g_param_spec_boolean (name: "toggled", nick: "", blurb: "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
244 gtk_menu_tracker_item_pspecs[PROP_ACCEL] =
245 g_param_spec_string (name: "accel", nick: "", blurb: "", NULL, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
246 gtk_menu_tracker_item_pspecs[PROP_SUBMENU_SHOWN] =
247 g_param_spec_boolean (name: "submenu-shown", nick: "", blurb: "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
248 gtk_menu_tracker_item_pspecs[PROP_IS_VISIBLE] =
249 g_param_spec_boolean (name: "is-visible", nick: "", blurb: "", FALSE, G_PARAM_STATIC_STRINGS | G_PARAM_READABLE);
250
251 g_object_class_install_properties (oclass: class, n_pspecs: N_PROPS, pspecs: gtk_menu_tracker_item_pspecs);
252}
253
254/* This syncs up the visibility for the hidden-when='' case. We call it
255 * from the action observer functions on changes to the action group and
256 * on initialisation (via the action observer functions that are invoked
257 * at that time).
258 */
259static void
260gtk_menu_tracker_item_update_visibility (GtkMenuTrackerItem *self)
261{
262 gboolean visible;
263
264 switch (self->hidden_when)
265 {
266 case HIDDEN_NEVER:
267 visible = TRUE;
268 break;
269
270 case HIDDEN_WHEN_MISSING:
271 visible = self->can_activate;
272 break;
273
274 case HIDDEN_WHEN_DISABLED:
275 visible = self->sensitive;
276 break;
277
278 case HIDDEN_WHEN_ALWAYS:
279 visible = FALSE;
280 break;
281
282 default:
283 g_assert_not_reached ();
284 }
285
286 if (visible != self->is_visible)
287 {
288 self->is_visible = visible;
289 g_object_notify_by_pspec (G_OBJECT (self), pspec: gtk_menu_tracker_item_pspecs[PROP_IS_VISIBLE]);
290 }
291}
292
293static void
294gtk_menu_tracker_item_action_added (GtkActionObserver *observer,
295 GtkActionObservable *observable,
296 const char *action_name,
297 const GVariantType *parameter_type,
298 gboolean enabled,
299 GVariant *state)
300{
301 GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (observer);
302 GVariant *action_target;
303 gboolean old_sensitive;
304 gboolean old_toggled;
305 GtkMenuTrackerItemRole old_role;
306 guint n_changed;
307
308 GTK_NOTE(ACTIONS, g_message ("menutracker: action %s added", action_name));
309
310 old_sensitive = self->sensitive;
311 old_toggled = self->toggled;
312 old_role = self->role;
313
314 action_target = g_menu_item_get_attribute_value (menu_item: self->item, G_MENU_ATTRIBUTE_TARGET, NULL);
315
316 self->can_activate = (action_target == NULL && parameter_type == NULL) ||
317 (action_target != NULL && parameter_type != NULL &&
318 g_variant_is_of_type (value: action_target, type: parameter_type));
319
320 if (!self->can_activate)
321 {
322 GTK_NOTE(ACTIONS, g_message ("menutracker: action %s can't be activated due to parameter type mismatch "
323 "(parameter type %s, target type %s)",
324 action_name,
325 parameter_type ? g_variant_type_peek_string (parameter_type) : "NULL",
326 action_target ? g_variant_get_type_string (action_target) : "NULL"));
327
328 if (action_target)
329 g_variant_unref (value: action_target);
330 return;
331 }
332
333 GTK_NOTE(ACTIONS, g_message ("menutracker: action %s can be activated", action_name));
334
335 self->sensitive = enabled;
336
337 GTK_NOTE(ACTIONS, g_message ("menutracker: action %s is %s", action_name, enabled ? "enabled" : "disabled"));
338
339 if (action_target != NULL && state != NULL)
340 {
341 self->toggled = g_variant_equal (one: state, two: action_target);
342 self->role = GTK_MENU_TRACKER_ITEM_ROLE_RADIO;
343 }
344
345 else if (state != NULL && g_variant_is_of_type (value: state, G_VARIANT_TYPE_BOOLEAN))
346 {
347 self->toggled = g_variant_get_boolean (value: state);
348 self->role = GTK_MENU_TRACKER_ITEM_ROLE_CHECK;
349 }
350
351 /* Avoid freeze/thaw_notify as they are quite expensive in runtime/memory
352 * unless we have more than one property to update. Additionally, only
353 * notify on properties that have changed to avoid extraneous signal
354 * emission. This code can get run a lot!
355 */
356 n_changed = (old_role != self->role)
357 + (old_toggled != self->toggled)
358 + (old_sensitive != self->sensitive);
359
360 if (n_changed > 1)
361 g_object_freeze_notify (G_OBJECT (self));
362
363 if (self->sensitive != old_sensitive)
364 g_object_notify_by_pspec (G_OBJECT (self), pspec: gtk_menu_tracker_item_pspecs[PROP_SENSITIVE]);
365
366 if (self->toggled != old_toggled)
367 g_object_notify_by_pspec (G_OBJECT (self), pspec: gtk_menu_tracker_item_pspecs[PROP_TOGGLED]);
368
369 if (self->role != old_role)
370 g_object_notify_by_pspec (G_OBJECT (self), pspec: gtk_menu_tracker_item_pspecs[PROP_ROLE]);
371
372 if (n_changed > 1)
373 g_object_thaw_notify (G_OBJECT (self));
374
375 if (action_target)
376 g_variant_unref (value: action_target);
377
378 /* In case of hidden-when='', we want to Wait until after refreshing
379 * all of the properties to emit the signal that will cause the
380 * tracker to expose us (to prevent too much thrashing).
381 */
382 gtk_menu_tracker_item_update_visibility (self);
383}
384
385static void
386gtk_menu_tracker_item_action_enabled_changed (GtkActionObserver *observer,
387 GtkActionObservable *observable,
388 const char *action_name,
389 gboolean enabled)
390{
391 GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (observer);
392
393 GTK_NOTE(ACTIONS, g_message ("menutracker: action %s: enabled changed to %d", action_name, enabled));
394
395 if (!self->can_activate)
396 return;
397
398 if (self->sensitive == enabled)
399 return;
400
401 self->sensitive = enabled;
402
403 g_object_notify_by_pspec (G_OBJECT (self), pspec: gtk_menu_tracker_item_pspecs[PROP_SENSITIVE]);
404
405 gtk_menu_tracker_item_update_visibility (self);
406}
407
408static void
409gtk_menu_tracker_item_action_state_changed (GtkActionObserver *observer,
410 GtkActionObservable *observable,
411 const char *action_name,
412 GVariant *state)
413{
414 GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (observer);
415 GVariant *action_target;
416 gboolean was_toggled;
417
418 GTK_NOTE(ACTIONS, g_message ("menutracker: action %s: state changed", action_name));
419
420 if (!self->can_activate)
421 return;
422
423 action_target = g_menu_item_get_attribute_value (menu_item: self->item, G_MENU_ATTRIBUTE_TARGET, NULL);
424 was_toggled = self->toggled;
425
426 if (action_target)
427 {
428 self->toggled = g_variant_equal (one: state, two: action_target);
429 g_variant_unref (value: action_target);
430 }
431
432 else if (g_variant_is_of_type (value: state, G_VARIANT_TYPE_BOOLEAN))
433 self->toggled = g_variant_get_boolean (value: state);
434
435 else
436 self->toggled = FALSE;
437
438 if (self->toggled != was_toggled)
439 g_object_notify_by_pspec (G_OBJECT (self), pspec: gtk_menu_tracker_item_pspecs[PROP_TOGGLED]);
440}
441
442static void
443gtk_menu_tracker_item_action_removed (GtkActionObserver *observer,
444 GtkActionObservable *observable,
445 const char *action_name)
446{
447 GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (observer);
448 gboolean was_sensitive, was_toggled;
449 GtkMenuTrackerItemRole old_role;
450
451 GTK_NOTE(ACTIONS, g_message ("menutracker: action %s was removed", action_name));
452
453 if (!self->can_activate)
454 return;
455
456 was_sensitive = self->sensitive;
457 was_toggled = self->toggled;
458 old_role = self->role;
459
460 self->can_activate = FALSE;
461 self->sensitive = FALSE;
462 self->toggled = FALSE;
463 self->role = GTK_MENU_TRACKER_ITEM_ROLE_NORMAL;
464
465 /* Backwards from adding: we want to remove ourselves from the menu
466 * -before- thrashing the properties.
467 */
468 gtk_menu_tracker_item_update_visibility (self);
469
470 g_object_freeze_notify (G_OBJECT (self));
471
472 if (was_sensitive)
473 g_object_notify_by_pspec (G_OBJECT (self), pspec: gtk_menu_tracker_item_pspecs[PROP_SENSITIVE]);
474
475 if (was_toggled)
476 g_object_notify_by_pspec (G_OBJECT (self), pspec: gtk_menu_tracker_item_pspecs[PROP_TOGGLED]);
477
478 if (old_role != GTK_MENU_TRACKER_ITEM_ROLE_NORMAL)
479 g_object_notify_by_pspec (G_OBJECT (self), pspec: gtk_menu_tracker_item_pspecs[PROP_ROLE]);
480
481 g_object_thaw_notify (G_OBJECT (self));
482}
483
484static void
485gtk_menu_tracker_item_primary_accel_changed (GtkActionObserver *observer,
486 GtkActionObservable *observable,
487 const char *action_name,
488 const char *action_and_target)
489{
490 GtkMenuTrackerItem *self = GTK_MENU_TRACKER_ITEM (observer);
491 const char *action;
492
493 action = strrchr (s: self->action_and_target, c: '|') + 1;
494
495 if ((action_and_target && g_str_equal (v1: action_and_target, v2: self->action_and_target)) ||
496 (action_name && g_str_equal (v1: action_name, v2: action)))
497 g_object_notify_by_pspec (G_OBJECT (self), pspec: gtk_menu_tracker_item_pspecs[PROP_ACCEL]);
498}
499
500static void
501gtk_menu_tracker_item_init_observer_iface (GtkActionObserverInterface *iface)
502{
503 iface->action_added = gtk_menu_tracker_item_action_added;
504 iface->action_enabled_changed = gtk_menu_tracker_item_action_enabled_changed;
505 iface->action_state_changed = gtk_menu_tracker_item_action_state_changed;
506 iface->action_removed = gtk_menu_tracker_item_action_removed;
507 iface->primary_accel_changed = gtk_menu_tracker_item_primary_accel_changed;
508}
509
510GtkMenuTrackerItem *
511_gtk_menu_tracker_item_new (GtkActionObservable *observable,
512 GMenuModel *model,
513 int item_index,
514 gboolean mac_os_mode,
515 const char *action_namespace,
516 gboolean is_separator)
517{
518 GtkMenuTrackerItem *self;
519 const char *action_name;
520 const char *hidden_when;
521
522 g_return_val_if_fail (GTK_IS_ACTION_OBSERVABLE (observable), NULL);
523 g_return_val_if_fail (G_IS_MENU_MODEL (model), NULL);
524
525 self = g_object_new (GTK_TYPE_MENU_TRACKER_ITEM, NULL);
526 self->item = g_menu_item_new_from_model (model, item_index);
527 self->action_namespace = g_strdup (str: action_namespace);
528 self->observable = g_object_ref (observable);
529 self->is_separator = is_separator;
530
531 if (!is_separator && g_menu_item_get_attribute (menu_item: self->item, attribute: "hidden-when", format_string: "&s", &hidden_when))
532 {
533 if (g_str_equal (v1: hidden_when, v2: "action-disabled"))
534 self->hidden_when = HIDDEN_WHEN_DISABLED;
535 else if (g_str_equal (v1: hidden_when, v2: "action-missing"))
536 self->hidden_when = HIDDEN_WHEN_MISSING;
537 else if (mac_os_mode && g_str_equal (v1: hidden_when, v2: "macos-menubar"))
538 self->hidden_when = HIDDEN_WHEN_ALWAYS;
539
540 /* Ignore other values -- this code may be running in context of a
541 * desktop shell or the like and should not spew criticals due to
542 * application bugs...
543 *
544 * Note: if we just set a hidden-when state, but don't get the
545 * action_name below then our visibility will be FALSE forever.
546 * That's to be expected since the action is missing...
547 */
548 }
549
550 if (!is_separator && g_menu_item_get_attribute (menu_item: self->item, attribute: "action", format_string: "&s", &action_name))
551 {
552 GtkActionMuxer *muxer = GTK_ACTION_MUXER (observable);
553 const GVariantType *parameter_type;
554 GVariant *target;
555 gboolean enabled;
556 GVariant *state;
557 gboolean found;
558
559 target = g_menu_item_get_attribute_value (menu_item: self->item, attribute: "target", NULL);
560
561 self->action_and_target = gtk_print_action_and_target (action_namespace, action_name, target);
562
563 if (target)
564 g_variant_unref (value: target);
565
566 action_name = strrchr (s: self->action_and_target, c: '|') + 1;
567
568 GTK_NOTE(ACTIONS,
569 if (!strchr (action_name, '.'))
570 g_message ("menutracker: action name %s doesn't look like 'app.' or 'win.'; "
571 "it is unlikely to work", action_name));
572
573 state = NULL;
574
575 gtk_action_observable_register_observer (observable: self->observable, action_name, GTK_ACTION_OBSERVER (self));
576 found = gtk_action_muxer_query_action (muxer, action_name, enabled: &enabled, parameter_type: &parameter_type, NULL, NULL, state: &state);
577
578 if (found)
579 {
580 GTK_NOTE(ACTIONS, g_message ("menutracker: action %s existed from the start", action_name));
581 gtk_menu_tracker_item_action_added (GTK_ACTION_OBSERVER (self), observable, action_name, parameter_type, enabled, state);
582 }
583 else
584 {
585 GTK_NOTE(ACTIONS, g_message ("menutracker: action %s missing from the start", action_name));
586 gtk_menu_tracker_item_update_visibility (self);
587 }
588
589 if (state)
590 g_variant_unref (value: state);
591 }
592 else
593 {
594 gtk_menu_tracker_item_update_visibility (self);
595 self->sensitive = TRUE;
596 }
597
598 return self;
599}
600
601GtkActionObservable *
602_gtk_menu_tracker_item_get_observable (GtkMenuTrackerItem *self)
603{
604 return self->observable;
605}
606
607/*< private >
608 * gtk_menu_tracker_item_get_is_separator:
609 * @self: A GtkMenuTrackerItem instance
610 *
611 * Returns: whether the menu item is a separator. If so, only
612 * certain properties may need to be obeyed. See the documentation
613 * for GtkMenuTrackerItem.
614 */
615gboolean
616gtk_menu_tracker_item_get_is_separator (GtkMenuTrackerItem *self)
617{
618 return self->is_separator;
619}
620
621/*< private >
622 * gtk_menu_tracker_item_get_has_submenu:
623 * @self: A GtkMenuTrackerItem instance
624 *
625 * Returns: whether the menu item has a submenu. If so, only
626 * certain properties may need to be obeyed. See the documentation
627 * for GtkMenuTrackerItem.
628 */
629gboolean
630gtk_menu_tracker_item_get_has_link (GtkMenuTrackerItem *self,
631 const char *link_name)
632{
633 GMenuModel *link;
634
635 link = g_menu_item_get_link (menu_item: self->item, link: link_name);
636
637 if (link)
638 {
639 g_object_unref (object: link);
640 return TRUE;
641 }
642 else
643 return FALSE;
644}
645
646const char *
647gtk_menu_tracker_item_get_label (GtkMenuTrackerItem *self)
648{
649 const char *label = NULL;
650
651 g_menu_item_get_attribute (menu_item: self->item, G_MENU_ATTRIBUTE_LABEL, format_string: "&s", &label);
652
653 return label;
654}
655
656gboolean
657gtk_menu_tracker_item_get_use_markup (GtkMenuTrackerItem *self)
658{
659 return g_menu_item_get_attribute (menu_item: self->item, attribute: "use-markup", format_string: "&s", NULL);
660}
661
662/*< private >
663 * gtk_menu_tracker_item_get_icon:
664 *
665 * Returns: (transfer full):
666 */
667GIcon *
668gtk_menu_tracker_item_get_icon (GtkMenuTrackerItem *self)
669{
670 GVariant *icon_data;
671 GIcon *icon;
672
673 icon_data = g_menu_item_get_attribute_value (menu_item: self->item, attribute: "icon", NULL);
674
675 if (icon_data == NULL)
676 return NULL;
677
678 icon = g_icon_deserialize (value: icon_data);
679 g_variant_unref (value: icon_data);
680
681 return icon;
682}
683
684/*< private >
685 * gtk_menu_tracker_item_get_verb_icon:
686 *
687 * Returns: (transfer full):
688 */
689GIcon *
690gtk_menu_tracker_item_get_verb_icon (GtkMenuTrackerItem *self)
691{
692 GVariant *icon_data;
693 GIcon *icon;
694
695 icon_data = g_menu_item_get_attribute_value (menu_item: self->item, attribute: "verb-icon", NULL);
696
697 if (icon_data == NULL)
698 return NULL;
699
700 icon = g_icon_deserialize (value: icon_data);
701 g_variant_unref (value: icon_data);
702
703 return icon;
704}
705
706gboolean
707gtk_menu_tracker_item_get_sensitive (GtkMenuTrackerItem *self)
708{
709 return self->sensitive;
710}
711
712GtkMenuTrackerItemRole
713gtk_menu_tracker_item_get_role (GtkMenuTrackerItem *self)
714{
715 return self->role;
716}
717
718gboolean
719gtk_menu_tracker_item_get_toggled (GtkMenuTrackerItem *self)
720{
721 return self->toggled;
722}
723
724const char *
725gtk_menu_tracker_item_get_accel (GtkMenuTrackerItem *self)
726{
727 const char *accel;
728
729 if (!self->action_and_target)
730 return NULL;
731
732 if (g_menu_item_get_attribute (menu_item: self->item, attribute: "accel", format_string: "&s", &accel))
733 return accel;
734
735 if (!GTK_IS_ACTION_MUXER (self->observable))
736 return NULL;
737
738 return gtk_action_muxer_get_primary_accel (GTK_ACTION_MUXER (self->observable), action_and_target: self->action_and_target);
739}
740
741const char *
742gtk_menu_tracker_item_get_special (GtkMenuTrackerItem *self)
743{
744 const char *special = NULL;
745
746 g_menu_item_get_attribute (menu_item: self->item, attribute: "x-gtk-private-special", format_string: "&s", &special);
747
748 return special;
749}
750
751const char *
752gtk_menu_tracker_item_get_custom (GtkMenuTrackerItem *self)
753{
754 const char *custom = NULL;
755
756 g_menu_item_get_attribute (menu_item: self->item, attribute: "custom", format_string: "&s", &custom);
757
758 return custom;
759}
760
761const char *
762gtk_menu_tracker_item_get_display_hint (GtkMenuTrackerItem *self)
763{
764 const char *display_hint = NULL;
765
766 g_menu_item_get_attribute (menu_item: self->item, attribute: "display-hint", format_string: "&s", &display_hint);
767
768 return display_hint;
769}
770
771const char *
772gtk_menu_tracker_item_get_text_direction (GtkMenuTrackerItem *self)
773{
774 const char *text_direction = NULL;
775
776 g_menu_item_get_attribute (menu_item: self->item, attribute: "text-direction", format_string: "&s", &text_direction);
777
778 return text_direction;
779}
780
781GMenuModel *
782_gtk_menu_tracker_item_get_link (GtkMenuTrackerItem *self,
783 const char *link_name)
784{
785 return g_menu_item_get_link (menu_item: self->item, link: link_name);
786}
787
788char *
789_gtk_menu_tracker_item_get_link_namespace (GtkMenuTrackerItem *self)
790{
791 const char *namespace;
792
793 if (g_menu_item_get_attribute (menu_item: self->item, attribute: "action-namespace", format_string: "&s", &namespace))
794 {
795 if (self->action_namespace)
796 return g_strjoin (separator: ".", self->action_namespace, namespace, NULL);
797 else
798 return g_strdup (str: namespace);
799 }
800 else
801 return g_strdup (str: self->action_namespace);
802}
803
804gboolean
805gtk_menu_tracker_item_get_should_request_show (GtkMenuTrackerItem *self)
806{
807 return g_menu_item_get_attribute (menu_item: self->item, attribute: "submenu-action", format_string: "&s", NULL);
808}
809
810gboolean
811gtk_menu_tracker_item_get_submenu_shown (GtkMenuTrackerItem *self)
812{
813 return self->submenu_shown;
814}
815
816static void
817gtk_menu_tracker_item_set_submenu_shown (GtkMenuTrackerItem *self,
818 gboolean submenu_shown)
819{
820 if (submenu_shown == self->submenu_shown)
821 return;
822
823 self->submenu_shown = submenu_shown;
824 g_object_notify_by_pspec (G_OBJECT (self), pspec: gtk_menu_tracker_item_pspecs[PROP_SUBMENU_SHOWN]);
825}
826
827void
828gtk_menu_tracker_item_activated (GtkMenuTrackerItem *self)
829{
830 const char *action_name;
831 GVariant *action_target;
832
833 g_return_if_fail (GTK_IS_MENU_TRACKER_ITEM (self));
834
835 if (!self->can_activate)
836 return;
837
838 action_name = strrchr (s: self->action_and_target, c: '|') + 1;
839 action_target = g_menu_item_get_attribute_value (menu_item: self->item, G_MENU_ATTRIBUTE_TARGET, NULL);
840
841 gtk_action_muxer_activate_action (GTK_ACTION_MUXER (self->observable), action_name, parameter: action_target);
842
843 if (action_target)
844 g_variant_unref (value: action_target);
845}
846
847typedef struct {
848 GObject parent;
849
850 GtkMenuTrackerItem *item;
851 char *submenu_action;
852 gboolean first_time;
853} GtkMenuTrackerOpener;
854
855typedef struct {
856 GObjectClass parent_class;
857} GtkMenuTrackerOpenerClass;
858
859static void gtk_menu_tracker_opener_observer_iface_init (GtkActionObserverInterface *iface);
860
861GType gtk_menu_tracker_opener_get_type (void) G_GNUC_CONST;
862
863G_DEFINE_TYPE_WITH_CODE (GtkMenuTrackerOpener, gtk_menu_tracker_opener, G_TYPE_OBJECT,
864 G_IMPLEMENT_INTERFACE (GTK_TYPE_ACTION_OBSERVER, gtk_menu_tracker_opener_observer_iface_init))
865
866static void
867gtk_menu_tracker_opener_init (GtkMenuTrackerOpener *self)
868{
869}
870
871static void
872gtk_menu_tracker_opener_finalize (GObject *object)
873{
874 GtkMenuTrackerOpener *opener = (GtkMenuTrackerOpener *)object;
875
876 gtk_action_observable_unregister_observer (observable: opener->item->observable,
877 action_name: opener->submenu_action,
878 observer: (GtkActionObserver *)opener);
879
880 if (GTK_IS_ACTION_MUXER (opener->item->observable))
881 gtk_action_muxer_change_action_state (GTK_ACTION_MUXER (opener->item->observable),
882 action_name: opener->submenu_action,
883 state: g_variant_new_boolean (FALSE));
884
885 gtk_menu_tracker_item_set_submenu_shown (self: opener->item, FALSE);
886
887 g_free (mem: opener->submenu_action);
888
889 G_OBJECT_CLASS (gtk_menu_tracker_opener_parent_class)->finalize (object);
890}
891
892static void
893gtk_menu_tracker_opener_class_init (GtkMenuTrackerOpenerClass *class)
894{
895 G_OBJECT_CLASS (class)->finalize = gtk_menu_tracker_opener_finalize;
896}
897
898static void
899gtk_menu_tracker_opener_update (GtkMenuTrackerOpener *opener)
900{
901 GtkActionMuxer *muxer = GTK_ACTION_MUXER (opener->item->observable);
902 gboolean is_open = TRUE;
903 GVariant *state;
904
905 /* We consider the menu as being "open" if the action does not exist
906 * or if there is another problem (no state, wrong state type, etc.).
907 * If the action exists, with the correct state then we consider it
908 * open if we have ever seen this state equal to TRUE.
909 *
910 * In the event that we see the state equal to FALSE, we force it back
911 * to TRUE. We do not signal that the menu was closed because this is
912 * likely to create UI thrashing.
913 *
914 * The only way the menu can have a true-to-false submenu-shown
915 * transition is if the user calls _request_submenu_shown (FALSE).
916 * That is handled in _free() below.
917 */
918
919 if (gtk_action_muxer_query_action (muxer, action_name: opener->submenu_action, NULL, NULL, NULL, NULL, state: &state))
920 {
921 if (state)
922 {
923 if (g_variant_is_of_type (value: state, G_VARIANT_TYPE_BOOLEAN))
924 is_open = g_variant_get_boolean (value: state);
925 g_variant_unref (value: state);
926 }
927 }
928
929 /* If it is already open, signal that.
930 *
931 * If it is not open, ask it to open.
932 */
933 if (is_open)
934 gtk_menu_tracker_item_set_submenu_shown (self: opener->item, TRUE);
935
936 if (!is_open || opener->first_time)
937 {
938 gtk_action_muxer_change_action_state (muxer, action_name: opener->submenu_action, state: g_variant_new_boolean (TRUE));
939 opener->first_time = FALSE;
940 }
941}
942
943static void
944gtk_menu_tracker_opener_added (GtkActionObserver *observer,
945 GtkActionObservable *observable,
946 const char *action_name,
947 const GVariantType *parameter_type,
948 gboolean enabled,
949 GVariant *state)
950{
951 gtk_menu_tracker_opener_update (opener: (GtkMenuTrackerOpener *)observer);
952}
953
954static void
955gtk_menu_tracker_opener_removed (GtkActionObserver *observer,
956 GtkActionObservable *observable,
957 const char *action_name)
958{
959 gtk_menu_tracker_opener_update (opener: (GtkMenuTrackerOpener *)observer);
960}
961
962static void
963gtk_menu_tracker_opener_enabled_changed (GtkActionObserver *observer,
964 GtkActionObservable *observable,
965 const char *action_name,
966 gboolean enabled)
967{
968 gtk_menu_tracker_opener_update (opener: (GtkMenuTrackerOpener *)observer);
969}
970
971static void
972gtk_menu_tracker_opener_state_changed (GtkActionObserver *observer,
973 GtkActionObservable *observable,
974 const char *action_name,
975 GVariant *state)
976{
977 gtk_menu_tracker_opener_update (opener: (GtkMenuTrackerOpener *)observer);
978}
979
980static void
981gtk_menu_tracker_opener_observer_iface_init (GtkActionObserverInterface *iface)
982{
983 iface->action_added = gtk_menu_tracker_opener_added;
984 iface->action_removed = gtk_menu_tracker_opener_removed;
985 iface->action_enabled_changed = gtk_menu_tracker_opener_enabled_changed;
986 iface->action_state_changed = gtk_menu_tracker_opener_state_changed;
987}
988
989static GtkMenuTrackerOpener *
990gtk_menu_tracker_opener_new (GtkMenuTrackerItem *item,
991 const char *submenu_action)
992{
993 GtkMenuTrackerOpener *opener;
994
995 opener = g_object_new (object_type: gtk_menu_tracker_opener_get_type (), NULL);
996
997 opener->first_time = TRUE;
998 opener->item = item;
999
1000 if (item->action_namespace)
1001 opener->submenu_action = g_strjoin (separator: ".", item->action_namespace, submenu_action, NULL);
1002 else
1003 opener->submenu_action = g_strdup (str: submenu_action);
1004
1005 gtk_action_observable_register_observer (observable: item->observable,
1006 action_name: opener->submenu_action,
1007 observer: (GtkActionObserver *)opener);
1008
1009 gtk_menu_tracker_opener_update (opener);
1010
1011 return opener;
1012}
1013
1014void
1015gtk_menu_tracker_item_request_submenu_shown (GtkMenuTrackerItem *self,
1016 gboolean shown)
1017{
1018 const char *submenu_action;
1019 gboolean has_submenu_action;
1020
1021 if (shown == self->submenu_requested)
1022 return;
1023
1024 has_submenu_action = g_menu_item_get_attribute (menu_item: self->item, attribute: "submenu-action", format_string: "&s", &submenu_action);
1025
1026 self->submenu_requested = shown;
1027
1028 /* If we have a submenu action, start a submenu opener and wait
1029 * for the reply from the client. Otherwise, simply open the
1030 * submenu immediately.
1031 */
1032 if (has_submenu_action)
1033 {
1034 if (shown)
1035 g_object_set_data_full (G_OBJECT (self), key: "submenu-opener",
1036 data: gtk_menu_tracker_opener_new (item: self, submenu_action),
1037 destroy: g_object_unref);
1038 else
1039 g_object_set_data (G_OBJECT (self), key: "submenu-opener", NULL);
1040 }
1041 else
1042 gtk_menu_tracker_item_set_submenu_shown (self, submenu_shown: shown);
1043}
1044
1045/*
1046 * gtk_menu_tracker_item_get_is_visible:
1047 * @self: A GtkMenuTrackerItem instance
1048 *
1049 * Don't use this unless you're tracking items for yourself -- normally
1050 * the tracker will emit add/remove automatically when this changes.
1051 *
1052 * Returns: if the item should currently be shown
1053 */
1054gboolean
1055gtk_menu_tracker_item_get_is_visible (GtkMenuTrackerItem *self)
1056{
1057 return self->is_visible;
1058}
1059
1060/*
1061 * gtk_menu_tracker_item_may_disappear:
1062 * @self: A GtkMenuTrackerItem instance
1063 *
1064 * Returns: if the item may disappear (ie: is-visible property may change)
1065 */
1066gboolean
1067gtk_menu_tracker_item_may_disappear (GtkMenuTrackerItem *self)
1068{
1069 return self->hidden_when != HIDDEN_NEVER;
1070}
1071

source code of gtk/gtk/gtkmenutrackeritem.c