1 | /* |
2 | * Copyright © 2012 Canonical Limited |
3 | * |
4 | * This library is free software: you can redistribute it and/or modify |
5 | * it under the terms of the GNU Lesser General Public License as |
6 | * published by the Free Software Foundation; either version 2 of the |
7 | * licence or (at your option) any later version. |
8 | * |
9 | * This library is distributed in the hope that it will be useful, but |
10 | * 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 | * Authors: Ryan Lortie <desrt@desrt.ca> |
18 | */ |
19 | |
20 | #include "gtkactionhelperprivate.h" |
21 | #include "gtkactionobservableprivate.h" |
22 | |
23 | #include "gtkwidgetprivate.h" |
24 | #include "gtkdebug.h" |
25 | #include "gtktypebuiltins.h" |
26 | #include "gtkmodelbuttonprivate.h" |
27 | |
28 | #include <string.h> |
29 | |
30 | typedef struct |
31 | { |
32 | GActionGroup *group; |
33 | |
34 | GHashTable *watchers; |
35 | } GtkActionHelperGroup; |
36 | |
37 | static void gtk_action_helper_action_added (GtkActionHelper *helper, |
38 | gboolean enabled, |
39 | const GVariantType *parameter_type, |
40 | GVariant *state, |
41 | gboolean should_emit_signals); |
42 | |
43 | static void gtk_action_helper_action_removed (GtkActionHelper *helper, |
44 | gboolean should_emit_signals); |
45 | |
46 | static void gtk_action_helper_action_enabled_changed (GtkActionHelper *helper, |
47 | gboolean enabled); |
48 | |
49 | static void gtk_action_helper_action_state_changed (GtkActionHelper *helper, |
50 | GVariant *new_state); |
51 | |
52 | typedef GObjectClass GtkActionHelperClass; |
53 | |
54 | struct _GtkActionHelper |
55 | { |
56 | GObject parent_instance; |
57 | |
58 | GtkWidget *widget; |
59 | |
60 | GtkActionHelperGroup *group; |
61 | |
62 | GtkActionMuxer *action_context; |
63 | char *action_name; |
64 | |
65 | GVariant *target; |
66 | |
67 | gboolean can_activate; |
68 | gboolean enabled; |
69 | gboolean active; |
70 | |
71 | GtkButtonRole role; |
72 | |
73 | int reporting; |
74 | }; |
75 | |
76 | enum |
77 | { |
78 | PROP_0, |
79 | PROP_ENABLED, |
80 | PROP_ACTIVE, |
81 | PROP_ROLE, |
82 | N_PROPS |
83 | }; |
84 | |
85 | static GParamSpec *gtk_action_helper_pspecs[N_PROPS]; |
86 | |
87 | static void gtk_action_helper_observer_iface_init (GtkActionObserverInterface *iface); |
88 | |
89 | G_DEFINE_TYPE_WITH_CODE (GtkActionHelper, gtk_action_helper, G_TYPE_OBJECT, |
90 | G_IMPLEMENT_INTERFACE (GTK_TYPE_ACTION_OBSERVER, gtk_action_helper_observer_iface_init)) |
91 | |
92 | static void |
93 | gtk_action_helper_report_change (GtkActionHelper *helper, |
94 | guint prop_id) |
95 | { |
96 | helper->reporting++; |
97 | |
98 | switch (prop_id) |
99 | { |
100 | case PROP_ENABLED: |
101 | gtk_widget_set_sensitive (GTK_WIDGET (helper->widget), sensitive: helper->enabled); |
102 | break; |
103 | |
104 | case PROP_ACTIVE: |
105 | { |
106 | GParamSpec *pspec; |
107 | |
108 | pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (helper->widget), property_name: "active" ); |
109 | |
110 | if (pspec && G_PARAM_SPEC_VALUE_TYPE (pspec) == G_TYPE_BOOLEAN) |
111 | g_object_set (G_OBJECT (helper->widget), first_property_name: "active" , helper->active, NULL); |
112 | } |
113 | break; |
114 | |
115 | case PROP_ROLE: |
116 | { |
117 | GParamSpec *pspec; |
118 | |
119 | pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (helper->widget), property_name: "role" ); |
120 | |
121 | if (pspec && G_PARAM_SPEC_VALUE_TYPE (pspec) == GTK_TYPE_BUTTON_ROLE) |
122 | g_object_set (G_OBJECT (helper->widget), first_property_name: "role" , helper->role, NULL); |
123 | } |
124 | break; |
125 | |
126 | default: |
127 | g_assert_not_reached (); |
128 | } |
129 | |
130 | g_object_notify_by_pspec (G_OBJECT (helper), pspec: gtk_action_helper_pspecs[prop_id]); |
131 | helper->reporting--; |
132 | } |
133 | |
134 | static void |
135 | gtk_action_helper_action_added (GtkActionHelper *helper, |
136 | gboolean enabled, |
137 | const GVariantType *parameter_type, |
138 | GVariant *state, |
139 | gboolean should_emit_signals) |
140 | { |
141 | GTK_NOTE(ACTIONS, g_message("%s: action %s added" , "actionhelper" , helper->action_name)); |
142 | |
143 | /* we can only activate if we have the correct type of parameter */ |
144 | helper->can_activate = (helper->target == NULL && parameter_type == NULL) || |
145 | (helper->target != NULL && parameter_type != NULL && |
146 | g_variant_is_of_type (value: helper->target, type: parameter_type)); |
147 | |
148 | if (!helper->can_activate) |
149 | { |
150 | g_warning ("%s: action %s can't be activated due to parameter type mismatch " |
151 | "(parameter type %s, target type %s)" , |
152 | "actionhelper" , |
153 | helper->action_name, |
154 | parameter_type ? g_variant_type_peek_string (parameter_type) : "NULL" , |
155 | helper->target ? g_variant_get_type_string (helper->target) : "NULL" ); |
156 | return; |
157 | } |
158 | |
159 | GTK_NOTE(ACTIONS, g_message ("%s: %s can be activated" , "actionhelper" , helper->action_name)); |
160 | |
161 | helper->enabled = enabled; |
162 | |
163 | GTK_NOTE(ACTIONS, g_message ("%s: action %s is %s" , "actionhelper" , helper->action_name, enabled ? "enabled" : "disabled" )); |
164 | |
165 | if (helper->target != NULL && state != NULL) |
166 | { |
167 | helper->active = g_variant_equal (one: state, two: helper->target); |
168 | helper->role = GTK_BUTTON_ROLE_RADIO; |
169 | } |
170 | else if (state != NULL && g_variant_is_of_type (value: state, G_VARIANT_TYPE_BOOLEAN)) |
171 | { |
172 | helper->active = g_variant_get_boolean (value: state); |
173 | helper->role = GTK_BUTTON_ROLE_CHECK; |
174 | } |
175 | else |
176 | { |
177 | helper->role = GTK_BUTTON_ROLE_NORMAL; |
178 | } |
179 | |
180 | if (should_emit_signals) |
181 | { |
182 | if (helper->enabled) |
183 | gtk_action_helper_report_change (helper, prop_id: PROP_ENABLED); |
184 | |
185 | if (helper->active) |
186 | gtk_action_helper_report_change (helper, prop_id: PROP_ACTIVE); |
187 | |
188 | gtk_action_helper_report_change (helper, prop_id: PROP_ROLE); |
189 | } |
190 | } |
191 | |
192 | static void |
193 | gtk_action_helper_action_removed (GtkActionHelper *helper, |
194 | gboolean should_emit_signals) |
195 | { |
196 | GTK_NOTE(ACTIONS, g_message ("%s: action %s was removed" , "actionhelper" , helper->action_name)); |
197 | |
198 | if (!helper->can_activate) |
199 | return; |
200 | |
201 | helper->can_activate = FALSE; |
202 | |
203 | if (helper->enabled) |
204 | { |
205 | helper->enabled = FALSE; |
206 | |
207 | if (should_emit_signals) |
208 | gtk_action_helper_report_change (helper, prop_id: PROP_ENABLED); |
209 | } |
210 | |
211 | if (helper->active) |
212 | { |
213 | helper->active = FALSE; |
214 | |
215 | if (should_emit_signals) |
216 | gtk_action_helper_report_change (helper, prop_id: PROP_ACTIVE); |
217 | } |
218 | } |
219 | |
220 | static void |
221 | gtk_action_helper_action_enabled_changed (GtkActionHelper *helper, |
222 | gboolean enabled) |
223 | { |
224 | GTK_NOTE(ACTIONS, g_message ("%s: action %s: enabled changed to %d" , "actionhelper" , helper->action_name, enabled)); |
225 | |
226 | if (!helper->can_activate) |
227 | return; |
228 | |
229 | if (helper->enabled == enabled) |
230 | return; |
231 | |
232 | helper->enabled = enabled; |
233 | gtk_action_helper_report_change (helper, prop_id: PROP_ENABLED); |
234 | } |
235 | |
236 | static void |
237 | gtk_action_helper_action_state_changed (GtkActionHelper *helper, |
238 | GVariant *new_state) |
239 | { |
240 | gboolean was_active; |
241 | |
242 | GTK_NOTE(ACTIONS, g_message ("%s: %s state changed" , "actionhelper" , helper->action_name)); |
243 | |
244 | if (!helper->can_activate) |
245 | return; |
246 | |
247 | was_active = helper->active; |
248 | |
249 | if (helper->target) |
250 | helper->active = g_variant_equal (one: new_state, two: helper->target); |
251 | |
252 | else if (g_variant_is_of_type (value: new_state, G_VARIANT_TYPE_BOOLEAN)) |
253 | helper->active = g_variant_get_boolean (value: new_state); |
254 | |
255 | else |
256 | helper->active = FALSE; |
257 | |
258 | if (helper->active != was_active) |
259 | gtk_action_helper_report_change (helper, prop_id: PROP_ACTIVE); |
260 | } |
261 | |
262 | static void |
263 | gtk_action_helper_get_property (GObject *object, guint prop_id, |
264 | GValue *value, GParamSpec *pspec) |
265 | { |
266 | GtkActionHelper *helper = GTK_ACTION_HELPER (object); |
267 | |
268 | switch (prop_id) |
269 | { |
270 | case PROP_ENABLED: |
271 | g_value_set_boolean (value, v_boolean: helper->enabled); |
272 | break; |
273 | |
274 | case PROP_ACTIVE: |
275 | g_value_set_boolean (value, v_boolean: helper->active); |
276 | break; |
277 | |
278 | case PROP_ROLE: |
279 | g_value_set_enum (value, v_enum: helper->role); |
280 | break; |
281 | |
282 | default: |
283 | g_assert_not_reached (); |
284 | } |
285 | } |
286 | |
287 | static void |
288 | gtk_action_helper_finalize (GObject *object) |
289 | { |
290 | GtkActionHelper *helper = GTK_ACTION_HELPER (object); |
291 | |
292 | g_free (mem: helper->action_name); |
293 | |
294 | if (helper->target) |
295 | g_variant_unref (value: helper->target); |
296 | |
297 | G_OBJECT_CLASS (gtk_action_helper_parent_class) |
298 | ->finalize (object); |
299 | } |
300 | |
301 | static void |
302 | gtk_action_helper_observer_action_added (GtkActionObserver *observer, |
303 | GtkActionObservable *observable, |
304 | const char *action_name, |
305 | const GVariantType *parameter_type, |
306 | gboolean enabled, |
307 | GVariant *state) |
308 | { |
309 | gtk_action_helper_action_added (GTK_ACTION_HELPER (observer), enabled, parameter_type, state, TRUE); |
310 | } |
311 | |
312 | static void |
313 | gtk_action_helper_observer_action_enabled_changed (GtkActionObserver *observer, |
314 | GtkActionObservable *observable, |
315 | const char *action_name, |
316 | gboolean enabled) |
317 | { |
318 | gtk_action_helper_action_enabled_changed (GTK_ACTION_HELPER (observer), enabled); |
319 | } |
320 | |
321 | static void |
322 | gtk_action_helper_observer_action_state_changed (GtkActionObserver *observer, |
323 | GtkActionObservable *observable, |
324 | const char *action_name, |
325 | GVariant *state) |
326 | { |
327 | gtk_action_helper_action_state_changed (GTK_ACTION_HELPER (observer), new_state: state); |
328 | } |
329 | |
330 | static void |
331 | gtk_action_helper_observer_action_removed (GtkActionObserver *observer, |
332 | GtkActionObservable *observable, |
333 | const char *action_name) |
334 | { |
335 | gtk_action_helper_action_removed (GTK_ACTION_HELPER (observer), TRUE); |
336 | } |
337 | |
338 | static void |
339 | gtk_action_helper_init (GtkActionHelper *helper) |
340 | { |
341 | } |
342 | |
343 | static void |
344 | gtk_action_helper_class_init (GtkActionHelperClass *class) |
345 | { |
346 | class->get_property = gtk_action_helper_get_property; |
347 | class->finalize = gtk_action_helper_finalize; |
348 | |
349 | gtk_action_helper_pspecs[PROP_ENABLED] = g_param_spec_boolean (name: "enabled" , nick: "enabled" , blurb: "enabled" , FALSE, |
350 | flags: G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); |
351 | gtk_action_helper_pspecs[PROP_ACTIVE] = g_param_spec_boolean (name: "active" , nick: "active" , blurb: "active" , FALSE, |
352 | flags: G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); |
353 | gtk_action_helper_pspecs[PROP_ROLE] = g_param_spec_enum (name: "role" , nick: "role" , blurb: "role" , |
354 | GTK_TYPE_BUTTON_ROLE, |
355 | default_value: GTK_BUTTON_ROLE_NORMAL, |
356 | flags: G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); |
357 | g_object_class_install_properties (oclass: class, n_pspecs: N_PROPS, pspecs: gtk_action_helper_pspecs); |
358 | } |
359 | |
360 | static void |
361 | gtk_action_helper_observer_iface_init (GtkActionObserverInterface *iface) |
362 | { |
363 | iface->action_added = gtk_action_helper_observer_action_added; |
364 | iface->action_enabled_changed = gtk_action_helper_observer_action_enabled_changed; |
365 | iface->action_state_changed = gtk_action_helper_observer_action_state_changed; |
366 | iface->action_removed = gtk_action_helper_observer_action_removed; |
367 | } |
368 | |
369 | /*< private > |
370 | * gtk_action_helper_new: |
371 | * @widget: a `GtkWidget` implementing `GtkActionable` |
372 | * |
373 | * Creates a helper to track the state of a named action. This will |
374 | * usually be used by widgets implementing `GtkActionable`. |
375 | * |
376 | * This helper class is usually used by @widget itself. In order to |
377 | * avoid reference cycles, the helper does not hold a reference on |
378 | * @widget, but will assume that it continues to exist for the duration |
379 | * of the life of the helper. If you are using the helper from outside |
380 | * of the widget, you should take a ref on @widget for each ref you hold |
381 | * on the helper. |
382 | * |
383 | * Returns: a new `GtkActionHelper` |
384 | */ |
385 | GtkActionHelper * |
386 | gtk_action_helper_new (GtkActionable *widget) |
387 | { |
388 | GtkActionHelper *helper; |
389 | GParamSpec *pspec; |
390 | |
391 | g_return_val_if_fail (GTK_IS_ACTIONABLE (widget), NULL); |
392 | helper = g_object_new (GTK_TYPE_ACTION_HELPER, NULL); |
393 | |
394 | helper->widget = GTK_WIDGET (widget); |
395 | helper->enabled = gtk_widget_get_sensitive (GTK_WIDGET (helper->widget)); |
396 | |
397 | pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (helper->widget), property_name: "active" ); |
398 | if (pspec && G_PARAM_SPEC_VALUE_TYPE (pspec) == G_TYPE_BOOLEAN) |
399 | g_object_get (G_OBJECT (helper->widget), first_property_name: "active" , &helper->active, NULL); |
400 | |
401 | helper->action_context = _gtk_widget_get_action_muxer (GTK_WIDGET (widget), TRUE); |
402 | |
403 | return helper; |
404 | } |
405 | |
406 | void |
407 | gtk_action_helper_set_action_name (GtkActionHelper *helper, |
408 | const char *action_name) |
409 | { |
410 | gboolean was_enabled, was_active; |
411 | const GVariantType *parameter_type; |
412 | gboolean enabled; |
413 | GVariant *state; |
414 | |
415 | if (g_strcmp0 (str1: action_name, str2: helper->action_name) == 0) |
416 | return; |
417 | |
418 | GTK_NOTE(ACTIONS, |
419 | if (action_name == NULL || !strchr (action_name, '.')) |
420 | g_message ("%s: action name %s doesn't look like 'app.' or 'win.'; " |
421 | "it is unlikely to work" , |
422 | "actionhelper" , action_name)); |
423 | |
424 | /* Start by recording the current state of our properties so we know |
425 | * what notify signals we will need to send. |
426 | */ |
427 | was_enabled = helper->enabled; |
428 | was_active = helper->active; |
429 | |
430 | if (helper->action_name) |
431 | { |
432 | gtk_action_helper_action_removed (helper, FALSE); |
433 | gtk_action_observable_unregister_observer (GTK_ACTION_OBSERVABLE (helper->action_context), |
434 | action_name: helper->action_name, |
435 | GTK_ACTION_OBSERVER (helper)); |
436 | g_clear_pointer (&helper->action_name, g_free); |
437 | } |
438 | |
439 | if (action_name) |
440 | { |
441 | helper->action_name = g_strdup (str: action_name); |
442 | |
443 | gtk_action_observable_register_observer (GTK_ACTION_OBSERVABLE (helper->action_context), |
444 | action_name: helper->action_name, |
445 | GTK_ACTION_OBSERVER (helper)); |
446 | |
447 | if (gtk_action_muxer_query_action (muxer: helper->action_context, action_name: helper->action_name, |
448 | enabled: &enabled, parameter_type: ¶meter_type, |
449 | NULL, NULL, state: &state)) |
450 | { |
451 | GTK_NOTE(ACTIONS, g_message ("%s: action %s existed from the start" , "actionhelper" , helper->action_name)); |
452 | |
453 | gtk_action_helper_action_added (helper, enabled, parameter_type, state, FALSE); |
454 | |
455 | if (state) |
456 | g_variant_unref (value: state); |
457 | } |
458 | else |
459 | { |
460 | GTK_NOTE(ACTIONS, g_message ("%s: action %s missing from the start" , "actionhelper" , helper->action_name)); |
461 | helper->enabled = FALSE; |
462 | } |
463 | } |
464 | |
465 | /* Send the notifies for the properties that changed. |
466 | * |
467 | * When called during construction, widget is NULL. We don't need to |
468 | * report in that case. |
469 | */ |
470 | if (helper->enabled != was_enabled) |
471 | gtk_action_helper_report_change (helper, prop_id: PROP_ENABLED); |
472 | |
473 | if (helper->active != was_active) |
474 | gtk_action_helper_report_change (helper, prop_id: PROP_ACTIVE); |
475 | |
476 | g_object_notify (G_OBJECT (helper->widget), property_name: "action-name" ); |
477 | } |
478 | |
479 | /*< private > |
480 | * gtk_action_helper_set_action_target_value: |
481 | * @helper: a `GtkActionHelper` |
482 | * @target_value: an action target, as per `GtkActionable` |
483 | * |
484 | * This function consumes @action_target if it is floating. |
485 | */ |
486 | void |
487 | gtk_action_helper_set_action_target_value (GtkActionHelper *helper, |
488 | GVariant *target_value) |
489 | { |
490 | gboolean was_enabled; |
491 | gboolean was_active; |
492 | |
493 | if (target_value == helper->target) |
494 | return; |
495 | |
496 | if (target_value && helper->target && g_variant_equal (one: target_value, two: helper->target)) |
497 | { |
498 | g_variant_unref (value: g_variant_ref_sink (value: target_value)); |
499 | return; |
500 | } |
501 | |
502 | if (helper->target) |
503 | { |
504 | g_variant_unref (value: helper->target); |
505 | helper->target = NULL; |
506 | } |
507 | |
508 | if (target_value) |
509 | helper->target = g_variant_ref_sink (value: target_value); |
510 | |
511 | /* The action_name has not yet been set. Don't do anything yet. */ |
512 | if (helper->action_name == NULL) |
513 | return; |
514 | |
515 | was_enabled = helper->enabled; |
516 | was_active = helper->active; |
517 | |
518 | /* If we are attached to an action group then it is possible that this |
519 | * change of the target value could impact our properties (including |
520 | * changes to 'can_activate' and therefore 'enabled', due to resolving |
521 | * a parameter type mismatch). |
522 | * |
523 | * Start over again by pretending the action gets re-added. |
524 | */ |
525 | helper->can_activate = FALSE; |
526 | helper->enabled = FALSE; |
527 | helper->active = FALSE; |
528 | |
529 | if (helper->action_context) |
530 | { |
531 | const GVariantType *parameter_type; |
532 | gboolean enabled; |
533 | GVariant *state; |
534 | |
535 | if (gtk_action_muxer_query_action (muxer: helper->action_context, |
536 | action_name: helper->action_name, enabled: &enabled, parameter_type: ¶meter_type, |
537 | NULL, NULL, state: &state)) |
538 | { |
539 | gtk_action_helper_action_added (helper, enabled, parameter_type, state, FALSE); |
540 | |
541 | if (state) |
542 | g_variant_unref (value: state); |
543 | } |
544 | } |
545 | |
546 | if (helper->enabled != was_enabled) |
547 | gtk_action_helper_report_change (helper, prop_id: PROP_ENABLED); |
548 | |
549 | if (helper->active != was_active) |
550 | gtk_action_helper_report_change (helper, prop_id: PROP_ACTIVE); |
551 | |
552 | g_object_notify (G_OBJECT (helper->widget), property_name: "action-target" ); |
553 | } |
554 | |
555 | const char * |
556 | gtk_action_helper_get_action_name (GtkActionHelper *helper) |
557 | { |
558 | if (helper == NULL) |
559 | return NULL; |
560 | |
561 | return helper->action_name; |
562 | } |
563 | |
564 | GVariant * |
565 | gtk_action_helper_get_action_target_value (GtkActionHelper *helper) |
566 | { |
567 | if (helper == NULL) |
568 | return NULL; |
569 | |
570 | return helper->target; |
571 | } |
572 | |
573 | gboolean |
574 | gtk_action_helper_get_enabled (GtkActionHelper *helper) |
575 | { |
576 | g_return_val_if_fail (GTK_IS_ACTION_HELPER (helper), FALSE); |
577 | |
578 | return helper->enabled; |
579 | } |
580 | |
581 | gboolean |
582 | gtk_action_helper_get_active (GtkActionHelper *helper) |
583 | { |
584 | g_return_val_if_fail (GTK_IS_ACTION_HELPER (helper), FALSE); |
585 | |
586 | return helper->active; |
587 | } |
588 | |
589 | void |
590 | gtk_action_helper_activate (GtkActionHelper *helper) |
591 | { |
592 | g_return_if_fail (GTK_IS_ACTION_HELPER (helper)); |
593 | |
594 | if (!helper->can_activate || helper->reporting) |
595 | return; |
596 | |
597 | gtk_action_muxer_activate_action (muxer: helper->action_context, |
598 | action_name: helper->action_name, |
599 | parameter: helper->target); |
600 | } |
601 | |
602 | GtkButtonRole |
603 | gtk_action_helper_get_role (GtkActionHelper *helper) |
604 | { |
605 | g_return_val_if_fail (GTK_IS_ACTION_HELPER (helper), GTK_BUTTON_ROLE_NORMAL); |
606 | |
607 | return helper->role; |
608 | } |
609 | |
610 | |