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
G_DEFINE_TYPE_WITH_CODE(GtkActionHelper,gtk_action_helper,G_TYPE_OBJECT,G_IMPLEMENT_INTERFACE (GTK_TYPE_ACTION_OBSERVER,gtk_action_helper_observer_iface_init))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), 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), "active");
109
110 if (pspec && G_PARAM_SPEC_VALUE_TYPE (pspec) == G_TYPE_BOOLEAN)
111 g_object_set (G_OBJECT (helper->widget), "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), "role");
120
121 if (pspec && G_PARAM_SPEC_VALUE_TYPE (pspec) == GTK_TYPE_BUTTON_ROLE)
122 g_object_set (G_OBJECT (helper->widget), "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), gtk_action_helper_pspecs[prop_id]);
131 helper->reporting--;
132 }
133
134 static void
gtk_action_helper_action_added(GtkActionHelper * helper,gboolean enabled,const GVariantType * parameter_type,GVariant * state,gboolean should_emit_signals)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 (helper->target, 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 (state, helper->target);
168 helper->role = GTK_BUTTON_ROLE_RADIO;
169 }
170 else if (state != NULL && g_variant_is_of_type (state, G_VARIANT_TYPE_BOOLEAN))
171 {
172 helper->active = g_variant_get_boolean (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_ENABLED);
184
185 if (helper->active)
186 gtk_action_helper_report_change (helper, PROP_ACTIVE);
187
188 gtk_action_helper_report_change (helper, PROP_ROLE);
189 }
190 }
191
192 static void
gtk_action_helper_action_removed(GtkActionHelper * helper,gboolean should_emit_signals)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_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_ACTIVE);
217 }
218 }
219
220 static void
gtk_action_helper_action_enabled_changed(GtkActionHelper * helper,gboolean enabled)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_ENABLED);
234 }
235
236 static void
gtk_action_helper_action_state_changed(GtkActionHelper * helper,GVariant * new_state)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 (new_state, helper->target);
251
252 else if (g_variant_is_of_type (new_state, G_VARIANT_TYPE_BOOLEAN))
253 helper->active = g_variant_get_boolean (new_state);
254
255 else
256 helper->active = FALSE;
257
258 if (helper->active != was_active)
259 gtk_action_helper_report_change (helper, PROP_ACTIVE);
260 }
261
262 static void
gtk_action_helper_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)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, helper->enabled);
272 break;
273
274 case PROP_ACTIVE:
275 g_value_set_boolean (value, helper->active);
276 break;
277
278 case PROP_ROLE:
279 g_value_set_enum (value, helper->role);
280 break;
281
282 default:
283 g_assert_not_reached ();
284 }
285 }
286
287 static void
gtk_action_helper_finalize(GObject * object)288 gtk_action_helper_finalize (GObject *object)
289 {
290 GtkActionHelper *helper = GTK_ACTION_HELPER (object);
291
292 g_free (helper->action_name);
293
294 if (helper->target)
295 g_variant_unref (helper->target);
296
297 G_OBJECT_CLASS (gtk_action_helper_parent_class)
298 ->finalize (object);
299 }
300
301 static void
gtk_action_helper_observer_action_added(GtkActionObserver * observer,GtkActionObservable * observable,const char * action_name,const GVariantType * parameter_type,gboolean enabled,GVariant * state)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
gtk_action_helper_observer_action_enabled_changed(GtkActionObserver * observer,GtkActionObservable * observable,const char * action_name,gboolean enabled)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
gtk_action_helper_observer_action_state_changed(GtkActionObserver * observer,GtkActionObservable * observable,const char * action_name,GVariant * state)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), state);
328 }
329
330 static void
gtk_action_helper_observer_action_removed(GtkActionObserver * observer,GtkActionObservable * observable,const char * action_name)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
gtk_action_helper_init(GtkActionHelper * helper)339 gtk_action_helper_init (GtkActionHelper *helper)
340 {
341 }
342
343 static void
gtk_action_helper_class_init(GtkActionHelperClass * class)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 ("enabled", "enabled", "enabled", FALSE,
350 G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
351 gtk_action_helper_pspecs[PROP_ACTIVE] = g_param_spec_boolean ("active", "active", "active", FALSE,
352 G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
353 gtk_action_helper_pspecs[PROP_ROLE] = g_param_spec_enum ("role", "role", "role",
354 GTK_TYPE_BUTTON_ROLE,
355 GTK_BUTTON_ROLE_NORMAL,
356 G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
357 g_object_class_install_properties (class, N_PROPS, gtk_action_helper_pspecs);
358 }
359
360 static void
gtk_action_helper_observer_iface_init(GtkActionObserverInterface * iface)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 *
gtk_action_helper_new(GtkActionable * widget)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), "active");
398 if (pspec && G_PARAM_SPEC_VALUE_TYPE (pspec) == G_TYPE_BOOLEAN)
399 g_object_get (G_OBJECT (helper->widget), "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
gtk_action_helper_set_action_name(GtkActionHelper * helper,const char * action_name)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 (action_name, 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 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 (action_name);
442
443 gtk_action_observable_register_observer (GTK_ACTION_OBSERVABLE (helper->action_context),
444 helper->action_name,
445 GTK_ACTION_OBSERVER (helper));
446
447 if (gtk_action_muxer_query_action (helper->action_context, helper->action_name,
448 &enabled, ¶meter_type,
449 NULL, NULL, &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 (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_ENABLED);
472
473 if (helper->active != was_active)
474 gtk_action_helper_report_change (helper, PROP_ACTIVE);
475
476 g_object_notify (G_OBJECT (helper->widget), "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
gtk_action_helper_set_action_target_value(GtkActionHelper * helper,GVariant * target_value)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 (target_value, helper->target))
497 {
498 g_variant_unref (g_variant_ref_sink (target_value));
499 return;
500 }
501
502 if (helper->target)
503 {
504 g_variant_unref (helper->target);
505 helper->target = NULL;
506 }
507
508 if (target_value)
509 helper->target = g_variant_ref_sink (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 (helper->action_context,
536 helper->action_name, &enabled, ¶meter_type,
537 NULL, NULL, &state))
538 {
539 gtk_action_helper_action_added (helper, enabled, parameter_type, state, FALSE);
540
541 if (state)
542 g_variant_unref (state);
543 }
544 }
545
546 if (helper->enabled != was_enabled)
547 gtk_action_helper_report_change (helper, PROP_ENABLED);
548
549 if (helper->active != was_active)
550 gtk_action_helper_report_change (helper, PROP_ACTIVE);
551
552 g_object_notify (G_OBJECT (helper->widget), "action-target");
553 }
554
555 const char *
gtk_action_helper_get_action_name(GtkActionHelper * helper)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 *
gtk_action_helper_get_action_target_value(GtkActionHelper * helper)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
gtk_action_helper_get_enabled(GtkActionHelper * helper)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
gtk_action_helper_get_active(GtkActionHelper * helper)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
gtk_action_helper_activate(GtkActionHelper * helper)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 (helper->action_context,
598 helper->action_name,
599 helper->target);
600 }
601
602 GtkButtonRole
gtk_action_helper_get_role(GtkActionHelper * helper)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