mutter/clutter/clutter-score.c

1001 lines
23 KiB
C
Raw Normal View History

/*
* Clutter.
*
* An OpenGL based 'interactive canvas' library.
*
* Authored By Matthew Allum <mallum@openedhand.com>
*
* Copyright (C) 2007 OpenedHand
*
* 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, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
/**
* SECTION:clutter-score
* @short_description: Controller for multiple timelines
*
* #ClutterScore is a base class for sequencing multiple timelines in order.
* Using #ClutterScore it is possible to start multiple timelines at the
* same time or launch multiple timelines when a particular timeline has
* emitted the ClutterTimeline::completed signal.
*
* Each time a #ClutterTimeline is started and completed, a signal will be
* emitted.
*
* For example, this code will start two #ClutterTimeline<!-- -->s after
* a third timeline terminates:
*
* <informalexample><programlisting>
* ClutterTimeline *timeline_1, *timeline_2, *timeline_3;
* ClutterScore *score;
*
* timeline_1 = clutter_timeline_new_for_duration (1000);
* timeline_2 = clutter_timeline_new_for_duration (500);
* timeline_3 = clutter_timeline_new_for_duration (500);
*
* score = clutter_score_new ();
* clutter_score_append (score, NULL, timeline_1);
* clutter_score_append (score, timeline_1, timeline_2);
* clutter_score_append (score, timeline_1, timeline_3);
*
* clutter_score_start ();
* </programlisting></informalexample>
*
* A #ClutterScore takes a reference on the timelines it manages.
*
* New timelines can be added to the #ClutterScore using
* clutter_score_append() and removed using clutter_score_remove().
*
* The score can be cleared using clutter_score_remove_all().
*
* The list of timelines can be retrieved using
* clutter_score_list_timelines().
*
* The score state is controlled using clutter_score_start(),
* clutter_score_pause(), clutter_score_stop() and clutter_score_rewind().
* The state can be queried using clutter_score_is_playing().
*
* #ClutterScore is available since Clutter 0.6
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "clutter-score.h"
#include "clutter-main.h"
#include "clutter-marshal.h"
#include "clutter-private.h"
#include "clutter-debug.h"
typedef struct _ClutterScoreEntry ClutterScoreEntry;
struct _ClutterScoreEntry
{
ClutterTimeline *timeline;
guint id;
/* signal handler id */
gulong completed_id;
ClutterScore *score;
/* pointer back to the tree structure */
GNode *node;
};
#define CLUTTER_SCORE_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), CLUTTER_TYPE_SCORE, ClutterScorePrivate))
struct _ClutterScorePrivate
{
GNode *root;
GHashTable *running_timelines;
guint last_id;
guint is_paused : 1;
guint loop : 1;
};
enum
{
PROP_0,
PROP_LOOP
};
enum
{
TIMELINE_STARTED,
TIMELINE_COMPLETED,
STARTED,
PAUSED,
COMPLETED,
LAST_SIGNAL
};
G_DEFINE_TYPE (ClutterScore, clutter_score, G_TYPE_OBJECT);
static int score_signals[LAST_SIGNAL] = { 0 };
/* Object */
static void
clutter_score_set_property (GObject *gobject,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
ClutterScorePrivate *priv = CLUTTER_SCORE_GET_PRIVATE (gobject);
switch (prop_id)
{
case PROP_LOOP:
priv->loop = g_value_get_boolean (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
break;
}
}
static void
clutter_score_get_property (GObject *gobject,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
ClutterScorePrivate *priv = CLUTTER_SCORE_GET_PRIVATE (gobject);
switch (prop_id)
{
case PROP_LOOP:
g_value_set_boolean (value, priv->loop);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
break;
}
}
static void
clutter_score_finalize (GObject *object)
{
ClutterScorePrivate *priv = CLUTTER_SCORE (object)->priv;
if (priv->running_timelines)
g_hash_table_destroy (priv->running_timelines);
G_OBJECT_CLASS (clutter_score_parent_class)->finalize (object);
}
static void
clutter_score_dispose (GObject *object)
{
ClutterScore *self = CLUTTER_SCORE(object);
ClutterScorePrivate *priv;
priv = self->priv;
G_OBJECT_CLASS (clutter_score_parent_class)->dispose (object);
}
static void
clutter_score_class_init (ClutterScoreClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
gobject_class->set_property = clutter_score_set_property;
gobject_class->get_property = clutter_score_get_property;
gobject_class->finalize = clutter_score_finalize;
gobject_class->dispose = clutter_score_dispose;
g_type_class_add_private (klass, sizeof (ClutterScorePrivate));
/**
* ClutterScore:loop:
*
* Whether the #ClutterScore should restart once finished.
*
* Since: 0.6
*/
g_object_class_install_property (gobject_class,
PROP_LOOP,
g_param_spec_boolean ("loop",
"Loop",
"Whether the score should restart once finished",
FALSE,
CLUTTER_PARAM_READWRITE));
/**
* ClutterScore::timeline-started:
* @score: the score which received the signal
* @timeline: the current timeline
*
* The ::timeline-started signal is emitted each time a new timeline
* inside a #ClutterScore starts playing.
*
* Since: 0.6
*/
score_signals[TIMELINE_STARTED] =
g_signal_new ("timeline-started",
G_TYPE_FROM_CLASS (gobject_class),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (ClutterScoreClass, timeline_started),
NULL, NULL,
clutter_marshal_VOID__OBJECT,
G_TYPE_NONE,
1, CLUTTER_TYPE_TIMELINE);
/**
* ClutterScore::timeline-completed:
* @score: the score which received the signal
* @timeline: the completed timeline
*
* The ::timeline-completed signal is emitted each time a timeline
* inside a #ClutterScore terminates.
*
* Since: 0.6
*/
score_signals[TIMELINE_COMPLETED] =
g_signal_new ("timeline-completed",
G_TYPE_FROM_CLASS (gobject_class),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (ClutterScoreClass, timeline_completed),
NULL, NULL,
clutter_marshal_VOID__OBJECT,
G_TYPE_NONE, 1,
CLUTTER_TYPE_TIMELINE);
/**
* ClutterScore::completed:
* @score: the score which received the signal
*
* The ::completed signal is emitted each time a #ClutterScore terminates.
*
* Since: 0.6
*/
score_signals[COMPLETED] =
g_signal_new ("completed",
G_TYPE_FROM_CLASS (gobject_class),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (ClutterScoreClass, completed),
NULL, NULL,
clutter_marshal_VOID__VOID,
G_TYPE_NONE, 0);
/**
* ClutterScore::started:
* @score: the score which received the signal
*
* The ::started signal is emitted each time a #ClutterScore starts playing.
*
* Since: 0.6
*/
score_signals[STARTED] =
g_signal_new ("started",
G_TYPE_FROM_CLASS (gobject_class),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (ClutterScoreClass, started),
NULL, NULL,
clutter_marshal_VOID__VOID,
G_TYPE_NONE, 0);
/**
* ClutterScore::paused:
* @score: the score which received the signal
*
* The ::paused signal is emitted each time a #ClutterScore
* is paused.
*
* Since: 0.6
*/
score_signals[PAUSED] =
g_signal_new ("paused",
G_TYPE_FROM_CLASS (gobject_class),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (ClutterScoreClass, paused),
NULL, NULL,
clutter_marshal_VOID__VOID,
G_TYPE_NONE, 0);
}
static void
clutter_score_init (ClutterScore *self)
{
ClutterScorePrivate *priv;
self->priv = priv = CLUTTER_SCORE_GET_PRIVATE (self);
/* sentinel */
priv->root = g_node_new (NULL);
priv->running_timelines = NULL;
priv->is_paused = FALSE;
priv->loop = FALSE;
priv->last_id = 1;
}
/**
* clutter_score_new:
*
* Creates a new #ClutterScore. A #ClutterScore is an object that can
* hold multiple #ClutterTimeline<!-- -->s in a sequential order.
*
* Return value: the newly created #ClutterScore. Use g_object_unref()
* when done.
*
* Since: 0.6
*/
ClutterScore *
clutter_score_new (void)
{
return g_object_new (CLUTTER_TYPE_SCORE, NULL);
}
/**
* clutter_score_set_loop:
* @score: a #ClutterScore
* @loop: %TRUE for enable looping
*
* Sets whether @score should loop. A looping #ClutterScore will start
* from its initial state after the ::complete signal has been fired.
*
* Since: 0.6
*/
void
clutter_score_set_loop (ClutterScore *score,
gboolean loop)
{
g_return_if_fail (CLUTTER_IS_SCORE (score));
if (score->priv->loop != loop)
{
score->priv->loop = loop;
g_object_notify (G_OBJECT (score), "loop");
}
}
/**
* clutter_score_get_loop:
* @score: a #ClutterScore
*
* Gets whether @score is looping
*
* Return value: %TRUE if the score is looping
*
* Since: 0.6
*/
gboolean
clutter_score_get_loop (ClutterScore *score)
{
g_return_val_if_fail (CLUTTER_IS_SCORE (score), FALSE);
return score->priv->loop;
}
/**
* clutter_score_is_playing:
* @score: A #ClutterScore
*
* Query state of a #ClutterScore instance.
*
* Return Value: %TRUE if score is currently playing
*
* Since: 0.6
*/
gboolean
clutter_score_is_playing (ClutterScore *score)
{
g_return_val_if_fail (CLUTTER_IS_SCORE (score), FALSE);
if (score->priv->is_paused)
return FALSE;
return (g_hash_table_size (score->priv->running_timelines) != 0);
}
/* forward declaration */
static void start_entry (ClutterScoreEntry *entry);
static void
start_children_entries (GNode *node,
gpointer data)
{
ClutterScoreEntry *entry = node->data;
start_entry (entry);
}
static void
on_timeline_finish (ClutterTimeline *timeline,
ClutterScoreEntry *entry)
{
ClutterScorePrivate *priv = entry->score->priv;
g_hash_table_remove (priv->running_timelines,
GINT_TO_POINTER (entry->id));
g_signal_handler_disconnect (timeline, entry->completed_id);
entry->completed_id = 0;
CLUTTER_NOTE (SCHEDULER, "timeline [%p] (%d) completed",
entry->timeline,
entry->id);
g_signal_emit (entry->score, score_signals[TIMELINE_COMPLETED], 0,
entry->timeline);
/* start every child */
if (entry->node->children)
{
g_node_children_foreach (entry->node,
G_TRAVERSE_ALL,
start_children_entries,
NULL);
}
/* score has finished - fire 'completed' signal */
if (g_hash_table_size (priv->running_timelines) == 0)
{
CLUTTER_NOTE (SCHEDULER, "looks like we finished");
g_signal_emit (entry->score, score_signals[COMPLETED], 0);
clutter_score_stop (entry->score);
if (priv->loop)
clutter_score_start (entry->score);
}
}
static void
start_entry (ClutterScoreEntry *entry)
{
ClutterScorePrivate *priv = entry->score->priv;
entry->completed_id =
g_signal_connect (entry->timeline,
"completed", G_CALLBACK (on_timeline_finish),
entry);
CLUTTER_NOTE (SCHEDULER, "timeline [%p] (%d) started",
entry->timeline,
entry->id);
if (G_UNLIKELY (priv->running_timelines == NULL))
priv->running_timelines = g_hash_table_new (NULL, NULL);
g_hash_table_insert (priv->running_timelines,
GINT_TO_POINTER (entry->id),
entry);
clutter_timeline_start (entry->timeline);
g_signal_emit (entry->score, score_signals[TIMELINE_STARTED], 0,
entry->timeline);
}
static void
foreach_running_timeline_start (gpointer key,
gpointer value,
gpointer user_data)
{
ClutterScoreEntry *entry = value;
clutter_timeline_start (entry->timeline);
}
/**
* clutter_score_start:
* @score: A #ClutterScore
*
* Starts the score.
*
* Since: 0.6
*/
void
clutter_score_start (ClutterScore *score)
{
ClutterScorePrivate *priv;
g_return_if_fail (CLUTTER_IS_SCORE (score));
priv = score->priv;
if (priv->is_paused)
{
g_hash_table_foreach (priv->running_timelines,
foreach_running_timeline_start,
NULL);
priv->is_paused = FALSE;
}
else
{
g_node_children_foreach (priv->root,
G_TRAVERSE_ALL,
start_children_entries,
NULL);
}
}
static gboolean
foreach_running_timeline_stop (gpointer key,
gpointer value,
gpointer user_data)
{
ClutterScoreEntry *entry = value;
clutter_timeline_stop (entry->timeline);
return TRUE;
}
/**
* clutter_score_stop:
* @score: A #ClutterScore
*
* Stops and rewinds a playing #ClutterScore instance.
*
* Since: 0.6
*/
void
clutter_score_stop (ClutterScore *score)
{
ClutterScorePrivate *priv;
g_return_if_fail (CLUTTER_IS_SCORE (score));
priv = score->priv;
g_hash_table_foreach_remove (priv->running_timelines,
foreach_running_timeline_stop,
NULL);
g_hash_table_destroy (priv->running_timelines);
priv->running_timelines = NULL;
}
/**
* clutter_score_rewind:
* @score: A #ClutterScore
*
* Rewinds a #ClutterScore to its initial state.
*
* Since: 0.6
*/
void
clutter_score_rewind (ClutterScore *score)
{
gboolean was_playing;
g_return_if_fail (CLUTTER_IS_SCORE (score));
was_playing = clutter_score_is_playing (score);
clutter_score_stop (score);
if (was_playing)
clutter_score_start (score);
}
static void
foreach_running_timeline_pause (gpointer key,
gpointer value,
gpointer user_data)
{
ClutterScoreEntry *entry = value;
clutter_timeline_pause (entry->timeline);
}
/**
* clutter_score_pause:
* @score: a #ClutterScore
*
* Pauses a playing score @score.
*
* Since: 0.6
*/
void
clutter_score_pause (ClutterScore *score)
{
ClutterScorePrivate *priv;
g_return_if_fail (CLUTTER_IS_SCORE (score));
priv = score->priv;
if (!clutter_score_is_playing (score))
return;
g_hash_table_foreach (priv->running_timelines,
foreach_running_timeline_pause,
NULL);
priv->is_paused = TRUE;
g_signal_emit (score, score_signals[PAUSED], 0);
}
typedef enum {
FIND_BY_TIMELINE,
FIND_BY_ID,
REMOVE_BY_ID,
LIST_TIMELINES
} TraverseAction;
typedef struct {
TraverseAction action;
ClutterScore *score;
/* parameters */
union {
ClutterTimeline *timeline;
guint id;
ClutterScoreEntry *entry;
} d;
gpointer result;
} TraverseClosure;
static gboolean
destroy_entry (GNode *node,
G_GNUC_UNUSED gpointer data)
{
ClutterScoreEntry *entry = node->data;
if (G_LIKELY (entry != NULL))
{
if (entry->completed_id)
g_signal_handler_disconnect (entry->timeline, entry->completed_id);
g_object_unref (entry->timeline);
g_slice_free (ClutterScoreEntry, entry);
node->data = NULL;
}
/* continue */
return FALSE;
}
/* multi-purpose traversal function for the N-ary tree used by the score */
static gboolean
traverse_children (GNode *node,
gpointer data)
{
TraverseClosure *closure = data;
ClutterScoreEntry *entry = node->data;
gboolean retval = FALSE;
/* root */
if (!entry)
return TRUE;
switch (closure->action)
{
case FIND_BY_TIMELINE:
if (closure->d.timeline == entry->timeline)
{
closure->result = node;
retval = TRUE;
}
break;
case FIND_BY_ID:
if (closure->d.id == entry->id)
{
closure->result = node;
retval = TRUE;
}
break;
case REMOVE_BY_ID:
if (closure->d.id == entry->id)
{
if (entry->completed_id)
g_signal_handler_disconnect (entry->timeline, entry->completed_id);
g_object_unref (entry->timeline);
g_node_traverse (node,
G_POST_ORDER,
G_TRAVERSE_ALL,
-1,
destroy_entry, NULL);
g_slice_free (ClutterScoreEntry, entry);
closure->result = node;
retval = TRUE;
}
break;
case LIST_TIMELINES:
retval = TRUE;
break;
}
return retval;
}
static GNode *
find_entry_by_timeline (ClutterScore *score,
ClutterTimeline *timeline)
{
ClutterScorePrivate *priv = score->priv;
TraverseClosure closure;
closure.action = FIND_BY_TIMELINE;
closure.score = score;
closure.d.timeline = timeline;
closure.result = NULL;
g_node_traverse (priv->root,
G_POST_ORDER,
G_TRAVERSE_ALL,
-1,
traverse_children, &closure);
if (closure.result)
return closure.result;
return NULL;
}
static GNode *
find_entry_by_id (ClutterScore *score,
guint id)
{
ClutterScorePrivate *priv = score->priv;
TraverseClosure closure;
closure.action = FIND_BY_ID;
closure.score = score;
closure.d.id = id;
closure.result = NULL;
g_node_traverse (priv->root,
G_POST_ORDER,
G_TRAVERSE_ALL,
-1,
traverse_children, &closure);
if (closure.result)
return closure.result;
return NULL;
}
/**
* clutter_score_append:
* @score: a #ClutterScore
* @parent: a #ClutterTimeline in the score or %NULL
* @timeline: a #ClutterTimeline
*
* Appends a timeline to another one existing in the score; the newly
* appended timeline will be started when @parent is complete.
*
* If @parent is %NULL, the new #ClutterTimeline will be started when
* clutter_score_start() is called.
*
* #ClutterScore will take a reference on @timeline.
*
* Return value: the id of the newly added timeline, to be used with
* clutter_score_get_timeline() and clutter_score_remove().
*
* Since: 0.6
*/
guint
clutter_score_append (ClutterScore *score,
ClutterTimeline *parent,
ClutterTimeline *timeline)
{
ClutterScorePrivate *priv;
g_return_val_if_fail (CLUTTER_IS_SCORE (score), 0);
g_return_val_if_fail (parent == NULL || CLUTTER_IS_TIMELINE (parent), 0);
g_return_val_if_fail (CLUTTER_IS_TIMELINE (timeline), 0);
priv = score->priv;
if (!parent)
{
ClutterScoreEntry *entry;
entry = g_slice_new (ClutterScoreEntry);
entry->timeline = g_object_ref (timeline);
entry->id = priv->last_id;
entry->completed_id = 0;
entry->score = score;
entry->node = g_node_append_data (priv->root, entry);
priv->last_id += 1;
return entry->id;
}
else
{
GNode *node;
ClutterScoreEntry *entry;
node = find_entry_by_timeline (score, parent);
if (G_UNLIKELY (!node))
{
g_warning ("Unable to find the parent timeline inside the score.");
return 0;
}
entry = g_slice_new (ClutterScoreEntry);
entry->timeline = g_object_ref (timeline);
entry->id = priv->last_id;
entry->completed_id = 0;
entry->score = score;
entry->node = g_node_append_data (node, entry);
priv->last_id += 1;
return entry->id;
}
return 0;
}
/**
* clutter_score_remove:
* @score: a #ClutterScore
* @id: the id of the timeline to remove
*
* Removes the #ClutterTimeline with the given id inside @score. If
* the timeline has other timelines attached to it, those are removed
* as well.
*
* Since: 0.6
*/
void
clutter_score_remove (ClutterScore *score,
guint id)
{
ClutterScorePrivate *priv;
TraverseClosure closure;
g_return_if_fail (CLUTTER_IS_SCORE (score));
g_return_if_fail (id > 0);
priv = score->priv;
closure.action = REMOVE_BY_ID;
closure.score = score;
closure.d.id = id;
closure.result = NULL;
g_node_traverse (priv->root,
G_POST_ORDER,
G_TRAVERSE_ALL,
-1,
traverse_children, &closure);
if (closure.result)
g_node_destroy (closure.result);
}
/**
* clutter_score_remove_all:
* @score: a #ClutterScore
*
* Removes all the timelines inside @score.
*
* Since: 0.6
*/
void
clutter_score_remove_all (ClutterScore *score)
{
ClutterScorePrivate *priv;
g_return_if_fail (CLUTTER_IS_SCORE (score));
/* this will take care of the running timelines */
clutter_score_stop (score);
priv = score->priv;
g_node_traverse (priv->root,
G_POST_ORDER,
G_TRAVERSE_ALL,
-1,
destroy_entry, NULL);
g_node_destroy (priv->root);
/* recreate the sentinel */
priv->root = g_node_new (NULL);
}
/**
* clutter_score_get_timeline:
* @score: a #ClutterScore
* @id: the id of the timeline
*
* Retrieves the #ClutterTimeline for @id inside @score.
*
* Return value: the requested timeline, or %NULL. This function does
* not increase the reference count on the returned #ClutterTimeline
*
* Since: 0.6
*/
ClutterTimeline *
clutter_score_get_timeline (ClutterScore *score,
guint id)
{
GNode *node;
ClutterScoreEntry *entry;
g_return_val_if_fail (CLUTTER_IS_SCORE (score), NULL);
g_return_val_if_fail (id > 0, NULL);
node = find_entry_by_id (score, id);
if (G_UNLIKELY (!node))
return NULL;
entry = node->data;
return entry->timeline;
}
/**
* clutter_score_list_timelines:
* @score: a #ClutterScore
*
* Retrieves a list of all the #ClutterTimelines managed by @score.
*
* Return value: a #GSList containing all the timelines in the score.
* This function does not increase the reference count of the
* returned timelines. Use g_slist_free() on the returned list to
* deallocate its resources.
*
* Since: 0.6
*/
GSList *
clutter_score_list_timelines (ClutterScore *score)
{
ClutterScorePrivate *priv;
TraverseClosure closure;
GSList *retval;
g_return_val_if_fail (CLUTTER_IS_SCORE (score), NULL);
priv = score->priv;
closure.action = LIST_TIMELINES;
closure.result = NULL;
g_node_traverse (priv->root,
G_POST_ORDER,
G_TRAVERSE_ALL,
-1,
traverse_children, &closure);
retval = closure.result;
return retval;
}