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 | |
79 | typedef GObjectClass ; |
80 | |
81 | struct |
82 | { |
83 | GObject ; |
84 | |
85 | GtkActionObservable *; |
86 | char *; |
87 | char *action_and_target; |
88 | GMenuItem *; |
89 | GtkMenuTrackerItemRole : 4; |
90 | guint : 1; |
91 | guint : 1; |
92 | guint : 1; |
93 | guint : 1; |
94 | guint : 1; |
95 | guint : 1; |
96 | guint : 2; |
97 | guint : 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 | |
105 | enum { |
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 | , |
117 | PROP_IS_VISIBLE, |
118 | N_PROPS |
119 | }; |
120 | |
121 | static GParamSpec *[N_PROPS]; |
122 | |
123 | static void gtk_menu_tracker_item_init_observer_iface (GtkActionObserverInterface *iface); |
124 | G_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 | |
127 | GType |
128 | (void) |
129 | { |
130 | static gsize ; |
131 | |
132 | if (g_once_init_enter (>k_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 (>k_menu_tracker_item_role_type, type); |
145 | } |
146 | |
147 | return gtk_menu_tracker_item_role_type; |
148 | } |
149 | |
150 | static void |
151 | (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 | |
199 | static void |
200 | (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 | |
215 | static void |
216 | (GtkMenuTrackerItem * self) |
217 | { |
218 | } |
219 | |
220 | static void |
221 | (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 | */ |
259 | static void |
260 | (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 | |
293 | static void |
294 | (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 | |
385 | static void |
386 | (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 | |
408 | static void |
409 | (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 | |
442 | static void |
443 | (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 | |
484 | static void |
485 | (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 | |
500 | static void |
501 | (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 | |
510 | GtkMenuTrackerItem * |
511 | (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: ¶meter_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 | |
601 | GtkActionObservable * |
602 | (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 | */ |
615 | gboolean |
616 | (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 | */ |
629 | gboolean |
630 | (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 | |
646 | const char * |
647 | (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 | |
656 | gboolean |
657 | (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 | */ |
667 | GIcon * |
668 | (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 | */ |
689 | GIcon * |
690 | (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 | |
706 | gboolean |
707 | (GtkMenuTrackerItem *self) |
708 | { |
709 | return self->sensitive; |
710 | } |
711 | |
712 | GtkMenuTrackerItemRole |
713 | (GtkMenuTrackerItem *self) |
714 | { |
715 | return self->role; |
716 | } |
717 | |
718 | gboolean |
719 | (GtkMenuTrackerItem *self) |
720 | { |
721 | return self->toggled; |
722 | } |
723 | |
724 | const char * |
725 | (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 | |
741 | const char * |
742 | (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 | |
751 | const char * |
752 | (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 | |
761 | const char * |
762 | (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 | |
771 | const char * |
772 | (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 | |
781 | GMenuModel * |
782 | (GtkMenuTrackerItem *self, |
783 | const char *link_name) |
784 | { |
785 | return g_menu_item_get_link (menu_item: self->item, link: link_name); |
786 | } |
787 | |
788 | char * |
789 | (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 | |
804 | gboolean |
805 | (GtkMenuTrackerItem *self) |
806 | { |
807 | return g_menu_item_get_attribute (menu_item: self->item, attribute: "submenu-action" , format_string: "&s" , NULL); |
808 | } |
809 | |
810 | gboolean |
811 | (GtkMenuTrackerItem *self) |
812 | { |
813 | return self->submenu_shown; |
814 | } |
815 | |
816 | static void |
817 | (GtkMenuTrackerItem *self, |
818 | gboolean ) |
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 | |
827 | void |
828 | (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 | |
847 | typedef struct { |
848 | GObject ; |
849 | |
850 | GtkMenuTrackerItem *; |
851 | char *; |
852 | gboolean ; |
853 | } ; |
854 | |
855 | typedef struct { |
856 | GObjectClass ; |
857 | } ; |
858 | |
859 | static void gtk_menu_tracker_opener_observer_iface_init (GtkActionObserverInterface *iface); |
860 | |
861 | GType gtk_menu_tracker_opener_get_type (void) G_GNUC_CONST; |
862 | |
863 | G_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 | |
866 | static void |
867 | (GtkMenuTrackerOpener *self) |
868 | { |
869 | } |
870 | |
871 | static void |
872 | (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 | |
892 | static void |
893 | (GtkMenuTrackerOpenerClass *class) |
894 | { |
895 | G_OBJECT_CLASS (class)->finalize = gtk_menu_tracker_opener_finalize; |
896 | } |
897 | |
898 | static void |
899 | (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 | |
943 | static void |
944 | (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 | |
954 | static void |
955 | (GtkActionObserver *observer, |
956 | GtkActionObservable *observable, |
957 | const char *action_name) |
958 | { |
959 | gtk_menu_tracker_opener_update (opener: (GtkMenuTrackerOpener *)observer); |
960 | } |
961 | |
962 | static void |
963 | (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 | |
971 | static void |
972 | (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 | |
980 | static void |
981 | (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 | |
989 | static GtkMenuTrackerOpener * |
990 | (GtkMenuTrackerItem *item, |
991 | const char *) |
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 | |
1014 | void |
1015 | (GtkMenuTrackerItem *self, |
1016 | gboolean shown) |
1017 | { |
1018 | const char *; |
1019 | gboolean ; |
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 | */ |
1054 | gboolean |
1055 | (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 | */ |
1066 | gboolean |
1067 | (GtkMenuTrackerItem *self) |
1068 | { |
1069 | return self->hidden_when != HIDDEN_NEVER; |
1070 | } |
1071 | |