diff --git a/.gitignore b/.gitignore index 9381cacfe..788783dbc 100644 --- a/.gitignore +++ b/.gitignore @@ -151,6 +151,8 @@ TAGS /tests/interactive/test-stage-sizing /tests/interactive/test-drag /tests/interactive/test-constraints +/tests/interactive/test-state +/tests/interactive/test-state-animator /tests/conform/stamp-test-conformance /tests/conform/test-anchors /tests/conform/test-cogl-backface-culling @@ -258,6 +260,7 @@ TAGS /tests/conform/test-cogl-path /tests/conform/test-cogl-wrap-modes /tests/conform/test-clutter-cairo-texture +/tests/conform/test-state-base /tests/conform/wrappers /tests/micro-bench/test-text-perf /tests/micro-bench/test-text diff --git a/clutter/Makefile.am b/clutter/Makefile.am index 6b112e4d0..f1e9bca53 100644 --- a/clutter/Makefile.am +++ b/clutter/Makefile.am @@ -121,6 +121,7 @@ source_h = \ $(srcdir)/clutter-stage.h \ $(srcdir)/clutter-stage-manager.h \ $(srcdir)/clutter-stage-window.h \ + $(srcdir)/clutter-state.h \ $(srcdir)/clutter-texture.h \ $(srcdir)/clutter-text.h \ $(srcdir)/clutter-timeline.h \ @@ -203,6 +204,7 @@ source_c = \ $(srcdir)/clutter-stage.c \ $(srcdir)/clutter-stage-manager.c \ $(srcdir)/clutter-stage-window.c \ + $(srcdir)/clutter-state.c \ $(srcdir)/clutter-texture.c \ $(srcdir)/clutter-text.c \ $(srcdir)/clutter-timeline.c \ diff --git a/clutter/clutter-state.c b/clutter/clutter-state.c new file mode 100644 index 000000000..95bd81a31 --- /dev/null +++ b/clutter/clutter-state.c @@ -0,0 +1,1769 @@ +/* + * Clutter. + * + * An OpenGL based 'interactive canvas' library. + * + * Authored By Øyvind Kolås + * + * Copyright (C) 2009 Intel Corporation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +/** + * SECTION:clutter-state + * @short_description: State machine with animated transitions + * + * #ClutterState controls the tweening of properties on multiple + * actors between a set of named states. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "clutter-state.h" + +#include +#include + +#include "clutter-alpha.h" +#include "clutter-animator.h" +#include "clutter-enum-types.h" +#include "clutter-interval.h" +#include "clutter-marshal.h" +#include "clutter-private.h" +#include "clutter-scriptable.h" +#include "clutter-script-private.h" + +typedef struct StateAnimator { + const gchar *source_state_name; /* interned string identifying entry */ + ClutterAnimator *animator; /* pointer to animator itself */ +} StateAnimator; + +typedef struct State +{ + const gchar *name; /* interned string for this state name */ + GHashTable *durations; /* durations for transitions from various state + names */ + GList *keys; /* list of all keys pertaining to transitions + from other states to this one */ + GArray *animators; /* list of animators for transitioning from + * specific source states */ + ClutterState *clutter_state; /* the ClutterState object this state belongs to + */ +} State; + +struct _ClutterStatePrivate +{ + GHashTable *states; /* contains state objects */ + guint duration; /* global fallback duration */ + ClutterTimeline *timeline; /* The timeline used for doing the + progress */ + ClutterTimeline *slave_timeline; /* a slave timeline used to compute + alphas */ + const gchar *source_state_name; /* current source state */ + State *source_state; /* current source_state */ + const gchar *target_state_name; /* current target state */ + State *target_state; /* target state name */ + ClutterAnimator *current_animator; /* !NULL if the current transition is + overriden by an animator */ +}; + +#define SLAVE_TIMELINE_LENGTH 10000 + +/* + * ClutterStateKey: + * + * An opaque data structure with accessor functions. + * + */ +typedef struct _ClutterStateKey +{ + GObject *object; /* an Gobject */ + const gchar *property_name;/* the name of a property */ + gulong mode; /* alpha to use */ + GValue value; /* target value */ + gdouble pre_delay; /* fraction of duration to delay before + starting */ + gdouble post_delay; /* fraction of duration to be done in */ + + State *source_state; /* source state */ + State *target_state; /* target state */ + + ClutterAlpha *alpha; /* The alpha this key uses for interpolation */ + ClutterInterval *interval; /* The interval this key uses for + interpolation */ + + guint is_inert : 1; /* set if the key is being destroyed due to + weak reference */ + gint ref_count; /* reference count for boxed life time */ +} _ClutterStateKey; + +enum +{ + PROP_0, + PROP_TARGET_STATE +}; + +enum +{ + COMPLETED, + LAST_SIGNAL +}; + +static void clutter_scriptable_iface_init (ClutterScriptableIface *iface); + +static guint state_signals[LAST_SIGNAL] = {0, }; + +#define CLUTTER_STATE_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE ((obj), \ + CLUTTER_TYPE_STATE, \ + ClutterStatePrivate)) + +G_DEFINE_TYPE_WITH_CODE (ClutterState, clutter_state, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (CLUTTER_TYPE_SCRIPTABLE, + clutter_scriptable_iface_init)); + +/** + * clutter_state_new: + * + * Creates a new #ClutterState + * + * Return value: the newly create #ClutterState instance + */ +ClutterState * +clutter_state_new (void) +{ + return g_object_new (CLUTTER_TYPE_STATE, NULL); +} + +static gint +sort_props_func (gconstpointer a, + gconstpointer b) +{ + const ClutterStateKey *pa = a; + const ClutterStateKey *pb = b; + + if (pa->object == pb->object) + { + gint propnamediff = pa->property_name-pb->property_name; + if (propnamediff == 0) + return pb->source_state - pa->source_state; + + return propnamediff; + } + + return pa->object - pb->object; +} + +static void object_disappeared (gpointer data, + GObject *where_the_object_was); + +static ClutterStateKey * +clutter_state_key_new (State *state, + GObject *object, + const gchar *property_name, + GParamSpec *pspec, + guint mode) +{ + ClutterStateKey *state_key; + GValue value = {0, }; + + state_key = g_slice_new0 (ClutterStateKey); + + state_key->target_state = state; + state_key->object = object; + state_key->property_name = g_intern_string (property_name); + state_key->mode = mode; + + state_key->alpha = clutter_alpha_new (); + g_object_ref_sink (state_key->alpha); + clutter_alpha_set_mode (state_key->alpha, mode); + clutter_alpha_set_timeline (state_key->alpha, + state->clutter_state->priv->slave_timeline); + + state_key->interval = + g_object_new (CLUTTER_TYPE_INTERVAL, + "value-type", G_PARAM_SPEC_VALUE_TYPE (pspec), + NULL); + g_object_ref_sink (state_key->interval); + + g_value_init (&value, G_PARAM_SPEC_VALUE_TYPE (pspec)); + clutter_interval_set_initial_value (state_key->interval, &value); + clutter_interval_set_final_value (state_key->interval, &value); + g_value_unset (&value); + + g_object_weak_ref (object, object_disappeared, + state_key->target_state->clutter_state); + + return state_key; +} + +static void +clutter_state_key_free (gpointer clutter_state_key) +{ + ClutterStateKey *key = clutter_state_key; + + if (key == NULL) + return; + + key->ref_count -= 1; + + if (key->ref_count > 0) + return; + + if (!key->is_inert) + g_object_weak_unref (key->object, object_disappeared, + key->target_state->clutter_state); + g_object_unref (key->alpha); + g_object_unref (key->interval); + + g_slice_free (ClutterStateKey, key); +} + + +static inline void +clutter_state_remove_key_internal (ClutterState *this, + const gchar *source_state_name, + const gchar *target_state_name, + GObject *object, + const gchar *property_name, + gboolean is_inert) +{ + GList *s, *state_list; + State *source_state = NULL; + + source_state_name = g_intern_string (source_state_name); + target_state_name = g_intern_string (target_state_name); + property_name = g_intern_string (property_name); + + if (source_state_name) + source_state = g_hash_table_lookup (this->priv->states, + source_state_name); + + if (target_state_name != NULL) + state_list = g_list_append (NULL, (gpointer) target_state_name); + else + state_list = clutter_state_get_states (this); + + for (s = state_list; s != NULL; s = s->next) + { + State *target_state; + target_state = g_hash_table_lookup (this->priv->states, s->data); + + if (target_state) + { + GList *k; + + for (k = target_state->keys; k; k = k->next) + { + ClutterStateKey *key = k->data; + + if ((object == NULL || (object == key->object)) && + (source_state == NULL || + (source_state == key->source_state)) && + (property_name == NULL || + ((property_name == key->property_name)))) + { + k = target_state->keys = + g_list_remove (target_state->keys, key); + + key->is_inert = is_inert; + clutter_state_key_free (key); + } + } + } + } + + g_list_free (state_list); +} + +static void +object_disappeared (gpointer data, + GObject *where_the_object_was) +{ + clutter_state_remove_key_internal (data, NULL, NULL, + (gpointer) where_the_object_was, + NULL, + TRUE); +} + + +static void +state_free (gpointer data) +{ + State *state = data; + + for (; state->keys; + state->keys = g_list_remove (state->keys, state->keys->data)) + clutter_state_key_free (state->keys->data); + + g_array_unref (state->animators); + g_hash_table_destroy (state->durations); + g_free (state); +} + +static State * +state_new (ClutterState *clutter_state, + const gchar *name) +{ + State *state; + + state = g_new0 (State, 1); + state->clutter_state = clutter_state; + state->name = name; + state->animators = g_array_new (TRUE, TRUE, sizeof (StateAnimator)); + state->durations = g_hash_table_new (g_direct_hash, g_direct_equal); + + return state; +} + +static void +clutter_state_finalize (GObject *object) +{ + ClutterStatePrivate *priv = CLUTTER_STATE (object)->priv; + + g_hash_table_destroy (priv->states); + + g_object_unref (priv->timeline); + g_object_unref (priv->slave_timeline); + + G_OBJECT_CLASS (clutter_state_parent_class)->finalize (object); +} + +static void +clutter_state_completed (ClutterTimeline *timeline, + ClutterState *state) +{ + ClutterStatePrivate *priv = state->priv; + + if (priv->current_animator) + { + clutter_animator_set_timeline (priv->current_animator, NULL); + priv->current_animator = NULL; + } + + g_signal_emit (state, state_signals[COMPLETED], 0); +} + +static void +clutter_state_new_frame (ClutterTimeline *timeline, + gint msecs, + ClutterState *state) +{ + GList *k; + gdouble progress; + const gchar *curprop = NULL; + GObject *curobj = NULL; + gboolean found_specific = FALSE; + + if (state->priv->current_animator) + return; + + progress = clutter_timeline_get_progress (timeline); + + for (k = state->priv->target_state->keys; k; k = k->next) + { + ClutterStateKey *key = k->data; + GValue value = {0,}; + gdouble sub_progress; + + if ((curprop && !(curprop == key->property_name)) || + key->object != curobj) + { + curprop = key->property_name; + curobj = key->object; + found_specific = FALSE; + } + + if (!found_specific) + { + if (key->source_state && + key->source_state->name && + g_str_equal (state->priv->source_state_name, + key->source_state->name)) + { + found_specific = TRUE; + } + + if (found_specific || key->source_state == NULL) + { + sub_progress = (progress - key->pre_delay) / + (1.0 - (key->pre_delay + key->post_delay)); + + if (sub_progress >= 0.0) + { + if (sub_progress >= 1.0) + sub_progress = 1.0; + clutter_timeline_advance (state->priv->slave_timeline, + sub_progress * SLAVE_TIMELINE_LENGTH); + sub_progress = clutter_alpha_get_alpha (key->alpha); + + g_value_init (&value, + clutter_interval_get_value_type (key->interval)); + clutter_interval_compute_value (key->interval, sub_progress, + &value); + g_object_set_property (key->object, key->property_name, + &value); + g_value_unset (&value); + } + /* XXX: should the target value of the default destination be + * used even when found a specific source_state key? + */ + } + } + } +} + + +/** + * clutter_state_change: + * @state_name: a #ClutterState + * @animate: whether we should animate the transition or not + * + * Change the current state of #ClutterState to @state_name + * + * If @animate is %FALSE, the state transition will happen immediately; + * otherwise, the state transition will be animated over the duration + * set using clutter_state_set_duration() + * + * Return value: the #ClutterTimeline that drives the state transition + * + * Since: 1.4 + */ +ClutterTimeline * +clutter_state_change (ClutterState *this, + const gchar *target_state_name, + gboolean animate) +{ + ClutterStatePrivate *priv = this->priv; + State *state; + + g_return_val_if_fail (CLUTTER_IS_STATE (this), NULL); + g_return_val_if_fail (target_state_name != NULL, NULL); + + if (target_state_name == NULL) + target_state_name = g_intern_static_string ("default"); + + target_state_name = g_intern_string (target_state_name); + if (priv->target_state_name == NULL) + priv->target_state_name = g_intern_static_string ("default"); + + if (target_state_name == priv->target_state_name) + { + /* Avoid transitioning if the desired state is already current */ + return priv->timeline; + } + + if (priv->current_animator) + { + clutter_animator_set_timeline (priv->current_animator, NULL); + priv->current_animator = NULL; + } + + priv->source_state_name = priv->target_state_name; + priv->target_state_name = target_state_name; + + g_object_notify (G_OBJECT (this), "target-state"); + + if (animate) + { + guint duration; + + duration = clutter_state_get_duration (this, + priv->source_state_name, + priv->target_state_name); + clutter_timeline_set_duration (priv->timeline, duration); + } + else + clutter_timeline_set_duration (priv->timeline, 1); + + state = g_hash_table_lookup (priv->states, target_state_name); + g_return_val_if_fail (state, NULL); + + { + ClutterAnimator *animator; + + animator = clutter_state_get_animator (this, + priv->source_state_name, + priv->target_state_name); + if (animator != NULL) + { + priv->current_animator = animator; + clutter_animator_set_timeline (animator, priv->timeline); + + clutter_timeline_stop (priv->timeline); + clutter_timeline_rewind (priv->timeline); + clutter_timeline_start (priv->timeline); + + return priv->timeline; + } + } + + if (state) + { + GList *k; + + for (k = state->keys; k; k = k->next) + { + ClutterStateKey *key = k->data; + GValue initial = {0,}; + + g_value_init (&initial, + clutter_interval_get_value_type (key->interval)); + + g_object_get_property (key->object, key->property_name, &initial); + if (clutter_alpha_get_mode (key->alpha) != key->mode) + clutter_alpha_set_mode (key->alpha, key->mode); + + clutter_interval_set_initial_value (key->interval, &initial); + clutter_interval_set_final_value (key->interval, &key->value); + + g_value_unset (&initial); + } + + priv->target_state = state; + clutter_timeline_rewind (priv->timeline); + clutter_timeline_start (priv->timeline); + } + else + g_warning ("State '%s' not found", target_state_name); + + return priv->timeline; +} + + +/** + * clutter_state_set: + * @state: a #ClutterState instance. + * @source_state_name: the name of the source state keys are being added for + * @target_state_name: the name of the target state keys are being added for + * @first_object: a #GObject + * @first_property_name: a property of @first_object to specify a key for + * @first_mode: the id of the alpha function to use + * @Varargs: the value @first_property_name should have in @state_name, + * followed by object, property name, mode, value tuples, terminated + * by %NULL + * + * Adds multiple keys to a named state of a #ClutterState instance, specifying + * the easing mode and value a given property of an object should have at a + * given progress of the animation. + * + * The mode specified is the easing mode used when going to from the previous + * key to the specified key. + * + * If a given object, state_name, property tuple already exist then the mode + * and value will be replaced with the new specified values. + * + * If a the property_name is prefixed with "delayed::" two additional + * arguments per key are expected: a value relative to the full state time + * to pause before transitioning and a similar value to pause after + * transitioning. + * + * Since: 1.4 + */ +void +clutter_state_set (ClutterState *state, + const gchar *source_state_name, + const gchar *target_state_name, + gpointer first_object, + const gchar *first_property_name, + gulong first_mode, + ...) +{ + GObjectClass *klass; + gpointer object; + const gchar *property_name; + gulong mode; + va_list args; + + object = first_object; + + property_name = first_property_name; + mode = first_mode; + va_start (args, first_mode); + + g_return_if_fail (G_IS_OBJECT (first_object)); + g_return_if_fail (first_property_name); + + while (object != NULL) + { + GParamSpec *pspec; + GValue value = { 0, }; + gchar *error = NULL; + const gchar *real_property_name = property_name; + + klass = G_OBJECT_GET_CLASS (object); + + if (g_str_has_prefix (property_name, "delayed::")) + real_property_name = strstr (property_name, "::") + 2; + + pspec = g_object_class_find_property (klass, real_property_name); + if (pspec == NULL) + { + g_warning ("Cannot bind property '%s': objects of type '%s' " + "do not have this property", + real_property_name, + G_OBJECT_TYPE_NAME (object)); + break; + } + +#if GLIB_CHECK_VERSION (2, 23, 2) + G_VALUE_COLLECT_INIT (&value, G_PARAM_SPEC_VALUE_TYPE (pspec), + args, 0, + &error); +#else + g_value_init (&value, G_PARAM_SPEC_VALUE_TYPE (pspec)); + G_VALUE_COLLECT (&value, args, 0, &error); +#endif /* GLIB_CHECK_VERSION */ + + if (error != NULL) + { + g_warning ("%s: %s", G_STRLOC, error); + g_free (error); + break; + } + + if (g_str_has_prefix (property_name, "delayed::")) + { + gdouble pre_delay = va_arg (args, gdouble); + gdouble post_delay = va_arg (args, gdouble); + + clutter_state_set_key (state, + source_state_name, + target_state_name, + object, + real_property_name, + mode, + &value, + pre_delay, + post_delay); + } + else + { + clutter_state_set_key (state, + source_state_name, + target_state_name, + object, + property_name, + mode, + &value, + 0.0, + 0.0); + } + + object = va_arg (args, gpointer); + if (object != NULL) + { + property_name = va_arg (args, gchar*); + mode = va_arg (args, gulong); + } + } + + va_end (args); +} + +static void +clutter_state_set_key_internal (ClutterState *state, + ClutterStateKey *key) +{ + State *target_state = key->target_state; + GList *old_item = NULL; + + if ((old_item = g_list_find_custom (target_state->keys, + key, + sort_props_func))) + { + ClutterStateKey *old_key = old_item->data; + + target_state->keys = g_list_remove (target_state->keys, old_key); + clutter_state_key_free (old_key); + } + + target_state->keys = g_list_insert_sorted (target_state->keys, + key, + sort_props_func); +} + +/** + * clutter_state_set_key: + * @this: a #ClutterState instance. + * @source_state_name: the source transition to specify transition for + * @target_state_name: the name of the transition to set a key value for. + * @object: the #GObject to set a key for + * @property_name: the property to set a key for + * @mode: the id of the alpha function to use + * @value: the value for property_name of object in state_name + * @pre_delay: relative time of the transition to be idle in the beginning + * of the transition + * @post_delay: relative time of the transition to be idle in the end of + * the transition + * + * Sets one specific end key for a state_name, object, property_name + * combination. + * + * Return value: the #ClutterState instance, allowing chaining of + * multiple calls + * + * Since: 1.4 + */ +ClutterState * +clutter_state_set_key (ClutterState *this, + const gchar *source_state_name, + const gchar *target_state_name, + GObject *object, + const gchar *property_name, + guint mode, + const GValue *value, + gdouble pre_delay, + gdouble post_delay) +{ + GParamSpec *pspec; + ClutterStateKey *state_key; + State *source_state = NULL; + State *target_state; + + g_return_val_if_fail (CLUTTER_IS_STATE (this), NULL); + g_return_val_if_fail (G_IS_OBJECT (object), NULL); + g_return_val_if_fail (property_name, NULL); + g_return_val_if_fail (value, NULL); + + if (target_state_name == NULL) + target_state_name = "default"; + + pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (object), + property_name); + if (pspec == NULL || !(pspec->flags & G_PARAM_WRITABLE)) + { + g_warning ("No writable property '%s' for object type '%s' found", + property_name, + G_OBJECT_TYPE_NAME (object)); + return NULL; + } + + source_state_name = g_intern_string (source_state_name); + target_state_name = g_intern_string (target_state_name); + property_name = g_intern_string (property_name); + + target_state = g_hash_table_lookup (this->priv->states, + target_state_name); + + if (!target_state) + { + target_state = state_new (this, target_state_name); + g_hash_table_insert (this->priv->states, + (gpointer)target_state_name, target_state); + } + + if (source_state_name) + { + source_state = g_hash_table_lookup (this->priv->states, + source_state_name); + if (!source_state) + { + source_state = state_new (this, source_state_name); + g_hash_table_insert (this->priv->states, + (gpointer)source_state_name, source_state); + } + } + + state_key = clutter_state_key_new (target_state, + object, property_name, pspec, + mode); + + state_key->source_state = source_state; + state_key->pre_delay = pre_delay; + state_key->post_delay = post_delay; + + g_value_init (&state_key->value, G_VALUE_TYPE (value)); + g_value_copy (value, &state_key->value); + + clutter_state_set_key_internal (this, state_key); + + return this; +} + +/** + * clutter_state_get_states: + * @state: a #ClutterState instance. + * + * Gets a list of all the state names managed by this #ClutterState. + * + * Return value: (transfer container) (element-type utf8): a newly allocated + * #GList of state names. The contents of the returned #GList are owned + * by the #ClutterState and should not be modified or freed. Use + * g_list_free() to free the resources allocated by the returned list when + * done using it + * + * Since: 1.4 + */ +GList * +clutter_state_get_states (ClutterState *state) +{ + g_return_val_if_fail (CLUTTER_IS_STATE (state), NULL); + + return g_hash_table_get_keys (state->priv->states); +} + +/** + * clutter_state_get_keys: + * @state: a #ClutterState instance. + * @source_state_name: (allow-none): the source transition name to query, + * or %NULL for all source states + * @target_state_name: (allow-none): the target transition name to query, + * or %NULL for all target states + * @object: (allow-none): the specific object instance to list keys for, + * or %NULL for all managed objects + * @property_name: (allow-none): the property name to search for, or %NULL + * for all properties. + * + * Returns a list of pointers to opaque structures with accessor functions + * that describe the keys added to an animator. + * + * Return value: (transfer container) (element-type ClutterStateKey): a + * newly allocated #GList of #ClutterStateKeys. The contents of + * the returned list are owned by the #ClutterState and should not be + * modified or freed. Use g_list_free() to free the resources allocated + * by the returned list when done using it + * + * Since: 1.4 + */ +GList * +clutter_state_get_keys (ClutterState *state, + const gchar *source_state_name, + const gchar *target_state_name, + GObject *object, + const gchar *property_name) +{ + GList *s, *state_list; + GList *targets = NULL; + State *source_state = NULL; + + g_return_val_if_fail (CLUTTER_IS_STATE (state), NULL); + + source_state_name = g_intern_string (source_state_name); + target_state_name = g_intern_string (target_state_name); + property_name = g_intern_string (property_name); + + if (target_state_name != NULL) + state_list = g_list_append (NULL, (gpointer) target_state_name); + else + state_list = clutter_state_get_states (state); + + if (source_state_name) + source_state = g_hash_table_lookup (state->priv->states, + source_state_name); + + for (s = state_list; s; s=s->next) + { + State *target_state; + target_state = g_hash_table_lookup (state->priv->states, s->data); + + if (target_state) + { + GList *k; + + for (k = target_state->keys; k; k = k->next) + { + ClutterStateKey *key = k->data; + + if ( (object == NULL || (object == key->object)) + && (source_state_name == NULL + ||source_state == key->source_state) + && (property_name == NULL + ||(property_name == key->property_name)) + ) + { + targets = g_list_prepend (targets, key); + } + } + } + } + + g_list_free (state_list); + + return g_list_reverse (targets); +} + + +/** + * clutter_state_remove_key: + * @state: a #ClutterState instance. + * @source_state_name: (allow-none): the source state name to query, + * or %NULL for all source states + * @target_state_name: (allow-none): the target state name to query, + * or %NULL for all target states + * @object: (allow-none): the specific object instance to list keys for, + * or %NULL for all managed objects + * @property_name: (allow-none): the property name to search for, + * or %NULL for all properties. + * + * Removes all keys matching the search criteria passed in arguments. + * + * Since: 1.4 + */ +void +clutter_state_remove_key (ClutterState *state, + const gchar *source_state_name, + const gchar *target_state_name, + GObject *object, + const gchar *property_name) +{ + g_return_if_fail (CLUTTER_IS_STATE (state)); + + clutter_state_remove_key_internal (state, + source_state_name, target_state_name, + object, property_name, + FALSE); +} + +/** + * clutter_state_get_timeline: + * @state: a #ClutterState + * + * Gets the timeline driving the #ClutterState + * + * Return value: (transfer none): the #ClutterTimeline that drives + * the state change animations. The returned timeline is owned + * by the #ClutterState and it should not be unreferenced directly + * + * Since: 1.4 + */ +ClutterTimeline * +clutter_state_get_timeline (ClutterState *state) +{ + g_return_val_if_fail (CLUTTER_IS_STATE (state), NULL); + + return state->priv->timeline; +} + + +static void +clutter_state_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ClutterState *state = CLUTTER_STATE (object); + + switch (prop_id) + { + case PROP_TARGET_STATE: + clutter_state_change (state, g_value_get_string (value), TRUE); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +clutter_state_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ClutterState *state = CLUTTER_STATE (object); + switch (prop_id) + { + case PROP_TARGET_STATE: + g_value_set_string (value, + clutter_state_get_target_state (state)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +clutter_state_class_init (ClutterStateClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GParamSpec *pspec; + + g_type_class_add_private (klass, sizeof (ClutterStatePrivate)); + + gobject_class->finalize = clutter_state_finalize; + gobject_class->set_property = clutter_state_set_property; + gobject_class->get_property = clutter_state_get_property; + + /** + * ClutterState::completed: + * @state: the #ClutterState that emitted the signal + * + * The ::completed signal is emitted when a #ClutterState reaches + * the target state specified by clutter_state_change() + * + * Since: 1.4 + */ + state_signals[COMPLETED] = + g_signal_new (I_("completed"), + G_TYPE_FROM_CLASS (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ClutterStateClass, completed), + NULL, NULL, + clutter_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + /** + * ClutterState:target-state: + * + * The currently set target state, setting it causes the + * state machine to transition to the new state, use + * clutter_state_change() directly to directly jump to + * a state. + */ + pspec = g_param_spec_string ("target-state", + "Target State", + "Currently set state", + "default", + CLUTTER_PARAM_READWRITE); + g_object_class_install_property (gobject_class, PROP_TARGET_STATE, pspec); +} + +static void +clutter_state_init (ClutterState *self) +{ + ClutterStatePrivate *priv; + + priv = self->priv = CLUTTER_STATE_GET_PRIVATE (self); + + priv->states = g_hash_table_new_full (g_direct_hash, g_direct_equal, + NULL, + state_free); + + self->priv->source_state_name = NULL; + self->priv->target_state_name = NULL; + + self->priv->duration = 1000; + + priv->timeline = clutter_timeline_new (1000); + g_signal_connect (priv->timeline, "new-frame", + G_CALLBACK (clutter_state_new_frame), + self); + g_signal_connect (priv->timeline, "completed", + G_CALLBACK (clutter_state_completed), + self); + + priv->slave_timeline = clutter_timeline_new (SLAVE_TIMELINE_LENGTH); +} + + +/** + * clutter_state_get_animator: + * @state: a #ClutterState instance. + * @source_state_name: the name of a source state + * @target_state_name: the name of a target state + * + * Retrieves the #ClutterAnimator that is being used for transitioning + * between the two states, if any has been set + * + * Return value: (transfer none): a #ClutterAnimator instance, or %NULL + * + * Since: 1.4 + */ +ClutterAnimator * +clutter_state_get_animator (ClutterState *state, + const gchar *source_state_name, + const gchar *target_state_name) +{ + State *target_state; + StateAnimator *animators; + gint i; + + g_return_val_if_fail (CLUTTER_IS_STATE (state), NULL); + + source_state_name = g_intern_string (source_state_name); + if (source_state_name == g_intern_static_string ("default") || + source_state_name == g_intern_static_string ("")) + source_state_name = NULL; + + target_state_name = g_intern_string (target_state_name); + + target_state = g_hash_table_lookup (state->priv->states, target_state_name); + if (target_state == NULL) + return NULL; + + animators = (StateAnimator*)target_state->animators->data; + + for (i = 0; animators[i].animator; i++) + { + if (animators[i].source_state_name == source_state_name) + return animators[i].animator; + } + + return NULL; +} + +/** + * clutter_state_set_animator: + * @state: a #ClutterState instance. + * @source_state_name: the name of a source state + * @target_state_name: the name of a target state + * @animator: (allow-none): a #ClutterAnimator instance, or %NULL to + * unset an existing #ClutterAnimator + * + * Specifies a #ClutterAnimator to be used when transitioning between + * the two named states. + * + * The @animator allows specifying a transition between the state that is + * more elaborate than the basic transitions other allowed by the simple + * tweening of properties defined in the #ClutterState keys. + * + * If @animator is %NULL it will unset an existing animator. + * + * #ClutterState will take a reference on the passed @animator, if any + * + * Since: 1.4 + */ +void +clutter_state_set_animator (ClutterState *state, + const gchar *source_state_name, + const gchar *target_state_name, + ClutterAnimator *animator) +{ + State *target_state; + StateAnimator *animators; + gint i; + + g_return_if_fail (CLUTTER_IS_STATE (state)); + + source_state_name = g_intern_string (source_state_name); + target_state_name = g_intern_string (target_state_name); + + target_state = g_hash_table_lookup (state->priv->states, target_state_name); + if (target_state == NULL) + return; + + animators = (StateAnimator *) target_state->animators->data; + for (i = 0; animators[i].animator; i++) + { + if (animators[i].source_state_name == source_state_name) + { + g_object_unref (animators[i].animator); + + if (animator != NULL) + animators[i].animator = g_object_ref (animator); + else + { + /* remove the matched animator if passed NULL */ + g_array_remove_index (target_state->animators, i); + } + + return; + } + } + + if (animator != NULL) + { + StateAnimator state_animator = + {source_state_name, g_object_ref (animator)}; + g_array_append_val (target_state->animators, state_animator); + } +} + +static gpointer +clutter_state_key_copy (gpointer boxed) +{ + if (boxed != NULL) + { + ClutterStateKey *key = boxed; + + key->ref_count += 1; + } + + return boxed; +} + +GType +clutter_state_key_get_type (void) +{ + static GType our_type = 0; + + if (!our_type) + our_type = g_boxed_type_register_static (I_("ClutterStateKey"), + clutter_state_key_copy, + clutter_state_key_free); + + return our_type; +} + +/** + * clutter_state_key_get_pre_delay: + * @state_key: a #ClutterStateKey + * + * Retrieves the pause before transitioning starts as a fraction of + * the total transition time. + * + * Return value: the pre delay used before starting the transition. + * + * Since: 1.4 + */ +gdouble +clutter_state_key_get_pre_delay (const ClutterStateKey *state_key) +{ + g_return_val_if_fail (state_key != NULL, 0.0); + + return state_key->pre_delay; +} + +/** + * clutter_state_key_get_post_delay: + * @state_key: a #ClutterStateKey + * + * Retrieves the duration of the pause after transitioning is complete + * as a fraction of the total transition time. + * + * Return value: the post delay, used after doing the transition. + * + * Since: 1.4 + */ +gdouble +clutter_state_key_get_post_delay (const ClutterStateKey *state_key) +{ + g_return_val_if_fail (state_key != NULL, 0.0); + + return state_key->post_delay; +} + +/** + * clutter_state_key_get_mode: + * @state_key: a #ClutterStateKey + * + * Retrieves the easing mode used for @state_key. + * + * Return value: the mode of a #ClutterStateKey + * + * Since: 1.4 + */ +gulong +clutter_state_key_get_mode (const ClutterStateKey *state_key) +{ + g_return_val_if_fail (state_key != NULL, 0); + + return state_key->mode; +} + +/** + * clutter_state_key_get_value: + * @state_key: a #ClutterStateKey + * @value: a #GValue initialized with the correct type for the @state_key + * + * Copies of the value for a #ClutterStateKey into @value + * + * The #GValue needs to be already initialized for the value type + * of the property or to a transformable type + * + * Since: 1.4 + */ +void +clutter_state_key_get_value (const ClutterStateKey *state_key, + GValue *value) +{ + g_return_if_fail (state_key != NULL); + g_return_if_fail (value != NULL); + g_return_if_fail (G_VALUE_TYPE (value) != G_TYPE_INVALID); + + if (!g_type_is_a (G_VALUE_TYPE (&state_key->value), G_VALUE_TYPE (value))) + { + if (g_value_type_compatible (G_VALUE_TYPE (&state_key->value), + G_VALUE_TYPE (value))) + { + g_value_copy (&state_key->value, value); + return; + } + + if (g_value_type_transformable (G_VALUE_TYPE (&state_key->value), + G_VALUE_TYPE (value))) + { + if (g_value_transform (&state_key->value, value)) + return; + } + + g_warning ("%s: Unable to convert from %s to %s for the " + "property '%s' of object %s in the state key", + G_STRLOC, + g_type_name (G_VALUE_TYPE (&state_key->value)), + g_type_name (G_VALUE_TYPE (value)), + state_key->property_name, + G_OBJECT_TYPE_NAME (state_key->object)); + } + else + g_value_copy (&state_key->value, value); +} + +/** + * clutter_state_key_get_object: + * @state_key: a #ClutterStateKey + * + * Retrieves the object instance this #ClutterStateKey applies to. + * + * Return value: (transfer none): the object this state key applies to. + * + * Since: 1.4 + */ +GObject * +clutter_state_key_get_object (const ClutterStateKey *state_key) +{ + g_return_val_if_fail (state_key, NULL); + + return state_key->object; +} + +/** + * clutter_state_key_get_property_name: + * @state_key: a #ClutterStateKey + * + * Retrieves the name of the property this #ClutterStateKey applies to + * + * Return value: the name of the property. The returned string is owned + * by the #ClutterStateKey and should never be modified or freed + * + * Since: 1.4 + */ +G_CONST_RETURN gchar * +clutter_state_key_get_property_name (const ClutterStateKey *state_key) +{ + g_return_val_if_fail (state_key, NULL); + + return state_key->property_name; +} + +/** + * clutter_state_key_get_source_state_name: + * @state_key: a #ClutterStateKey + * + * Retrieves the name of the source state of the @state_key + * + * Return value: the name of the source state for this key, or %NULL + * if this is the generic state key for the given property when + * transitioning to the target state. The returned string is owned + * by the #ClutterStateKey and should never be modified or freed + * + * Since: 1.4 + */ +G_CONST_RETURN gchar * +clutter_state_key_get_source_state_name (const ClutterStateKey *state_key) +{ + g_return_val_if_fail (state_key, NULL); + + if (state_key->source_state != NULL) + return state_key->source_state->name; + + return NULL; +} + +/** + * clutter_state_key_get_target_state_name: + * @state_key: a #ClutterStateKey + * + * Get the name of the source state this #ClutterStateKey contains, + * or NULL if this is the generic state key for the given property + * when transitioning to the target state. + * + * Return value: the name of the source state for this key, or NULL if + * the key is generic + * + * Since: 1.4 + */ +G_CONST_RETURN gchar * +clutter_state_key_get_target_state_name (const ClutterStateKey *state_key) +{ + g_return_val_if_fail (state_key, NULL); + + return state_key->target_state->name; +} + +/** + * clutter_state_set_duration: + * @state: a #ClutterState + * @source_state_name: the name of the source state to set duration for or NULL + * @target_state_name: the name of the source state to set duration for or NULL + * @duration: milliseconds for transition between source_state and target_state. + * + * If both state names are NULL the default duration for ClutterState is set, + * if only target_state_name is specified this becomes the default duration + * for transitions to this state. When both are specified the change only + * applies to this transition. + * + * Since: 1.4 + */ +void +clutter_state_set_duration (ClutterState *state, + const gchar *source_state_name, + const gchar *target_state_name, + guint duration) +{ + State *target_state; + + g_return_if_fail (CLUTTER_IS_STATE (state)); + + source_state_name = g_intern_string (source_state_name); + if (source_state_name == g_intern_static_string ("default") || + source_state_name == g_intern_static_string ("")) + source_state_name = NULL; + + target_state_name = g_intern_string (target_state_name); + if (target_state_name == g_intern_static_string ("default") || + target_state_name == g_intern_static_string ("")) + target_state_name = NULL; + + if (target_state_name == NULL) + { + state->priv->duration = duration; + return; + } + + target_state = g_hash_table_lookup (state->priv->states, target_state_name); + if (target_state != NULL) + { + if (source_state_name != NULL) + g_hash_table_insert (target_state->durations, + (gpointer) source_state_name, + GINT_TO_POINTER (duration)); + else + g_hash_table_insert (target_state->durations, + NULL, + GINT_TO_POINTER (duration)); + } +} + +/** + * clutter_state_get_duration: + * @state: a #ClutterState + * @source_state_name: (allow-none): the name of the source state to + * get the duration of, or %NULL + * @target_state_name: (allow-none): the name of the source state to + * get the duration of, or %NULL + * + * Queries the duration used for transitions between a source and + * target state pair + * + * The semantics for the query are the same as the semantics used for + * setting the duration with clutter_state_set_duration() + * + * Return value: the duration, in milliseconds + * + * Since: 1.4 + */ +guint +clutter_state_get_duration (ClutterState *state, + const gchar *source_state_name, + const gchar *target_state_name) +{ + State *target_state; + guint ret = 0; + + g_return_val_if_fail (CLUTTER_IS_STATE (state), 0); + + source_state_name = g_intern_string (source_state_name); + if (source_state_name == g_intern_static_string ("default") || + source_state_name == g_intern_static_string ("")) + source_state_name = NULL; + + target_state_name = g_intern_string (target_state_name); + if (target_state_name == g_intern_static_string ("default") || + target_state_name == g_intern_static_string ("")) + target_state_name = NULL; + + if (target_state_name == NULL) + return state->priv->duration; + + target_state = g_hash_table_lookup (state->priv->states, + target_state_name); + if (target_state != NULL) + { + if (source_state_name) + ret = GPOINTER_TO_INT (g_hash_table_lookup (target_state->durations, + source_state_name)); + else + ret = GPOINTER_TO_INT (g_hash_table_lookup (target_state->durations, + NULL)); + } + + if (!ret) + ret = state->priv->duration; + + return ret; +} + +/** + * clutter_state_get_target_state: + * @state: a #ClutterState + * + * Queries the currently set target state. + * + * During a transition this function will also return the current target. + * + * This function is useful when called from handlers of the + * #ClutterState::completed signal. + * + * Return value: a string containing the target state. The returned string + * is owned by the #ClutterState and should not be modified or freed + * + * Since: 1.4 + */ +G_CONST_RETURN gchar * +clutter_state_get_target_state (ClutterState *state) +{ + g_return_val_if_fail (CLUTTER_IS_STATE (state), NULL); + + return state->priv->target_state_name; +} + +typedef struct _ParseClosure { + ClutterState *state; + ClutterScript *script; + + GValue *value; + + gboolean result; +} ParseClosure; + +static void +parse_state_transition (JsonArray *array, + guint index_, + JsonNode *element, + gpointer data) +{ + ParseClosure *clos = data; + ClutterStatePrivate *priv = clos->state->priv; + JsonObject *object; + const gchar *source_name, *target_name; + State *source_state, *target_state; + JsonArray *keys; + GSList *valid_keys = NULL; + GList *k; + + if (JSON_NODE_TYPE (element) != JSON_NODE_OBJECT) + { + g_warning ("The 'transitions' member of a ClutterState description " + "should be an array of objects, but the element %d of the " + "array is of type '%s'. The element will be ignored.", + index_, + json_node_type_name (element)); + return; + } + + object = json_node_get_object (element); + + if (!json_object_has_member (object, "source") || + !json_object_has_member (object, "target") || + !json_object_has_member (object, "keys")) + { + g_warning ("The transition description at index %d is missing one " + "of the mandatory members: source, target and keys", + index_); + return; + } + + source_name = json_object_get_string_member (object, "source"); + target_name = json_object_get_string_member (object, "target"); + + keys = json_object_get_array_member (object, "keys"); + if (keys == NULL) + { + g_warning ("The transition description at index %d has an invalid " + "key member of type '%s' when an array was expected.", + index_, + json_node_type_name (json_object_get_member (object, "keys"))); + return; + } + + source_name = g_intern_string (source_name); + source_state = g_hash_table_lookup (priv->states, source_name); + if (source_state == NULL) + { + source_state = state_new (clos->state, source_name); + g_hash_table_insert (priv->states, (gpointer) source_name, source_state); + } + + target_name = g_intern_string (target_name); + target_state = g_hash_table_lookup (priv->states, target_name); + if (target_state == NULL) + { + target_state = state_new (clos->state, target_name); + g_hash_table_insert (priv->states, (gpointer) target_name, target_state); + } + + if (json_object_has_member (object, "duration")) + { + guint duration = json_object_get_int_member (object, "duration"); + + clutter_state_set_duration (clos->state, + source_name, + target_name, + duration); + } + + if (json_object_has_member (object, "animator")) + { + const gchar *id = json_object_get_string_member (object, "animator"); + GObject *animator; + + animator = clutter_script_get_object (clos->script, id); + if (animator == NULL) + { + g_warning ("No object with id '%s' has been defined.", id); + return; + } + + clutter_state_set_animator (clos->state, + source_name, + target_name, + CLUTTER_ANIMATOR (animator)); + } + + if (G_IS_VALUE (clos->value)) + valid_keys = g_slist_reverse (g_value_get_pointer (clos->value)); + else + g_value_init (clos->value, G_TYPE_POINTER); + + for (k = json_array_get_elements (keys); + k != NULL; + k = k->next) + { + JsonNode *node = k->data; + JsonArray *key = json_node_get_array (node); + ClutterStateKey *state_key; + GObject *gobject; + GParamSpec *pspec; + const gchar *id; + const gchar *property; + gulong mode; + gboolean res; + + id = json_array_get_string_element (key, 0); + gobject = clutter_script_get_object (clos->script, id); + if (gobject == NULL) + { + g_warning ("No object with id '%s' has been defined.", id); + continue; + } + + property = json_array_get_string_element (key, 1); + pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (gobject), + property); + if (pspec == NULL) + { + g_warning ("The object of type '%s' and name '%s' has no " + "property named '%s'.", + G_OBJECT_TYPE_NAME (gobject), + id, + property); + continue; + } + + mode = clutter_script_resolve_animation_mode (json_array_get_element (key, 2)); + + state_key = clutter_state_key_new (target_state, + gobject, property, pspec, + mode); + + res = clutter_script_parse_node (clos->script, + &(state_key->value), + property, + json_array_get_element (key, 3), + pspec); + if (!res) + { + g_warning ("Unable to parse the key value for the " + "property '%s' of object '%s' at index %d", + property, + id, + index_); + clutter_state_key_free (state_key); + continue; + } + + if (json_array_get_length (key) == 5) + { + state_key->pre_delay = json_array_get_double_element (key, 4); + state_key->post_delay = 0.0; + } + else if (json_array_get_length (key) == 6) + { + state_key->pre_delay = json_array_get_double_element (key, 4); + state_key->post_delay = json_array_get_double_element (key, 5); + } + else + { + state_key->pre_delay = 0.0; + state_key->post_delay = 0.0; + } + + state_key->source_state = source_state; + + valid_keys = g_slist_prepend (valid_keys, state_key); + } + + g_value_set_pointer (clos->value, g_slist_reverse (valid_keys)); + + clos->result = TRUE; +} + +static gboolean +clutter_state_parse_custom_node (ClutterScriptable *scriptable, + ClutterScript *script, + GValue *value, + const gchar *name, + JsonNode *node) +{ + ParseClosure clos; + + if (strcmp (name, "transitions") != 0) + return FALSE; + + if (JSON_NODE_TYPE (node) != JSON_NODE_ARRAY) + return FALSE; + + clos.state = CLUTTER_STATE (scriptable); + clos.script = script; + clos.value = value; + clos.result = FALSE; + + json_array_foreach_element (json_node_get_array (node), + parse_state_transition, + &clos); + + return clos.result; +} + +static void +clutter_state_set_custom_property (ClutterScriptable *scriptable, + ClutterScript *script, + const gchar *name, + const GValue *value) +{ + if (strcmp (name, "transitions") == 0) + { + ClutterState *state = CLUTTER_STATE (scriptable); + GSList *keys = g_value_get_pointer (value); + GSList *k; + + for (k = keys; k != NULL; k = k->next) + clutter_state_set_key_internal (state, k->data); + + g_slist_free (keys); + } + else + g_object_set_property (G_OBJECT (scriptable), name, value); +} + +static void +clutter_scriptable_iface_init (ClutterScriptableIface *iface) +{ + iface->parse_custom_node = clutter_state_parse_custom_node; + iface->set_custom_property = clutter_state_set_custom_property; +} diff --git a/clutter/clutter-state.h b/clutter/clutter-state.h new file mode 100644 index 000000000..59e61538e --- /dev/null +++ b/clutter/clutter-state.h @@ -0,0 +1,158 @@ +/* + * Clutter. + * + * An OpenGL based 'interactive canvas' library. + * + * Authored By Øyvind Kolås + * + * Copyright (C) 2009 Intel Corporation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#ifndef __CLUTTER_STATE_H__ +#define __CLUTTER_STATE_H__ + +#include +#include + +G_BEGIN_DECLS + +#define CLUTTER_TYPE_STATE (clutter_state_get_type ()) +#define CLUTTER_STATE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CLUTTER_TYPE_STATE, ClutterState)) +#define CLUTTER_STATE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CLUTTER_TYPE_STATE, ClutterStateClass)) +#define CLUTTER_IS_STATE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CLUTTER_TYPE_STATE)) +#define CLUTTER_IS_STATE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CLUTTER_TYPE_STATE)) +#define CLUTTER_STATE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CLUTTER_TYPE_STATE, ClutterStateClass)) + +typedef struct _ClutterState ClutterState; +typedef struct _ClutterStatePrivate ClutterStatePrivate; +typedef struct _ClutterStateClass ClutterStateClass; + +/** + * ClutterStateKey: + * + * ClutterStateKey is an opaque structure whose + * members cannot be accessed directly + * + * Since: 1.4 + */ +typedef struct _ClutterStateKey ClutterStateKey; + +/** + * ClutterState: + * + * The ClutterState structure contains only + * private data and should be accessed using the provided API + * + * Since: 1.4 + */ +struct _ClutterState +{ + /*< private >*/ + GObject parent; + ClutterStatePrivate *priv; +}; + +/** + * ClutterStateClass: + * @completed: class handler for the #ClutterState::completed signal + * + * The ClutterStateClass structure contains + * only private data + * + * Since: 1.4 + */ +struct _ClutterStateClass +{ + /*< private >*/ + GObjectClass parent_class; + + /*< public >*/ + void (* completed) (ClutterState *state); + + /*< private >*/ + /* padding for future expansion */ + gpointer _padding_dummy[8]; +}; + +GType clutter_state_get_type (void) G_GNUC_CONST; +ClutterState *clutter_state_new (void); + + +ClutterTimeline * clutter_state_change (ClutterState *state, + const gchar *target_transition_name, + gboolean animate); +ClutterState * clutter_state_set_key (ClutterState *state, + const gchar *source_transition_name, + const gchar *target_transition_name, + GObject *object, + const gchar *property_name, + guint mode, + const GValue *value, + gdouble pre_delay, + gdouble post_delay); +void clutter_state_set_duration (ClutterState *state, + const gchar *source_transition_name, + const gchar *target_transition_name, + guint duration); +guint clutter_state_get_duration (ClutterState *state, + const gchar *source_transition_name, + const gchar *target_transition_name); +void clutter_state_set (ClutterState *state, + const gchar *source_transition_name, + const gchar *target_transition_name, + gpointer first_object, + const gchar *first_property_name, + gulong first_mode, + ...); +GList * clutter_state_get_states (ClutterState *state); +GList * clutter_state_get_keys (ClutterState *state, + const gchar *source_transition_name, + const gchar *target_transition_name, + GObject *object, + const gchar *property_name); +void clutter_state_remove_key (ClutterState *state, + const gchar *source_transition_name, + const gchar *target_transition_name, + GObject *object, + const gchar *property_name); +ClutterTimeline * clutter_state_get_timeline (ClutterState *state); +void clutter_state_set_animator (ClutterState *state, + const gchar *source_transition_name, + const gchar *target_transition_name, + ClutterAnimator *animator); +ClutterAnimator * clutter_state_get_animator (ClutterState *state, + const gchar *source_transition_name, + const gchar *target_transition_name); +G_CONST_RETURN gchar *clutter_state_get_target_state (ClutterState *state); + +/* + * ClutterStateKey + */ + +GType clutter_state_key_get_type (void) G_GNUC_CONST; +gdouble clutter_state_key_get_pre_delay (const ClutterStateKey *state_key); +gdouble clutter_state_key_get_post_delay (const ClutterStateKey *state_key); +gulong clutter_state_key_get_mode (const ClutterStateKey *state_key); +void clutter_state_key_get_value (const ClutterStateKey *state_key, + GValue *value); +GObject * clutter_state_key_get_object (const ClutterStateKey *state_key); +G_CONST_RETURN gchar *clutter_state_key_get_property_name (const ClutterStateKey *state_key); +G_CONST_RETURN gchar *clutter_state_key_get_source_state_name (const ClutterStateKey *state_key); +G_CONST_RETURN gchar *clutter_state_key_get_target_state_name (const ClutterStateKey *state_key); + +G_END_DECLS + +#endif /* __CLUTTER_STATE_H__ */ diff --git a/clutter/clutter.h b/clutter/clutter.h index 49c3dabe3..9779025a9 100644 --- a/clutter/clutter.h +++ b/clutter/clutter.h @@ -84,6 +84,7 @@ #include "clutter-stage.h" #include "clutter-stage-manager.h" #include "clutter-stage-window.h" +#include "clutter-state.h" #include "clutter-texture.h" #include "clutter-text.h" #include "clutter-timeline.h" diff --git a/tests/conform/Makefile.am b/tests/conform/Makefile.am index 5ad918d81..59ddea711 100644 --- a/tests/conform/Makefile.am +++ b/tests/conform/Makefile.am @@ -51,6 +51,7 @@ test_conformance_SOURCES = \ test-actor-destroy.c \ test-behaviours.c \ test-animator.c \ + test-state.c \ $(NULL) # For convenience, this provides a way to easily run individual unit tests: diff --git a/tests/conform/test-conform-main.c b/tests/conform/test-conform-main.c index 44e64d36d..939220d06 100644 --- a/tests/conform/test-conform-main.c +++ b/tests/conform/test-conform-main.c @@ -184,6 +184,7 @@ main (int argc, char **argv) TEST_CONFORM_SIMPLE ("/script", test_animator_base); TEST_CONFORM_SIMPLE ("/script", test_animator_properties); TEST_CONFORM_SIMPLE ("/script", test_animator_multi_properties); + TEST_CONFORM_SIMPLE ("/script", test_state_base); TEST_CONFORM_SIMPLE ("/behaviours", test_behaviours); diff --git a/tests/conform/test-state.c b/tests/conform/test-state.c new file mode 100644 index 000000000..0552ff9af --- /dev/null +++ b/tests/conform/test-state.c @@ -0,0 +1,59 @@ +#include + +#include "test-conform-common.h" + +void +test_state_base (TestConformSimpleFixture *fixture G_GNUC_UNUSED, + gconstpointer dummy G_GNUC_UNUSED) +{ + ClutterScript *script = clutter_script_new (); + GObject *state = NULL; + GError *error = NULL; + gchar *test_file; + GList *states, *keys; + ClutterStateKey *state_key; + guint duration; + + test_file = clutter_test_get_data_file ("test-state-1.json"); + clutter_script_load_from_file (script, test_file, &error); + if (g_test_verbose () && error) + g_print ("Error: %s\n", error->message); + + g_free (test_file); + +#if GLIB_CHECK_VERSION (2, 20, 0) + g_assert_no_error (error); +#else + g_assert (error == NULL); +#endif + + state = clutter_script_get_object (script, "state"); + g_assert (CLUTTER_IS_STATE (state)); + + states = clutter_state_get_states (CLUTTER_STATE (state)); + g_assert (states != NULL); + + g_assert (g_list_find (states, g_intern_static_string ("clicked"))); + g_list_free (states); + + duration = clutter_state_get_duration (CLUTTER_STATE (state), "base", "clicked"); + g_assert_cmpint (duration, ==, 250); + + duration = clutter_state_get_duration (CLUTTER_STATE (state), "clicked", "base"); + g_assert_cmpint (duration, ==, 150); + + keys = clutter_state_get_keys (CLUTTER_STATE (state), "base", "clicked", + clutter_script_get_object (script, "rect"), + "opacity"); + g_assert (keys != NULL); + g_assert_cmpint (g_list_length (keys), ==, 1); + + state_key = keys->data; + g_assert (clutter_state_key_get_object (state_key) == clutter_script_get_object (script, "rect")); + g_assert (clutter_state_key_get_mode (state_key) == CLUTTER_LINEAR); + g_assert_cmpstr (clutter_state_key_get_property_name (state_key), ==, "opacity"); + + g_list_free (keys); + + g_object_unref (script); +} diff --git a/tests/data/Makefile.am b/tests/data/Makefile.am index 95ff06080..2dfca3e79 100644 --- a/tests/data/Makefile.am +++ b/tests/data/Makefile.am @@ -12,6 +12,7 @@ json_files = \ test-animator-1.json \ test-animator-2.json \ test-animator-3.json \ + test-state-1.json \ $(NULL) png_files = \ diff --git a/tests/data/test-state-1.json b/tests/data/test-state-1.json new file mode 100644 index 000000000..78ab5de89 --- /dev/null +++ b/tests/data/test-state-1.json @@ -0,0 +1,33 @@ +[ + { + "type" : "ClutterRectangle", + "id" : "rect", + "width" : 100, + "height" : 100 + }, + { + "type" : "ClutterState", + "id" : "state", + + "transitions" : [ + { + "source" : "base", + "target" : "clicked", + "duration" : 250, + + "keys" : [ + [ "rect", "opacity", "linear", 128 ] + ] + }, + { + "source" : "clicked", + "target" : "base", + "duration" : 150, + + "keys" : [ + [ "rect", "opacity", "linear", 255 ] + ] + } + ] + } +] diff --git a/tests/interactive/Makefile.am b/tests/interactive/Makefile.am index 19f829177..672375340 100644 --- a/tests/interactive/Makefile.am +++ b/tests/interactive/Makefile.am @@ -20,6 +20,8 @@ UNIT_TESTS = \ test-fullscreen.c \ test-shader.c \ test-animator.c \ + test-state.c \ + test-state-animator.c \ test-unproject.c \ test-viewport.c \ test-fbo.c \ diff --git a/tests/interactive/test-state-animator.c b/tests/interactive/test-state-animator.c new file mode 100644 index 000000000..bdd88a91d --- /dev/null +++ b/tests/interactive/test-state-animator.c @@ -0,0 +1,134 @@ +#include +#include +#include +#include + +static ClutterState *state; +static ClutterAnimator *animator; + +static gboolean press_event (ClutterActor *actor, + ClutterEvent *event, + gpointer user_data) +{ + clutter_grab_pointer (actor); + clutter_state_change (state, "end", TRUE); + return TRUE; +} + +static gboolean release_event (ClutterActor *actor, + ClutterEvent *event, + gpointer user_data) +{ + clutter_state_change (state, "start", TRUE); + clutter_ungrab_pointer (); + return TRUE; +} + + +static ClutterActor *new_rect (gint r, + gint g, + gint b, + gint a) +{ + GError *error = NULL; + ClutterColor *color = clutter_color_new (r, g, b, a); + ClutterActor *rectangle = clutter_rectangle_new_with_color (color); + + gchar *file = g_build_filename (TESTS_DATADIR, "redhand.png", NULL); + rectangle = clutter_texture_new_from_file (file, &error); + if (rectangle == NULL) + g_error ("image load failed: %s", error->message); + g_free (file); + + clutter_actor_set_size (rectangle, 128, 128); + clutter_color_free (color); + return rectangle; +} + +G_MODULE_EXPORT gint +test_state_animator_main (gint argc, + gchar **argv) +{ + ClutterActor *stage; + ClutterActor *rects[40]; + gint i; + clutter_init (&argc, &argv); + + stage = clutter_stage_get_default (); + + for (i=0; i<2; i++) + { + rects[i]=new_rect (255 *(i * 1.0/40), 50, 160, 255); + clutter_container_add_actor (CLUTTER_CONTAINER (stage), rects[i]); + clutter_actor_set_anchor_point (rects[i], 64, 64); + clutter_actor_set_position (rects[i], 320.0, 240.0); + clutter_actor_set_opacity (rects[i], 0x70); + + clutter_actor_set_reactive (rects[i], TRUE); + g_signal_connect (rects[i], "button-press-event", G_CALLBACK (press_event), NULL); + g_signal_connect (rects[i], "button-release-event", G_CALLBACK (release_event), NULL); + } + + state = clutter_state_new (); + clutter_state_set (state, NULL, "start", + rects[0], "depth", CLUTTER_LINEAR, 0.0, + rects[0], "x", CLUTTER_LINEAR, 100.0, + rects[0], "y", CLUTTER_LINEAR, 300.0, + rects[1], "opacity", CLUTTER_LINEAR, 0x20, + rects[1], "scale-x", CLUTTER_LINEAR, 1.0, + rects[1], "scale-y", CLUTTER_LINEAR, 1.0, + NULL); + clutter_state_set (state, NULL, "end", + rects[0], "depth", CLUTTER_LINEAR, 200.0, + rects[0], "x", CLUTTER_LINEAR, 320.0, + rects[0], "y", CLUTTER_LINEAR, 240.0, + rects[1], "opacity", CLUTTER_LINEAR, 0xff, + rects[1], "scale-x", CLUTTER_LINEAR, 2.0, + rects[1], "scale-y", CLUTTER_LINEAR, 2.0, + NULL); + + animator = clutter_animator_new (); + clutter_animator_set (animator, + rects[0], "depth", -1, 0.0, 0.0, + rects[0], "depth", CLUTTER_LINEAR, 1.0, 275.0, + rects[0], "x", -1, 0.0, 0.0, + rects[0], "x", CLUTTER_LINEAR, 0.5, 200.0, + rects[0], "x", CLUTTER_LINEAR, 1.0, 320.0, + + rects[0], "y", -1, 0.0, 0.0, + rects[0], "y", CLUTTER_LINEAR, 0.3, 100.0, + rects[0], "y", CLUTTER_LINEAR, 1.0, 240.0, + + rects[1], "opacity", -1, 0.0, 0x20, + rects[1], "opacity", CLUTTER_LINEAR, 1.0, 0xff, + rects[1], "scale-x", -1, 0.0, 1.0, + rects[1], "scale-x", CLUTTER_LINEAR, 0.5, 2.0, + rects[1], "scale-x", CLUTTER_LINEAR, 1.0, 2.0, + rects[1], "scale-y", -1, 0.0, 1.0, + rects[1], "scale-y", CLUTTER_LINEAR, 0.5, 2.0, + rects[1], "scale-y", CLUTTER_LINEAR, 1.0, 2.0, + NULL); + + clutter_animator_property_set_ease_in (animator, G_OBJECT (rects[0]), "depth", TRUE); + clutter_animator_property_set_ease_in (animator, G_OBJECT (rects[0]), "x", TRUE); + clutter_animator_property_set_ease_in (animator, G_OBJECT (rects[0]), "y", TRUE); + clutter_animator_property_set_ease_in (animator, G_OBJECT (rects[1]), "opacity", TRUE); + clutter_animator_property_set_ease_in (animator, G_OBJECT (rects[1]), "scale-x", TRUE); + clutter_animator_property_set_ease_in (animator, G_OBJECT (rects[1]), "scale-y", TRUE); + + clutter_animator_property_set_interpolation (animator, G_OBJECT (rects[0]), "x", + CLUTTER_INTERPOLATION_CUBIC); + clutter_animator_property_set_interpolation (animator, G_OBJECT (rects[0]), "y", + CLUTTER_INTERPOLATION_CUBIC); + + clutter_state_set_animator (state, "start", "end", animator); + g_object_unref (animator); + + clutter_actor_show (stage); + clutter_state_change (state, "start", TRUE); + + clutter_main (); + g_object_unref (state); + + return EXIT_SUCCESS; +} diff --git a/tests/interactive/test-state.c b/tests/interactive/test-state.c new file mode 100644 index 000000000..064c03a4c --- /dev/null +++ b/tests/interactive/test-state.c @@ -0,0 +1,104 @@ +#include +#include +#include +#include + +static ClutterState *state; + +static gboolean press_event (ClutterActor *actor, + ClutterEvent *event, + gpointer user_data) +{ + clutter_grab_pointer (actor); + clutter_state_change (state, "end", TRUE); + return TRUE; +} + +static gboolean release_event (ClutterActor *actor, + ClutterEvent *event, + gpointer user_data) +{ + clutter_state_change (state, "start", TRUE); + clutter_ungrab_pointer (); + return TRUE; +} + +static void completed (ClutterState *sstate, + gpointer data) +{ + g_print ("Completed transitioning to state: %s\n", + clutter_state_get_target_state (sstate)); +} + +static ClutterActor *new_rect (gint r, + gint g, + gint b, + gint a) +{ + GError *error = NULL; + ClutterColor *color = clutter_color_new (r, g, b, a); + ClutterActor *rectangle = clutter_rectangle_new_with_color (color); + + gchar *file = g_build_filename (TESTS_DATADIR, "redhand.png", NULL); + rectangle = clutter_texture_new_from_file (file, &error); + if (rectangle == NULL) + g_error ("image load failed: %s", error->message); + g_free (file); + + clutter_actor_set_size (rectangle, 128, 128); + clutter_color_free (color); + return rectangle; +} + +G_MODULE_EXPORT gint +test_state_main (gint argc, + gchar **argv) +{ + ClutterActor *stage; + ClutterActor *rects[40]; + gint i; + clutter_init (&argc, &argv); + + stage = clutter_stage_get_default (); + + for (i=0; i<2; i++) + { + rects[i]=new_rect (255 *(i * 1.0/40), 50, 160, 255); + clutter_container_add_actor (CLUTTER_CONTAINER (stage), rects[i]); + clutter_actor_set_anchor_point (rects[i], 64, 64); + clutter_actor_set_position (rects[i], 320.0, 240.0); + clutter_actor_set_opacity (rects[i], 0x70); + + clutter_actor_set_reactive (rects[i], TRUE); + g_signal_connect (rects[i], "button-press-event", G_CALLBACK (press_event), NULL); + g_signal_connect (rects[i], "button-release-event", G_CALLBACK (release_event), NULL); + } + + state = clutter_state_new (); + clutter_state_set (state, NULL, "start", + rects[0], "depth", CLUTTER_LINEAR, 0.0, + rects[0], "x", CLUTTER_LINEAR, 100.0, + rects[0], "y", CLUTTER_LINEAR, 300.0, + rects[1], "opacity", CLUTTER_LINEAR, 0x20, + rects[1], "scale-x", CLUTTER_LINEAR, 1.0, + rects[1], "scale-y", CLUTTER_LINEAR, 1.0, + NULL); + clutter_state_set (state, NULL, "end", + rects[0], "depth", CLUTTER_LINEAR, 200.0, + rects[0], "x", CLUTTER_LINEAR, 320.0, + rects[0], "y", CLUTTER_LINEAR, 240.0, + rects[1], "opacity", CLUTTER_LINEAR, 0xff, + rects[1], "scale-x", CLUTTER_LINEAR, 2.0, + rects[1], "scale-y", CLUTTER_LINEAR, 2.0, + NULL); + clutter_state_set_duration (state, "start", "end", 5000); + g_signal_connect (state, "completed", G_CALLBACK (completed), NULL); + + clutter_actor_show (stage); + clutter_state_change (state, "start", TRUE); + + clutter_main (); + g_object_unref (state); + + return EXIT_SUCCESS; +}