3
* Copyright (C) 2015 Christian Hergert <christian@hergert.me>
5
* This file is free software; you can redistribute it and/or modify it
6
* under the terms of the GNU Lesser General Public License as
7
* published by the Free Software Foundation; either version 3 of the
8
* License, or (at your option) any later version.
10
* This file is distributed in the hope that it will be useful, but
11
* WITHOUT ANY WARRANTY; without even the implied warranty of
12
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13
* Lesser General Public License for more details.
15
* You should have received a copy of the GNU General Public License
16
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#include <glib/gi18n.h>
21
#include "egg-binding-set.h"
22
#include "egg-signal-group.h"
23
#include "egg-state-machine.h"
30
* Containers for lazily bound signals and bindings.
32
* Each is a GHashTable indexed by state name, containing another GHashTable indexed by
35
GHashTable *binding_sets_by_state;
36
GHashTable *signal_groups_by_state;
39
* Container for actions which should have sensitivity mutated during state transitions.
41
* GHashTable of GPtrArray of ActionState.
43
GHashTable *actions_by_state;
46
} EggStateMachinePrivate;
50
GSimpleAction *action;
51
guint invert_enabled : 1;
54
G_DEFINE_TYPE_WITH_PRIVATE (EggStateMachine, egg_state_machine, G_TYPE_OBJECT)
55
G_DEFINE_QUARK (EggStateMachineError, egg_state_machine_error)
68
static GParamSpec *gParamSpecs [LAST_PROP];
69
static guint gSignals [LAST_SIGNAL];
72
action_state_free (gpointer data)
74
ActionState *state = data;
76
g_clear_object (&state->action);
77
g_slice_free (ActionState, state);
81
egg_state_transition_accumulator (GSignalInvocationHint *hint,
83
const GValue *handler_return,
86
EggStateTransition ret;
88
ret = g_value_get_enum (handler_return);
90
if (ret == EGG_STATE_TRANSITION_INVALID)
92
g_value_set_enum (return_value, ret);
100
egg_state_machine_get_state (EggStateMachine *self)
102
EggStateMachinePrivate *priv = egg_state_machine_get_instance_private (self);
104
g_return_val_if_fail (EGG_IS_STATE_MACHINE (self), NULL);
110
egg_state_machine_do_transition (EggStateMachine *self,
111
const gchar *new_state)
113
EggStateMachinePrivate *priv = egg_state_machine_get_instance_private (self);
116
GPtrArray *action_states;
120
g_assert (EGG_IS_STATE_MACHINE (self));
121
g_assert (new_state != NULL);
125
g_free (priv->state);
126
priv->state = g_strdup (new_state);
128
g_hash_table_iter_init (&iter, priv->signal_groups_by_state);
129
while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *)&value))
131
GHashTable *signal_groups = value;
132
GHashTableIter groups_iter;
133
EggSignalGroup *signal_group;
135
gboolean enabled = (g_strcmp0 (key, new_state) == 0);
137
g_hash_table_iter_init (&groups_iter, signal_groups);
139
while (g_hash_table_iter_next (&groups_iter, &instance, (gpointer *)&signal_group))
141
g_assert (G_IS_OBJECT (instance));
142
g_assert (EGG_IS_SIGNAL_GROUP (signal_group));
144
egg_signal_group_set_target (signal_group, enabled ? instance : NULL);
148
g_hash_table_iter_init (&iter, priv->binding_sets_by_state);
149
while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *)&value))
151
GHashTable *binding_sets = value;
152
GHashTableIter groups_iter;
153
EggBindingSet *binding_set;
155
gboolean enabled = (g_strcmp0 (key, new_state) == 0);
157
g_hash_table_iter_init (&groups_iter, binding_sets);
159
while (g_hash_table_iter_next (&groups_iter, &instance, (gpointer *)&binding_set))
161
g_assert (G_IS_OBJECT (instance));
162
g_assert (EGG_IS_BINDING_SET (binding_set));
164
egg_binding_set_set_source (binding_set, enabled ? instance : NULL);
168
/* apply GSimpleAction:enabled to non-matching states */
169
g_hash_table_iter_init (&iter, priv->actions_by_state);
170
while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *)&action_states))
172
if (g_strcmp0 (key, priv->state) == 0)
175
for (i = 0; i < action_states->len; i++)
177
ActionState *action_state;
179
action_state = g_ptr_array_index (action_states, i);
180
g_simple_action_set_enabled (action_state->action, action_state->invert_enabled);
184
/* apply GSimpleAction:enabled to matching state */
185
action_states = g_hash_table_lookup (priv->actions_by_state, priv->state);
186
if (action_states != NULL)
188
for (i = 0; i < action_states->len; i++)
190
ActionState *action_state;
192
action_state = g_ptr_array_index (action_states, i);
193
g_simple_action_set_enabled (action_state->action, !action_state->invert_enabled);
199
* egg_state_machine_transition:
200
* @self: A #EggStateMachine.
201
* @new_state: The name of the new state.
202
* @error: A location for a #GError, or %NULL.
204
* Attempts to change the state of the state machine to @new_state.
206
* This operation can fail, in which %EGG_STATE_TRANSITION_INVALID will be
207
* returned and @error will be set.
209
* Upon success, %EGG_STATE_TRANSITION_SUCCESS is returned.
211
* Returns: An #EggStateTransition.
214
egg_state_machine_transition (EggStateMachine *self,
215
const gchar *new_state,
218
EggStateMachinePrivate *priv = egg_state_machine_get_instance_private (self);
219
g_autofree gchar *old_state = NULL;
220
EggStateTransition ret = EGG_STATE_TRANSITION_IGNORED;
221
g_autoptr(GError) local_error = NULL;
224
g_return_val_if_fail (EGG_IS_STATE_MACHINE (self), EGG_STATE_TRANSITION_INVALID);
225
g_return_val_if_fail (new_state != NULL, EGG_STATE_TRANSITION_INVALID);
226
g_return_val_if_fail (error == NULL || *error == NULL, EGG_STATE_TRANSITION_INVALID);
228
if (g_strcmp0 (new_state, priv->state) == 0)
229
return EGG_STATE_TRANSITION_SUCCESS;
231
/* Be careful with reentrancy. */
233
old_state = g_strdup (priv->state);
234
sequence = priv->sequence;
236
g_signal_emit (self, gSignals [TRANSITION], 0, old_state, new_state, &local_error, &ret);
238
if (ret == EGG_STATE_TRANSITION_INVALID)
240
if (local_error == NULL)
241
local_error = g_error_new_literal (EGG_STATE_MACHINE_ERROR,
242
EGG_STATE_MACHINE_ERROR_INVALID_TRANSITION,
243
"Unknown error during state transition.");
244
g_propagate_error (error, local_error);
249
if (sequence == priv->sequence)
251
egg_state_machine_do_transition (self, new_state);
252
g_object_notify_by_pspec (G_OBJECT (self), gParamSpecs [PROP_STATE]);
255
return EGG_STATE_TRANSITION_SUCCESS;
258
static EggStateTransition
259
egg_state_machine_real_transition (EggStateMachine *self,
260
const gchar *old_state,
261
const gchar *new_state,
264
return EGG_STATE_TRANSITION_IGNORED;
268
egg_state_machine_finalize (GObject *object)
270
EggStateMachine *self = (EggStateMachine *)object;
271
EggStateMachinePrivate *priv = egg_state_machine_get_instance_private (self);
273
g_clear_pointer (&priv->state, g_free);
274
g_clear_pointer (&priv->binding_sets_by_state, g_hash_table_unref);
275
g_clear_pointer (&priv->signal_groups_by_state, g_hash_table_unref);
276
g_clear_pointer (&priv->actions_by_state, g_hash_table_unref);
278
G_OBJECT_CLASS (egg_state_machine_parent_class)->finalize (object);
282
egg_state_machine_get_property (GObject *object,
287
EggStateMachine *self = EGG_STATE_MACHINE (object);
292
g_value_set_string (value, egg_state_machine_get_state (self));
296
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
301
egg_state_machine_set_property (GObject *object,
306
EggStateMachine *self = EGG_STATE_MACHINE (object);
307
EggStateMachinePrivate *priv = egg_state_machine_get_instance_private (self);
312
priv->state = g_value_dup_string (value);
316
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
321
egg_state_machine_class_init (EggStateMachineClass *klass)
323
GObjectClass *object_class = G_OBJECT_CLASS (klass);
325
object_class->finalize = egg_state_machine_finalize;
326
object_class->get_property = egg_state_machine_get_property;
327
object_class->set_property = egg_state_machine_set_property;
329
klass->transition = egg_state_machine_real_transition;
331
gParamSpecs [PROP_STATE] =
332
g_param_spec_string ("state",
334
_("The current state of the machine."),
336
(G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
338
g_object_class_install_properties (object_class, LAST_PROP, gParamSpecs);
341
* EggStateMachine::transition:
342
* @self: An #EggStateMachine.
343
* @old_state: The current state.
344
* @new_state: The new state.
345
* @error: (ctype GError**): A location for a #GError, or %NULL.
347
* Determines if the transition is allowed.
349
* If the state transition is invalid, @error should be set to a new #GError.
351
* Returns: %TRUE if the state transition is acceptable.
353
gSignals [TRANSITION] =
354
g_signal_new ("transition",
355
G_TYPE_FROM_CLASS (klass),
357
G_STRUCT_OFFSET (EggStateMachineClass, transition),
358
egg_state_transition_accumulator, NULL,
360
EGG_TYPE_STATE_TRANSITION,
368
egg_state_machine_init (EggStateMachine *self)
370
EggStateMachinePrivate *priv = egg_state_machine_get_instance_private (self);
372
priv->binding_sets_by_state =
373
g_hash_table_new_full (g_str_hash,
376
(GDestroyNotify)g_hash_table_destroy);
378
priv->signal_groups_by_state =
379
g_hash_table_new_full (g_str_hash,
382
(GDestroyNotify)g_hash_table_destroy);
384
priv->actions_by_state =
385
g_hash_table_new_full (g_str_hash,
388
(GDestroyNotify)g_ptr_array_unref);
392
egg_state_machine__connect_object_weak_notify (gpointer data,
393
GObject *where_object_was)
395
EggStateMachine *self = data;
396
EggStateMachinePrivate *priv = egg_state_machine_get_instance_private (self);
401
g_assert (EGG_IS_STATE_MACHINE (self));
402
g_assert (where_object_was != NULL);
404
g_hash_table_iter_init (&iter, priv->signal_groups_by_state);
405
while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *)&value))
407
GHashTable *signal_groups = value;
409
g_hash_table_remove (signal_groups, where_object_was);
414
egg_state_machine_connect_object (EggStateMachine *self,
417
const gchar *detailed_signal,
422
EggStateMachinePrivate *priv = egg_state_machine_get_instance_private (self);
423
GHashTable *signal_groups;
424
EggSignalGroup *signal_group;
425
gboolean created = FALSE;
427
g_return_if_fail (EGG_IS_STATE_MACHINE (self));
428
g_return_if_fail (state != NULL);
429
g_return_if_fail (G_IS_OBJECT (instance));
430
g_return_if_fail (detailed_signal != NULL);
431
g_return_if_fail (g_signal_parse_name (detailed_signal,
432
G_TYPE_FROM_INSTANCE (instance),
433
NULL, NULL, FALSE) != 0);
434
g_return_if_fail (callback != NULL);
436
signal_groups = g_hash_table_lookup (priv->signal_groups_by_state, state);
438
if (signal_groups == NULL)
440
signal_groups = g_hash_table_new_full (g_direct_hash,
444
g_hash_table_insert (priv->signal_groups_by_state, g_strdup (state), signal_groups);
447
g_assert (signal_groups != NULL);
449
signal_group = g_hash_table_lookup (signal_groups, instance);
451
if (signal_group == NULL)
454
signal_group = egg_signal_group_new (G_TYPE_FROM_INSTANCE (instance));
455
g_hash_table_insert (signal_groups, instance, signal_group);
456
g_object_weak_ref (instance,
457
(GWeakNotify)egg_state_machine__connect_object_weak_notify,
461
egg_signal_group_connect_object (signal_group, detailed_signal, callback, user_data, flags);
463
if ((created == TRUE) && (g_strcmp0 (state, priv->state) == 0))
464
egg_signal_group_set_target (signal_group, instance);
468
egg_state_machine__bind_source_weak_notify (gpointer data,
469
GObject *where_object_was)
471
EggStateMachine *self = data;
472
EggStateMachinePrivate *priv = egg_state_machine_get_instance_private (self);
477
g_assert (EGG_IS_STATE_MACHINE (self));
478
g_assert (where_object_was != NULL);
480
g_hash_table_iter_init (&iter, priv->binding_sets_by_state);
481
while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *)&value))
483
GHashTable *binding_sets = value;
485
g_hash_table_remove (binding_sets, where_object_was);
490
egg_state_machine_bind (EggStateMachine *self,
493
const gchar *source_property,
495
const gchar *target_property,
498
EggStateMachinePrivate *priv = egg_state_machine_get_instance_private (self);
499
GHashTable *binding_sets;
500
EggBindingSet *binding_set;
501
gboolean created = FALSE;
503
g_return_if_fail (EGG_IS_STATE_MACHINE (self));
504
g_return_if_fail (state != NULL);
505
g_return_if_fail (G_IS_OBJECT (source));
506
g_return_if_fail (source_property != NULL);
507
g_return_if_fail (g_object_class_find_property (G_OBJECT_GET_CLASS (source),
508
source_property) != NULL);
509
g_return_if_fail (G_IS_OBJECT (target));
510
g_return_if_fail (target_property != NULL);
511
g_return_if_fail (g_object_class_find_property (G_OBJECT_GET_CLASS (target),
512
target_property) != NULL);
514
/* Use G_BINDING_SYNC_CREATE as we lazily connect them. */
515
flags |= G_BINDING_SYNC_CREATE;
517
binding_sets = g_hash_table_lookup (priv->binding_sets_by_state, state);
519
if (binding_sets == NULL)
521
binding_sets = g_hash_table_new_full (g_direct_hash,
525
g_hash_table_insert (priv->binding_sets_by_state, g_strdup (state), binding_sets);
528
g_assert (binding_sets != NULL);
530
binding_set = g_hash_table_lookup (binding_sets, source);
532
if (binding_set == NULL)
535
binding_set = egg_binding_set_new ();
536
g_hash_table_insert (binding_sets, source, binding_set);
537
g_object_weak_ref (source,
538
(GWeakNotify)egg_state_machine__bind_source_weak_notify,
542
egg_binding_set_bind (binding_set,
548
if ((created == TRUE) && (g_strcmp0 (state, priv->state) == 0))
549
egg_binding_set_set_source (binding_set, source);
553
egg_state_machine_add_action (EggStateMachine *self,
555
GSimpleAction *action,
556
gboolean invert_enabled)
558
EggStateMachinePrivate *priv = egg_state_machine_get_instance_private (self);
559
ActionState *action_state;
563
g_return_if_fail (EGG_IS_STATE_MACHINE (self));
564
g_return_if_fail (state != NULL);
565
g_return_if_fail (G_IS_SIMPLE_ACTION (action));
567
action_state = g_slice_new0 (ActionState);
568
action_state->action = g_object_ref (action);
569
action_state->invert_enabled = invert_enabled;
571
actions = g_hash_table_lookup (priv->actions_by_state, state);
575
actions = g_ptr_array_new_with_free_func (action_state_free);
576
g_hash_table_insert (priv->actions_by_state, g_strdup (state), actions);
579
g_ptr_array_add (actions, action_state);
581
enabled = (g_strcmp0 (state, priv->state) == 0);
585
g_simple_action_set_enabled (action, enabled);
589
egg_state_machine_new (void)
591
return g_object_new (EGG_TYPE_STATE_MACHINE, NULL);
595
egg_state_machine_error_get_type (void)
597
static gsize type_id;
599
if (g_once_init_enter (&type_id))
601
static const GEnumValue values[] = {
602
{ EGG_STATE_MACHINE_ERROR_INVALID_TRANSITION,
603
"EGG_STATE_MACHINE_ERROR_INVALID_TRANSITION",
604
"invalid-transition" },
609
_type_id = g_enum_register_static ("EggStateMachineError", values);
610
g_once_init_leave (&type_id, _type_id);
617
egg_state_transition_get_type (void)
619
static gsize type_id;
621
if (g_once_init_enter (&type_id))
623
static const GEnumValue values[] = {
624
{ EGG_STATE_TRANSITION_IGNORED, "EGG_STATE_TRANSITION_IGNORED", "ignored" },
625
{ EGG_STATE_TRANSITION_INVALID, "EGG_STATE_TRANSITION_INVALID", "invalid" },
626
{ EGG_STATE_TRANSITION_SUCCESS, "EGG_STATE_TRANSITION_SUCCESS", "success" },
631
_type_id = g_enum_register_static ("EggStateTransition", values);
632
g_once_init_leave (&type_id, _type_id);