1 /*
2 * Copyright © 2013 Canonical Limited
3 *
4 * SPDX-License-Identifier: LGPL-2.1-or-later
5 *
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 2.1 of the License, or (at your option) any later version.
10 *
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General
17 * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
18 *
19 * Authors: Ryan Lortie <desrt@desrt.ca>
20 */
21
22 #include "config.h"
23
24 #include "gpropertyaction.h"
25
26 #include "gsettings-mapping.h"
27 #include "gaction.h"
28 #include "glibintl.h"
29
30 /**
31 * GPropertyAction:
32 *
33 * A `GPropertyAction` is a way to get a [iface@Gio.Action] with a state value
34 * reflecting and controlling the value of a [class@GObject.Object] property.
35 *
36 * The state of the action will correspond to the value of the property.
37 * Changing it will change the property (assuming the requested value
38 * matches the requirements as specified in the [type@GObject.ParamSpec]).
39 *
40 * Only the most common types are presently supported. Booleans are
41 * mapped to booleans, strings to strings, signed/unsigned integers to
42 * int32/uint32 and floats and doubles to doubles.
43 *
44 * If the property is an enum then the state will be string-typed and
45 * conversion will automatically be performed between the enum value and
46 * ‘nick’ string as per the [type@GObject.EnumValue] table.
47 *
48 * Flags types are not currently supported.
49 *
50 * Properties of object types, boxed types and pointer types are not
51 * supported and probably never will be.
52 *
53 * Properties of [type@GLib.Variant] types are not currently supported.
54 *
55 * If the property is boolean-valued then the action will have a `NULL`
56 * parameter type, and activating the action (with no parameter) will
57 * toggle the value of the property.
58 *
59 * In all other cases, the parameter type will correspond to the type of
60 * the property.
61 *
62 * The general idea here is to reduce the number of locations where a
63 * particular piece of state is kept (and therefore has to be synchronised
64 * between). `GPropertyAction` does not have a separate state that is kept
65 * in sync with the property value — its state is the property value.
66 *
67 * For example, it might be useful to create a [iface@Gio.Action] corresponding
68 * to the `visible-child-name` property of a [class@Gtk.Stack] so that the
69 * current page can be switched from a menu. The active radio indication in the
70 * menu is then directly determined from the active page of the
71 * [class@Gtk.Stack].
72 *
73 * An anti-example would be binding the `active-id` property on a
74 * [class@Gtk.ComboBox]. This is because the state of the combobox itself is
75 * probably uninteresting and is actually being used to control
76 * something else.
77 *
78 * Another anti-example would be to bind to the `visible-child-name`
79 * property of a [class@Gtk.Stack] if this value is actually stored in
80 * [class@Gio.Settings]. In that case, the real source of the value is
81 * [class@Gio.Settings]. If you want a [iface@Gio.Action] to control a setting
82 * stored in [class@Gio.Settings], see [method@Gio.Settings.create_action]
83 * instead, and possibly combine its use with [method@Gio.Settings.bind].
84 *
85 * Since: 2.38
86 **/
87 struct _GPropertyAction
88 {
89 GObject parent_instance;
90
91 gchar *name;
92 gpointer object;
93 GParamSpec *pspec;
94 const GVariantType *state_type;
95 gboolean invert_boolean;
96 };
97
98 typedef GObjectClass GPropertyActionClass;
99
100 static void g_property_action_iface_init (GActionInterface *iface);
101 G_DEFINE_TYPE_WITH_CODE (GPropertyAction, g_property_action, G_TYPE_OBJECT,
102 G_IMPLEMENT_INTERFACE (G_TYPE_ACTION, g_property_action_iface_init))
103
104 enum
105 {
106 PROP_NONE,
107 PROP_NAME,
108 PROP_PARAMETER_TYPE,
109 PROP_ENABLED,
110 PROP_STATE_TYPE,
111 PROP_STATE,
112 PROP_OBJECT,
113 PROP_PROPERTY_NAME,
114 PROP_INVERT_BOOLEAN
115 };
116
117 static gboolean
118 g_property_action_get_invert_boolean (GAction *action)
119 {
120 GPropertyAction *paction = G_PROPERTY_ACTION (action);
121
122 return paction->invert_boolean;
123 }
124
125 static const gchar *
126 g_property_action_get_name (GAction *action)
127 {
128 GPropertyAction *paction = G_PROPERTY_ACTION (action);
129
130 return paction->name;
131 }
132
133 static const GVariantType *
134 g_property_action_get_parameter_type (GAction *action)
135 {
136 GPropertyAction *paction = G_PROPERTY_ACTION (action);
137
138 return paction->pspec->value_type == G_TYPE_BOOLEAN ? NULL : paction->state_type;
139 }
140
141 static const GVariantType *
142 g_property_action_get_state_type (GAction *action)
143 {
144 GPropertyAction *paction = G_PROPERTY_ACTION (action);
145
146 return paction->state_type;
147 }
148
149 static GVariant *
150 g_property_action_get_state_hint (GAction *action)
151 {
152 GPropertyAction *paction = G_PROPERTY_ACTION (action);
153
154 if (paction->pspec->value_type == G_TYPE_INT)
155 {
156 GParamSpecInt *pspec = (GParamSpecInt *)paction->pspec;
157 return g_variant_new ("(ii)", pspec->minimum, pspec->maximum);
158 }
159 else if (paction->pspec->value_type == G_TYPE_UINT)
160 {
161 GParamSpecUInt *pspec = (GParamSpecUInt *)paction->pspec;
162 return g_variant_new ("(uu)", pspec->minimum, pspec->maximum);
163 }
164 else if (paction->pspec->value_type == G_TYPE_FLOAT)
165 {
166 GParamSpecFloat *pspec = (GParamSpecFloat *)paction->pspec;
167 return g_variant_new ("(dd)", (double)pspec->minimum, (double)pspec->maximum);
168 }
169 else if (paction->pspec->value_type == G_TYPE_DOUBLE)
170 {
171 GParamSpecDouble *pspec = (GParamSpecDouble *)paction->pspec;
172 return g_variant_new ("(dd)", pspec->minimum, pspec->maximum);
173 }
174
175 return NULL;
176 }
177
178 static gboolean
179 g_property_action_get_enabled (GAction *action)
180 {
181 return TRUE;
182 }
183
184 static void
185 g_property_action_set_state (GPropertyAction *paction,
186 GVariant *variant)
187 {
188 GValue value = G_VALUE_INIT;
189
190 g_value_init (&value, paction->pspec->value_type);
191 g_settings_get_mapping (&value, variant, NULL);
192
193 if (paction->pspec->value_type == G_TYPE_BOOLEAN && paction->invert_boolean)
194 g_value_set_boolean (&value, !g_value_get_boolean (&value));
195
196 g_object_set_property (paction->object, paction->pspec->name, &value);
197 g_value_unset (&value);
198 }
199
200 static void
201 g_property_action_change_state (GAction *action,
202 GVariant *value)
203 {
204 GPropertyAction *paction = G_PROPERTY_ACTION (action);
205
206 g_return_if_fail (g_variant_is_of_type (value, paction->state_type));
207
208 g_property_action_set_state (paction, value);
209 }
210
211 static GVariant *
212 g_property_action_get_state (GAction *action)
213 {
214 GPropertyAction *paction = G_PROPERTY_ACTION (action);
215 GValue value = G_VALUE_INIT;
216 GVariant *result;
217
218 g_value_init (&value, paction->pspec->value_type);
219 g_object_get_property (paction->object, paction->pspec->name, &value);
220
221 if (paction->pspec->value_type == G_TYPE_BOOLEAN && paction->invert_boolean)
222 g_value_set_boolean (&value, !g_value_get_boolean (&value));
223
224 result = g_settings_set_mapping (&value, paction->state_type, NULL);
225 g_value_unset (&value);
226
227 return g_variant_ref_sink (result);
228 }
229
230 static void
231 g_property_action_activate (GAction *action,
232 GVariant *parameter)
233 {
234 GPropertyAction *paction = G_PROPERTY_ACTION (action);
235
236 if (paction->pspec->value_type == G_TYPE_BOOLEAN)
237 {
238 gboolean value;
239
240 g_return_if_fail (paction->pspec->value_type == G_TYPE_BOOLEAN && parameter == NULL);
241
242 g_object_get (paction->object, paction->pspec->name, &value, NULL);
243 value = !value;
244 g_object_set (paction->object, paction->pspec->name, value, NULL);
245 }
246 else
247 {
248 g_return_if_fail (parameter != NULL && g_variant_is_of_type (parameter, paction->state_type));
249
250 g_property_action_set_state (paction, parameter);
251 }
252 }
253
254 static const GVariantType *
255 g_property_action_determine_type (GParamSpec *pspec)
256 {
257 if (G_TYPE_IS_ENUM (pspec->value_type))
258 return G_VARIANT_TYPE_STRING;
259
260 switch (pspec->value_type)
261 {
262 case G_TYPE_BOOLEAN:
263 return G_VARIANT_TYPE_BOOLEAN;
264
265 case G_TYPE_INT:
266 return G_VARIANT_TYPE_INT32;
267
268 case G_TYPE_UINT:
269 return G_VARIANT_TYPE_UINT32;
270
271 case G_TYPE_DOUBLE:
272 case G_TYPE_FLOAT:
273 return G_VARIANT_TYPE_DOUBLE;
274
275 case G_TYPE_STRING:
276 return G_VARIANT_TYPE_STRING;
277
278 default:
279 g_critical ("Unable to use GPropertyAction with property '%s::%s' of type '%s'",
280 g_type_name (pspec->owner_type), pspec->name, g_type_name (pspec->value_type));
281 return NULL;
282 }
283 }
284
285 static void
286 g_property_action_notify (GObject *object,
287 GParamSpec *pspec,
288 gpointer user_data)
289 {
290 GPropertyAction *paction = user_data;
291
292 g_assert (object == paction->object);
293 g_assert (pspec == paction->pspec);
294
295 g_object_notify (G_OBJECT (paction), "state");
296 }
297
298 static void
299 g_property_action_set_property_name (GPropertyAction *paction,
300 const gchar *property_name)
301 {
302 GParamSpec *pspec;
303 gchar *detailed;
304
305 /* In case somebody is constructing GPropertyAction without passing
306 * a property name
307 */
308 if (G_UNLIKELY (property_name == NULL || property_name[0] == '\0'))
309 {
310 g_critical ("Attempted to use an empty property name for GPropertyAction");
311 return;
312 }
313
314 pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (paction->object), property_name);
315
316 if (pspec == NULL)
317 {
318 g_critical ("Attempted to use non-existent property '%s::%s' for GPropertyAction",
319 G_OBJECT_TYPE_NAME (paction->object), property_name);
320 return;
321 }
322
323 if (~pspec->flags & G_PARAM_READABLE || ~pspec->flags & G_PARAM_WRITABLE || pspec->flags & G_PARAM_CONSTRUCT_ONLY)
324 {
325 g_critical ("Property '%s::%s' used with GPropertyAction must be readable, writable, and not construct-only",
326 G_OBJECT_TYPE_NAME (paction->object), property_name);
327 return;
328 }
329
330 paction->pspec = pspec;
331
332 detailed = g_strconcat ("notify::", paction->pspec->name, NULL);
333 paction->state_type = g_property_action_determine_type (paction->pspec);
334 g_signal_connect (paction->object, detailed, G_CALLBACK (g_property_action_notify), paction);
335 g_free (detailed);
336 }
337
338 static void
339 g_property_action_set_property (GObject *object,
340 guint prop_id,
341 const GValue *value,
342 GParamSpec *pspec)
343 {
344 GPropertyAction *paction = G_PROPERTY_ACTION (object);
345
346 switch (prop_id)
347 {
348 case PROP_NAME:
349 paction->name = g_value_dup_string (value);
350 break;
351
352 case PROP_OBJECT:
353 paction->object = g_value_dup_object (value);
354 break;
355
356 case PROP_PROPERTY_NAME:
357 g_property_action_set_property_name (paction, g_value_get_string (value));
358 break;
359
360 case PROP_INVERT_BOOLEAN:
361 paction->invert_boolean = g_value_get_boolean (value);
362 break;
363
364 default:
365 g_assert_not_reached ();
366 }
367 }
368
369 static void
370 g_property_action_get_property (GObject *object,
371 guint prop_id,
372 GValue *value,
373 GParamSpec *pspec)
374 {
375 GAction *action = G_ACTION (object);
376
377 switch (prop_id)
378 {
379 case PROP_NAME:
380 g_value_set_string (value, g_property_action_get_name (action));
381 break;
382
383 case PROP_PARAMETER_TYPE:
384 g_value_set_boxed (value, g_property_action_get_parameter_type (action));
385 break;
386
387 case PROP_ENABLED:
388 g_value_set_boolean (value, g_property_action_get_enabled (action));
389 break;
390
391 case PROP_STATE_TYPE:
392 g_value_set_boxed (value, g_property_action_get_state_type (action));
393 break;
394
395 case PROP_STATE:
396 g_value_take_variant (value, g_property_action_get_state (action));
397 break;
398
399 case PROP_INVERT_BOOLEAN:
400 g_value_set_boolean (value, g_property_action_get_invert_boolean (action));
401 break;
402
403 default:
404 g_assert_not_reached ();
405 }
406 }
407
408 static void
409 g_property_action_dispose (GObject *object)
410 {
411 GPropertyAction *paction = G_PROPERTY_ACTION (object);
412
413 if (paction->object != NULL)
414 {
415 g_signal_handlers_disconnect_by_func (paction->object, g_property_action_notify, paction);
416 g_clear_object (&paction->object);
417 }
418
419 G_OBJECT_CLASS (g_property_action_parent_class)->dispose (object);
420 }
421
422 static void
423 g_property_action_finalize (GObject *object)
424 {
425 GPropertyAction *paction = G_PROPERTY_ACTION (object);
426
427 g_free (paction->name);
428
429 G_OBJECT_CLASS (g_property_action_parent_class)
430 ->finalize (object);
431 }
432
433 void
434 g_property_action_init (GPropertyAction *property)
435 {
436 }
437
438 void
439 g_property_action_iface_init (GActionInterface *iface)
440 {
441 iface->get_name = g_property_action_get_name;
442 iface->get_parameter_type = g_property_action_get_parameter_type;
443 iface->get_state_type = g_property_action_get_state_type;
444 iface->get_state_hint = g_property_action_get_state_hint;
445 iface->get_enabled = g_property_action_get_enabled;
446 iface->get_state = g_property_action_get_state;
447 iface->change_state = g_property_action_change_state;
448 iface->activate = g_property_action_activate;
449 }
450
451 void
452 g_property_action_class_init (GPropertyActionClass *class)
453 {
454 GObjectClass *object_class = G_OBJECT_CLASS (class);
455
456 object_class->set_property = g_property_action_set_property;
457 object_class->get_property = g_property_action_get_property;
458 object_class->dispose = g_property_action_dispose;
459 object_class->finalize = g_property_action_finalize;
460
461 /**
462 * GPropertyAction:name:
463 *
464 * The name of the action. This is mostly meaningful for identifying
465 * the action once it has been added to a #GActionMap.
466 *
467 * Since: 2.38
468 **/
469 g_object_class_install_property (object_class, PROP_NAME,
470 g_param_spec_string ("name", NULL, NULL,
471 NULL,
472 G_PARAM_READWRITE |
473 G_PARAM_CONSTRUCT_ONLY |
474 G_PARAM_STATIC_STRINGS));
475
476 /**
477 * GPropertyAction:parameter-type:
478 *
479 * The type of the parameter that must be given when activating the
480 * action.
481 *
482 * Since: 2.38
483 **/
484 g_object_class_install_property (object_class, PROP_PARAMETER_TYPE,
485 g_param_spec_boxed ("parameter-type", NULL, NULL,
486 G_TYPE_VARIANT_TYPE,
487 G_PARAM_READABLE |
488 G_PARAM_STATIC_STRINGS));
489
490 /**
491 * GPropertyAction:enabled:
492 *
493 * If @action is currently enabled.
494 *
495 * If the action is disabled then calls to g_action_activate() and
496 * g_action_change_state() have no effect.
497 *
498 * Since: 2.38
499 **/
500 g_object_class_install_property (object_class, PROP_ENABLED,
501 g_param_spec_boolean ("enabled", NULL, NULL,
502 TRUE,
503 G_PARAM_READABLE |
504 G_PARAM_STATIC_STRINGS));
505
506 /**
507 * GPropertyAction:state-type:
508 *
509 * The #GVariantType of the state that the action has, or %NULL if the
510 * action is stateless.
511 *
512 * Since: 2.38
513 **/
514 g_object_class_install_property (object_class, PROP_STATE_TYPE,
515 g_param_spec_boxed ("state-type", NULL, NULL,
516 G_TYPE_VARIANT_TYPE,
517 G_PARAM_READABLE |
518 G_PARAM_STATIC_STRINGS));
519
520 /**
521 * GPropertyAction:state:
522 *
523 * The state of the action, or %NULL if the action is stateless.
524 *
525 * Since: 2.38
526 **/
527 g_object_class_install_property (object_class, PROP_STATE,
528 g_param_spec_variant ("state", NULL, NULL,
529 G_VARIANT_TYPE_ANY,
530 NULL,
531 G_PARAM_READABLE |
532 G_PARAM_STATIC_STRINGS));
533
534 /**
535 * GPropertyAction:object:
536 *
537 * The object to wrap a property on.
538 *
539 * The object must be a non-%NULL #GObject with properties.
540 *
541 * Since: 2.38
542 **/
543 g_object_class_install_property (object_class, PROP_OBJECT,
544 g_param_spec_object ("object", NULL, NULL,
545 G_TYPE_OBJECT,
546 G_PARAM_WRITABLE |
547 G_PARAM_CONSTRUCT_ONLY |
548 G_PARAM_STATIC_STRINGS));
549
550 /**
551 * GPropertyAction:property-name:
552 *
553 * The name of the property to wrap on the object.
554 *
555 * The property must exist on the passed-in object and it must be
556 * readable and writable (and not construct-only).
557 *
558 * Since: 2.38
559 **/
560 g_object_class_install_property (object_class, PROP_PROPERTY_NAME,
561 g_param_spec_string ("property-name", NULL, NULL,
562 NULL,
563 G_PARAM_WRITABLE |
564 G_PARAM_CONSTRUCT_ONLY |
565 G_PARAM_STATIC_STRINGS));
566
567 /**
568 * GPropertyAction:invert-boolean:
569 *
570 * If %TRUE, the state of the action will be the negation of the
571 * property value, provided the property is boolean.
572 *
573 * Since: 2.46
574 */
575 g_object_class_install_property (object_class, PROP_INVERT_BOOLEAN,
576 g_param_spec_boolean ("invert-boolean", NULL, NULL,
577 FALSE,
578 G_PARAM_READWRITE |
579 G_PARAM_CONSTRUCT_ONLY |
580 G_PARAM_STATIC_STRINGS));
581 }
582
583 /**
584 * g_property_action_new:
585 * @name: the name of the action to create
586 * @object: (type GObject.Object): the object that has the property
587 * to wrap
588 * @property_name: the name of the property
589 *
590 * Creates a #GAction corresponding to the value of property
591 * @property_name on @object.
592 *
593 * The property must be existent and readable and writable (and not
594 * construct-only).
595 *
596 * This function takes a reference on @object and doesn't release it
597 * until the action is destroyed.
598 *
599 * Returns: a new #GPropertyAction
600 *
601 * Since: 2.38
602 **/
603 GPropertyAction *
604 g_property_action_new (const gchar *name,
605 gpointer object,
606 const gchar *property_name)
607 {
608 g_return_val_if_fail (name != NULL, NULL);
609 g_return_val_if_fail (G_IS_OBJECT (object), NULL);
610 g_return_val_if_fail (property_name != NULL, NULL);
611
612 return g_object_new (G_TYPE_PROPERTY_ACTION,
613 "name", name,
614 "object", object,
615 "property-name", property_name,
616 NULL);
617 }