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, &parameter_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, &parameter_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