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
30typedef struct
31{
32 GActionGroup *group;
33
34 GHashTable *watchers;
35} GtkActionHelperGroup;
36
37static 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
43static void gtk_action_helper_action_removed (GtkActionHelper *helper,
44 gboolean should_emit_signals);
45
46static void gtk_action_helper_action_enabled_changed (GtkActionHelper *helper,
47 gboolean enabled);
48
49static void gtk_action_helper_action_state_changed (GtkActionHelper *helper,
50 GVariant *new_state);
51
52typedef GObjectClass GtkActionHelperClass;
53
54struct _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
76enum
77{
78 PROP_0,
79 PROP_ENABLED,
80 PROP_ACTIVE,
81 PROP_ROLE,
82 N_PROPS
83};
84
85static GParamSpec *gtk_action_helper_pspecs[N_PROPS];
86
87static void gtk_action_helper_observer_iface_init (GtkActionObserverInterface *iface);
88
89G_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
92static void
93gtk_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
134static void
135gtk_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
192static void
193gtk_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
220static void
221gtk_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
236static void
237gtk_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
262static void
263gtk_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
287static void
288gtk_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
301static void
302gtk_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
312static void
313gtk_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
321static void
322gtk_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
330static void
331gtk_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
338static void
339gtk_action_helper_init (GtkActionHelper *helper)
340{
341}
342
343static void
344gtk_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
360static void
361gtk_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 */
385GtkActionHelper *
386gtk_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
406void
407gtk_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: &parameter_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 */
486void
487gtk_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: &parameter_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
555const char *
556gtk_action_helper_get_action_name (GtkActionHelper *helper)
557{
558 if (helper == NULL)
559 return NULL;
560
561 return helper->action_name;
562}
563
564GVariant *
565gtk_action_helper_get_action_target_value (GtkActionHelper *helper)
566{
567 if (helper == NULL)
568 return NULL;
569
570 return helper->target;
571}
572
573gboolean
574gtk_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
581gboolean
582gtk_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
589void
590gtk_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
602GtkButtonRole
603gtk_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

source code of gtk/gtk/gtkactionhelper.c