mutter/clutter/clutter-script.c
Emmanuele Bassi 35132fb5de 2007-10-27 Emmanuele Bassi <ebassi@openedhand.com>
* clutter/clutter-script.[ch]: Slight API change in the
	clutter_script_get_objects() function: now it takes
	object name/object return location pairs and returns the
	number of objects found and returned.

	* tests/test-script.c: Exercise the clutter_script_get_objects()
	function.
2007-10-27 19:08:59 +00:00

1633 lines
45 KiB
C

/*
* Clutter.
*
* An OpenGL based 'interactive canvas' library.
*
* Authored By Matthew Allum <mallum@openedhand.com>
*
* Copyright (C) 2006 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-script
* @short_description: Loads a scene from UI definition data
*
* #ClutterScript is an object used for loading and building parts or a
* complete scenegraph from external definition data in forms of string
* buffers or files.
*
* The UI definition format is JSON, the JavaScript Object Notation as
* described by RFC 4627. #ClutterScript can load a JSON data stream,
* parse it and build all the objects defined into it. Each object must
* have an "id" and a "type" properties defining the name to be used
* to retrieve it from #ClutterScript with clutter_script_get_object(),
* and the class type to be instanciated. Every other attribute will
* be mapped to the class properties.
*
* A #ClutterScript holds a reference on every object it creates from
* the definition data, except for the stage. Every non-actor object
* will be finalized when the #ClutterScript instance holding it will
* be finalized, so they need to be referenced using g_object_ref() in
* order for them to survive.
*
* A simple object might be defined as:
*
* <programlisting>
* {
* "id" : "red-button",
* "type" : "ClutterRectangle",
* "width" : 100,
* "height" : 100,
* "color" : "0xff0000ff"
* }
* </programlisting>
*
* This will produce a red #ClutterRectangle, 100x100 pixels wide, and
* with a name of "red-button"; it can be retrieved by calling:
*
* <programlisting>
* ClutterActor *red_button;
*
* red_button = CLUTTER_ACTOR (clutter_script_get_object (script, "red-button"));
* </programlisting>
*
* and then manipulated with the Clutter API.
*
* Packing can be represented using the "children" member, and passing an
* array of objects or ids of objects already defined (but not packed: the
* packing rules of Clutter still apply, and an actor cannot be packed
* in multiple containers without unparenting it in between).
*
* Behaviours and timelines can also be defined inside a UI definition
* buffer:
*
* <programlisting>
* {
* "id" : "rotate-behaviour",
* "type" : "ClutterBehaviourRotate",
* "angle-begin" : 0.0,
* "angle-end" : 360.0,
* "axis" : "z-axis",
* "alpha" : {
* "timeline" : { "num-frames" : 240, "fps" : 60, "loop" : true },
* "function" : "sine"
* }
* }
* </programlisting>
*
* And then to apply a defined behaviour to an actor defined inside the
* definition of an actor, the "behaviour" member can be used:
*
* <programlisting>
* {
* "id" : "my-rotating-actor",
* "type" : "ClutterTexture",
* ...
* "behaviours" : [ "rotate-behaviour" ]
* }
* </programlisting>
*
* A #ClutterAlpha belonging to a #ClutterBehaviour can only be defined
* implicitely. A #ClutterTimeline belonging to a #ClutterAlpha can be
* either defined implicitely or explicitely. Implicitely defined
* #ClutterAlpha<!-- -->s and #ClutterTimeline<!-- -->s can omit the
* <varname>id</varname> member, as well as the <varname>type</varname> member,
* but will not be available using clutter_script_get_object() (they can,
* however, be extracted using the #ClutterBehaviour and #ClutterAlpha
* API respectively).
*
* Clutter reserves the following names, so classes defining properties
* through the usual GObject registration process should avoid using these
* names to avoid collisions:
*
* <programlisting><![CDATA[
* "id" := the unique name of a ClutterScript object
* "type" := the class literal name, also used to infer the type function
* "type_func" := the GType function name, for non-standard classes
* "children" := an array of names or objects to add as children
* "behaviours" := an array of names or objects to apply to an actor
* ]]></programlisting>
*
* #ClutterScript is available since Clutter 0.6
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <glib.h>
#include <glib-object.h>
#include "clutter-actor.h"
#include "clutter-alpha.h"
#include "clutter-behaviour.h"
#include "clutter-container.h"
#include "clutter-stage.h"
#include "clutter-texture.h"
#include "clutter-script.h"
#include "clutter-script-private.h"
#include "clutter-scriptable.h"
#include "clutter-private.h"
#include "clutter-debug.h"
#include "json/json-parser.h"
#define CLUTTER_SCRIPT_GET_PRIVATE(obj) \
(G_TYPE_INSTANCE_GET_PRIVATE ((obj), CLUTTER_TYPE_SCRIPT, ClutterScriptPrivate))
struct _ClutterScriptPrivate
{
GHashTable *objects;
guint last_merge_id;
guint last_unknown;
JsonParser *parser;
gchar *filename;
guint is_filename : 1;
};
G_DEFINE_TYPE (ClutterScript, clutter_script, G_TYPE_OBJECT);
static void
warn_missing_attribute (ClutterScript *script,
const gchar *id,
const gchar *attribute)
{
ClutterScriptPrivate *priv = script->priv;
if (G_LIKELY (id))
{
g_warning ("%s: %d: object `%s' has no `%s' attribute",
priv->is_filename ? priv->filename : "<input>",
json_parser_get_current_line (priv->parser),
id,
attribute);
}
else
{
g_warning ("%s: %d: object has no `%s' attribute",
priv->is_filename ? priv->filename : "<input>",
json_parser_get_current_line (priv->parser),
attribute);
}
}
#if 0
static void
warn_invalid_value (ClutterScript *script,
const gchar *attribute,
JsonNode *node)
{
ClutterScriptPrivate *priv = script->priv;
if (G_LIKELY (node))
{
g_warning ("%s: %d: invalid value of type `%s' for attribute `%s'",
priv->is_filename ? priv->filename : "<input>",
json_parser_get_current_line (priv->parser),
json_node_type_name (node),
attribute);
}
else
{
g_warning ("%s: %d: invalid value for attribute `%s'",
priv->is_filename ? priv->filename : "<input>",
json_parser_get_current_line (priv->parser),
attribute);
}
}
#endif
static const gchar *
get_id_from_node (JsonNode *node)
{
JsonObject *object;
switch (JSON_NODE_TYPE (node))
{
case JSON_NODE_OBJECT:
object = json_node_get_object (node);
if (json_object_has_member (object, "id"))
{
JsonNode *id = json_object_get_member (object, "id");
return json_node_get_string (id);
}
break;
case JSON_NODE_VALUE:
return json_node_get_string (node);
default:
break;
}
return NULL;
}
static GList *
parse_children (JsonNode *node)
{
JsonArray *array;
GList *retval;
guint array_len, i;
if (JSON_NODE_TYPE (node) != JSON_NODE_ARRAY)
return NULL;
retval = NULL;
array = json_node_get_array (node);
array_len = json_array_get_length (array);
for (i = 0; i < array_len; i++)
{
JsonNode *child = json_array_get_element (array, i);
const gchar *id;
id = get_id_from_node (child);
if (id)
retval = g_list_prepend (retval, g_strdup (id));
}
return g_list_reverse (retval);
}
static GList *
parse_behaviours (JsonNode *node)
{
JsonArray *array;
GList *retval;
guint array_len, i;
if (JSON_NODE_TYPE (node) != JSON_NODE_ARRAY)
return NULL;
retval = NULL;
array = json_node_get_array (node);
array_len = json_array_get_length (array);
for (i = 0; i < array_len; i++)
{
JsonNode *child = json_array_get_element (array, i);
const gchar *id;
id = get_id_from_node (child);
if (id)
retval = g_list_prepend (retval, g_strdup (id));
}
return g_list_reverse (retval);
}
static ClutterTimeline *
construct_timeline (ClutterScript *script,
JsonObject *object)
{
ClutterTimeline *retval = NULL;
ObjectInfo *oinfo;
GList *members, *l;
/* we fake an ObjectInfo so we can reuse clutter_script_construct_object()
* here; we do not save it inside the hash table, because if this had
* been a named object then we wouldn't have ended up here in the first
* place
*/
oinfo = g_slice_new0 (ObjectInfo);
oinfo->gtype = CLUTTER_TYPE_TIMELINE;
oinfo->id = g_strdup ("dummy");
members = json_object_get_members (object);
for (l = members; l != NULL; l = l->next)
{
const gchar *name = l->data;
JsonNode *node = json_object_get_member (object, name);
PropertyInfo *pinfo = g_slice_new0 (PropertyInfo);
pinfo->name = g_strdelimit (g_strdup (name), G_STR_DELIMITERS, '-');
pinfo->node = node;
oinfo->properties = g_list_prepend (oinfo->properties, pinfo);
}
g_list_free (members);
retval = CLUTTER_TIMELINE (clutter_script_construct_object (script, oinfo));
/* we transfer ownership to the alpha function later */
oinfo->is_toplevel = FALSE;
object_info_free (oinfo);
return retval;
}
static ClutterAlphaFunc
resolve_alpha_func (const gchar *name)
{
static GModule *module = NULL;
ClutterAlphaFunc func;
GString *symbol_name;
gchar c, *symbol;
gint i;
if (G_UNLIKELY (!module))
module = g_module_open (NULL, 0);
CLUTTER_NOTE (SCRIPT, "Looking for `%s' alpha function", name);
if (g_module_symbol (module, name, (gpointer) &func))
return func;
symbol_name = g_string_new ("");
g_string_append (symbol_name, "clutter_");
for (i = 0; name[i] != '\0'; i++)
{
c = name[i];
if (name[i] == '-')
g_string_append_c (symbol_name, '_');
else
g_string_append_c (symbol_name, g_ascii_tolower (name[i]));
}
g_string_append (symbol_name, "_func");
symbol = g_string_free (symbol_name, FALSE);
if (!g_module_symbol (module, symbol, (gpointer)&func))
func = NULL;
g_free (symbol);
return func;
}
GObject *
clutter_script_parse_alpha (ClutterScript *script,
JsonNode *node)
{
GObject *retval = NULL;
JsonObject *object;
ClutterTimeline *timeline = NULL;
ClutterAlphaFunc alpha_func = NULL;
JsonNode *val;
gboolean unref_timeline = FALSE;
if (JSON_NODE_TYPE (node) != JSON_NODE_OBJECT)
return NULL;
object = json_node_get_object (node);
val = json_object_get_member (object, "timeline");
if (val)
{
if (JSON_NODE_TYPE (val) == JSON_NODE_VALUE &&
json_node_get_string (val) != NULL)
{
const gchar *id = json_node_get_string (val);
timeline =
CLUTTER_TIMELINE (clutter_script_get_object (script, id));
}
else if (JSON_NODE_TYPE (val) == JSON_NODE_OBJECT)
{
timeline = construct_timeline (script, json_node_get_object (val));
unref_timeline = TRUE;
}
}
val = json_object_get_member (object, "function");
if (val && json_node_get_string (val) != NULL)
alpha_func = resolve_alpha_func (json_node_get_string (val));
CLUTTER_NOTE (SCRIPT, "Parsed alpha: %s timeline (%p) and func:%p",
unref_timeline ? "implicit" : "explicit",
timeline ? timeline : 0x0,
alpha_func ? alpha_func : 0x0);
retval = g_object_new (CLUTTER_TYPE_ALPHA, NULL);
clutter_alpha_set_func (CLUTTER_ALPHA (retval), alpha_func, NULL, NULL);
clutter_alpha_set_timeline (CLUTTER_ALPHA (retval), timeline);
if (unref_timeline)
g_object_unref (timeline);
return retval;
}
static void
json_object_end (JsonParser *parser,
JsonObject *object,
gpointer user_data)
{
ClutterScript *script = user_data;
ClutterScriptPrivate *priv = script->priv;
ObjectInfo *oinfo;
JsonNode *val;
GList *members, *l;
if (!json_object_has_member (object, "id"))
{
gchar *fake;
if (!json_object_has_member (object, "type"))
return;
fake = g_strdup_printf ("script-%d-%d",
priv->last_merge_id,
priv->last_unknown++);
val = json_node_new (JSON_NODE_VALUE);
json_node_set_string (val, fake);
json_object_add_member (object, "id", val);
g_free (fake);
}
if (!json_object_has_member (object, "type"))
{
val = json_object_get_member (object, "id");
warn_missing_attribute (script, json_node_get_string (val), "type");
return;
}
oinfo = g_slice_new0 (ObjectInfo);
oinfo->merge_id = priv->last_merge_id;
val = json_object_get_member (object, "id");
oinfo->id = json_node_dup_string (val);
val = json_object_get_member (object, "type");
oinfo->class_name = json_node_dup_string (val);
if (json_object_has_member (object, "type_func"))
{
val = json_object_get_member (object, "type_func");
oinfo->type_func = json_node_dup_string (val);
json_object_remove_member (object, "type_func");
}
if (json_object_has_member (object, "children"))
{
val = json_object_get_member (object, "children");
oinfo->children = parse_children (val);
json_object_remove_member (object, "children");
}
if (json_object_has_member (object, "behaviours"))
{
val = json_object_get_member (object, "behaviours");
oinfo->behaviours = parse_behaviours (val);
json_object_remove_member (object, "behaviours");
}
oinfo->is_toplevel = FALSE;
oinfo->is_unmerged = FALSE;
oinfo->has_unresolved = TRUE;
members = json_object_get_members (object);
for (l = members; l; l = l->next)
{
const gchar *name = l->data;
PropertyInfo *pinfo;
JsonNode *node;
/* we have already parsed these */
if (strcmp (name, "id") == 0 || strcmp (name, "type") == 0)
continue;
node = json_object_get_member (object, name);
pinfo = g_slice_new (PropertyInfo);
pinfo->name = g_strdup (name);
pinfo->node = node;
pinfo->pspec = NULL;
oinfo->properties = g_list_prepend (oinfo->properties, pinfo);
}
g_list_free (members);
CLUTTER_NOTE (SCRIPT, "Added object `%s' (type:%s, id:%d) with %d properties",
oinfo->id,
oinfo->class_name,
oinfo->merge_id,
g_list_length (oinfo->properties));
g_hash_table_replace (priv->objects, g_strdup (oinfo->id), oinfo);
oinfo->object = clutter_script_construct_object (script, oinfo);
}
gboolean
clutter_script_parse_node (ClutterScript *script,
GValue *value,
const gchar *name,
JsonNode *node,
GParamSpec *pspec)
{
GValue node_value = { 0, };
gboolean retval = FALSE;
g_return_val_if_fail (CLUTTER_IS_SCRIPT (script), FALSE);
g_return_val_if_fail (name != NULL, FALSE);
g_return_val_if_fail (node != NULL, FALSE);
switch (JSON_NODE_TYPE (node))
{
case JSON_NODE_OBJECT:
if (!pspec)
return FALSE;
else
{
g_value_init (value, G_PARAM_SPEC_VALUE_TYPE (pspec));
if (G_VALUE_HOLDS (value, CLUTTER_TYPE_ALPHA))
{
GObject *alpha;
alpha = clutter_script_parse_alpha (script, node);
if (alpha)
{
g_value_set_object (value, alpha);
return TRUE;
}
}
}
return FALSE;
case JSON_NODE_ARRAY:
if (!pspec)
return FALSE;
else
{
g_value_init (value, G_PARAM_SPEC_VALUE_TYPE (pspec));
if (G_VALUE_HOLDS (value, CLUTTER_TYPE_KNOT))
{
ClutterKnot knot = { 0, };
if (clutter_script_parse_knot (script, node, &knot))
{
g_value_set_boxed (value, &knot);
return TRUE;
}
}
else if (G_VALUE_HOLDS (value, CLUTTER_TYPE_PADDING))
{
ClutterPadding padding = { 0, };
if (clutter_script_parse_padding (script, node, &padding))
{
g_value_set_boxed (value, &padding);
return TRUE;
}
}
else if (G_VALUE_HOLDS (value, CLUTTER_TYPE_MARGIN))
{
ClutterMargin margin = { 0, };
if (clutter_script_parse_margin (script, node, &margin))
{
g_value_set_boxed (value, &margin);
return TRUE;
}
}
else if (G_VALUE_HOLDS (value, CLUTTER_TYPE_GEOMETRY))
{
ClutterGeometry geom = { 0, };
if (clutter_script_parse_geometry (script, node, &geom))
{
g_value_set_boxed (value, &geom);
return TRUE;
}
}
}
return FALSE;
case JSON_NODE_NULL:
return FALSE;
case JSON_NODE_VALUE:
json_node_get_value (node, &node_value);
if (pspec)
g_value_init (value, G_PARAM_SPEC_VALUE_TYPE (pspec));
else
g_value_init (value, G_VALUE_TYPE (&node_value));
switch (G_TYPE_FUNDAMENTAL (G_VALUE_TYPE (value)))
{
/* fundamental JSON types */
case G_TYPE_INT:
case G_TYPE_DOUBLE:
case G_TYPE_STRING:
case G_TYPE_BOOLEAN:
g_value_copy (&node_value, value);
retval = TRUE;
break;
case G_TYPE_UINT:
g_value_set_uint (value, (guint) g_value_get_int (&node_value));
retval = TRUE;
break;
case G_TYPE_ULONG:
g_value_set_ulong (value, (gulong) g_value_get_int (&node_value));
retval = TRUE;
break;
case G_TYPE_UCHAR:
g_value_set_uchar (value, (guchar) g_value_get_int (&node_value));
retval = TRUE;
break;
case G_TYPE_ENUM:
if (G_VALUE_HOLDS (&node_value, G_TYPE_INT))
{
g_value_set_enum (value, g_value_get_int (&node_value));
retval = TRUE;
}
else if (G_VALUE_HOLDS (&node_value, G_TYPE_STRING))
{
gint enum_value;
retval = clutter_script_enum_from_string (G_VALUE_TYPE (value),
g_value_get_string (&node_value),
&enum_value);
if (retval)
g_value_set_enum (value, enum_value);
}
break;
case G_TYPE_FLAGS:
if (G_VALUE_HOLDS (&node_value, G_TYPE_INT))
{
g_value_set_flags (value, g_value_get_int (&node_value));
retval = TRUE;
}
else if (G_VALUE_HOLDS (&node_value, G_TYPE_STRING))
{
gint flags_value;
retval = clutter_script_flags_from_string (G_VALUE_TYPE (value),
g_value_get_string (&node_value),
&flags_value);
if (retval)
g_value_set_flags (value, flags_value);
}
break;
case G_TYPE_BOXED:
if (G_VALUE_HOLDS (value, CLUTTER_TYPE_COLOR))
{
if (G_VALUE_HOLDS (&node_value, G_TYPE_STRING))
{
const gchar *str = g_value_get_string (&node_value);
ClutterColor color = { 0, };
if (str && str[0] != '\0')
clutter_color_parse (str, &color);
g_value_set_boxed (value, &color);
retval = TRUE;
}
}
break;
case G_TYPE_OBJECT:
if (G_VALUE_HOLDS (value, GDK_TYPE_PIXBUF))
{
if (G_VALUE_HOLDS (&node_value, G_TYPE_STRING))
{
const gchar *str = g_value_get_string (&node_value);
GdkPixbuf *pixbuf = NULL;
gchar *path;
GError *error;
if (g_path_is_absolute (str))
path = g_strdup (str);
else
{
gchar *dirname = NULL;
if (script->priv->is_filename)
dirname = g_path_get_dirname (script->priv->filename);
else
dirname = g_get_current_dir ();
path = g_build_filename (dirname, str, NULL);
g_free (dirname);
}
error = NULL;
pixbuf = gdk_pixbuf_new_from_file (path, &error);
if (error)
{
g_warning ("Unable to open image at path `%s': %s",
path,
error->message);
g_error_free (error);
}
else
{
g_value_set_object (value, pixbuf);
retval = TRUE;
}
g_free (path);
}
}
else
{
if (G_VALUE_HOLDS (&node_value, G_TYPE_STRING))
{
const gchar *str = g_value_get_string (&node_value);
GObject *object = clutter_script_get_object (script, str);
if (object)
{
g_value_set_object (value, object);
retval = TRUE;
}
}
}
break;
default:
retval = FALSE;
break;
}
g_value_unset (&node_value);
break;
}
#if 0
if (!retval)
{
if (pspec)
{
g_param_value_defaults (pspec, value);
retval = TRUE;
}
}
#endif
return retval;
}
static GList *
clutter_script_translate_parameters (ClutterScript *script,
GObject *object,
const gchar *name,
GList *properties,
GArray **params)
{
ClutterScriptable *scriptable = NULL;
ClutterScriptableIface *iface = NULL;
GList *l, *unparsed;
gboolean parse_custom = FALSE;
*params = g_array_new (FALSE, FALSE, sizeof (GParameter));
if (CLUTTER_IS_SCRIPTABLE (object))
{
scriptable = CLUTTER_SCRIPTABLE (object);
iface = CLUTTER_SCRIPTABLE_GET_IFACE (scriptable);
if (iface->parse_custom_node)
parse_custom = TRUE;
}
unparsed = NULL;
for (l = properties; l != NULL; l = l->next)
{
PropertyInfo *pinfo = l->data;
GParameter param = { NULL };
gboolean res = FALSE;
CLUTTER_NOTE (SCRIPT, "Parsing %s property (id:%s)",
pinfo->pspec ? "regular" : "custom",
pinfo->name);
if (parse_custom)
res = iface->parse_custom_node (scriptable, script, &param.value,
pinfo->name,
pinfo->node);
if (!res)
res = clutter_script_parse_node (script, &param.value,
pinfo->name,
pinfo->node,
pinfo->pspec);
if (!res)
{
unparsed = g_list_prepend (unparsed, pinfo);
continue;
}
param.name = g_strdup (pinfo->name);
g_array_append_val (*params, param);
property_info_free (pinfo);
}
g_list_free (properties);
return unparsed;
}
static GList *
clutter_script_get_parameters (ClutterScript *script,
GType gtype,
const gchar *name,
GList *properties,
GArray **construct_params)
{
GObjectClass *klass;
GList *l, *unparsed;
klass = g_type_class_ref (gtype);
g_assert (klass != NULL);
*construct_params = g_array_new (FALSE, FALSE, sizeof (GParameter));
unparsed = NULL;
for (l = properties; l != NULL; l = l->next)
{
PropertyInfo *pinfo = l->data;
GParameter param = { NULL };
GParamSpec *pspec = NULL;
/* we allow custom property names for classes, so if we
* don't find a corresponding GObject property for this
* class we just skip it and let the class itself deal
* with it later on
*/
pspec = g_object_class_find_property (klass, pinfo->name);
if (pspec)
{
pinfo->pspec = g_param_spec_ref (pspec);
}
else
{
pinfo->pspec = NULL;
unparsed = g_list_prepend (unparsed, pinfo);
continue;
}
if (!(pspec->flags & G_PARAM_CONSTRUCT_ONLY))
{
unparsed = g_list_prepend (unparsed, pinfo);
continue;
}
if (!clutter_script_parse_node (script, &param.value,
pinfo->name,
pinfo->node,
pinfo->pspec))
{
unparsed = g_list_prepend (unparsed, pinfo);
continue;
}
param.name = g_strdup (pinfo->name);
g_array_append_val (*construct_params, param);
property_info_free (pinfo);
}
g_list_free (properties);
g_type_class_unref (klass);
return unparsed;
}
static void
apply_behaviours (ClutterScript *script,
ClutterActor *actor,
ObjectInfo *oinfo)
{
GObject *object;
GList *l, *unresolved;
unresolved = NULL;
for (l = oinfo->behaviours; l != NULL; l = l->next)
{
const gchar *name = l->data;
object = clutter_script_get_object (script, name);
if (!object)
{
ObjectInfo *oinfo;
oinfo = g_hash_table_lookup (script->priv->objects, name);
if (oinfo)
object = clutter_script_construct_object (script, oinfo);
}
if (!object)
{
unresolved = g_list_prepend (unresolved, g_strdup (name));
continue;
}
CLUTTER_NOTE (SCRIPT, "Applying behaviour `%s' to actor of type `%s'",
name,
g_type_name (G_OBJECT_TYPE (actor)));
clutter_behaviour_apply (CLUTTER_BEHAVIOUR (object), actor);
}
g_list_foreach (oinfo->behaviours, (GFunc) g_free, NULL);
g_list_free (oinfo->behaviours);
oinfo->behaviours = unresolved;
}
static void
add_children (ClutterScript *script,
ClutterContainer *container,
ObjectInfo *oinfo)
{
GObject *object;
GList *l, *unresolved;
unresolved = NULL;
for (l = oinfo->children; l != NULL; l = l->next)
{
const gchar *name = l->data;
object = clutter_script_get_object (script, name);
if (!object)
{
ObjectInfo *oinfo;
oinfo = g_hash_table_lookup (script->priv->objects, name);
if (oinfo)
object = clutter_script_construct_object (script, oinfo);
}
if (!object)
{
unresolved = g_list_prepend (unresolved, g_strdup (name));
continue;
}
CLUTTER_NOTE (SCRIPT, "Adding children `%s' to actor of type `%s'",
name,
g_type_name (G_OBJECT_TYPE (container)));
clutter_container_add_actor (container, CLUTTER_ACTOR (object));
}
g_list_foreach (oinfo->children, (GFunc) g_free, NULL);
g_list_free (oinfo->children);
oinfo->children = unresolved;
}
GObject *
clutter_script_construct_object (ClutterScript *script,
ObjectInfo *oinfo)
{
GObject *object;
guint i;
GArray *params;
GArray *construct_params;
ClutterScriptable *scriptable = NULL;
ClutterScriptableIface *iface = NULL;
gboolean set_custom_property = FALSE;
g_return_val_if_fail (CLUTTER_IS_SCRIPT (script), NULL);
g_return_val_if_fail (oinfo != NULL, NULL);
/* we have completely updated the object */
if (oinfo->object && !oinfo->has_unresolved)
return oinfo->object;
if (oinfo->gtype == G_TYPE_INVALID)
{
if (G_UNLIKELY (oinfo->type_func))
oinfo->gtype = clutter_script_get_type_from_symbol (oinfo->type_func);
else
oinfo->gtype = clutter_script_get_type_from_name (script, oinfo->class_name);
}
if (G_UNLIKELY (oinfo->gtype == G_TYPE_INVALID))
return NULL;
if (oinfo->object)
object = oinfo->object;
else if (oinfo->gtype == CLUTTER_TYPE_STAGE)
{
/* the stage is a complex beast: we cannot create it using
* g_object_newv() but we need clutter_script_get_parameters()
* to add the GParamSpec to the PropertyInfo pspec member, so
* that we don't have to implement every complex property (like
* the "color" one) directly inside the ClutterStage class.
*/
oinfo->properties = clutter_script_get_parameters (script,
oinfo->gtype,
oinfo->id,
oinfo->properties,
&construct_params);
object = G_OBJECT (clutter_stage_get_default ());
for (i = 0; i < construct_params->len; i++)
{
GParameter *param = &g_array_index (construct_params, GParameter, i);
g_free ((gchar *) param->name);
g_value_unset (&param->value);
}
g_array_free (construct_params, TRUE);
}
else
{
/* every other object: first, we get the construction parameters */
oinfo->properties = clutter_script_get_parameters (script,
oinfo->gtype,
oinfo->id,
oinfo->properties,
&construct_params);
object = g_object_newv (oinfo->gtype,
construct_params->len,
(GParameter *) construct_params->data);
for (i = 0; i < construct_params->len; i++)
{
GParameter *param = &g_array_index (construct_params, GParameter, i);
g_free ((gchar *) param->name);
g_value_unset (&param->value);
}
g_array_free (construct_params, TRUE);
}
/* shortcut, to avoid typechecking every time */
if (CLUTTER_IS_SCRIPTABLE (object))
{
scriptable = CLUTTER_SCRIPTABLE (object);
iface = CLUTTER_SCRIPTABLE_GET_IFACE (scriptable);
if (iface->set_custom_property)
set_custom_property = TRUE;
}
/* then we get the rest of the parameters, asking the object itself
* to translate them for us, if we cannot do that
*/
oinfo->properties = clutter_script_translate_parameters (script,
object,
oinfo->id,
oinfo->properties,
&params);
/* consume all the properties we could translate in this pass */
for (i = 0; i < params->len; i++)
{
GParameter *param = &g_array_index (params, GParameter, i);
CLUTTER_NOTE (SCRIPT,
"Setting %s property `%s' (type:%s) to object `%s' (id:%s)",
set_custom_property ? "custom" : "regular",
param->name,
g_type_name (G_VALUE_TYPE (&param->value)),
g_type_name (oinfo->gtype),
oinfo->id);
if (set_custom_property)
iface->set_custom_property (scriptable, script,
param->name,
&param->value);
else
g_object_set_property (object, param->name, &param->value);
g_free ((gchar *) param->name);
g_value_unset (&param->value);
}
g_array_free (params, TRUE);
if (oinfo->children && CLUTTER_IS_CONTAINER (object))
add_children (script, CLUTTER_CONTAINER (object), oinfo);
if (oinfo->behaviours && CLUTTER_IS_ACTOR (object))
apply_behaviours (script, CLUTTER_ACTOR (object), oinfo);
if (oinfo->properties || oinfo->children || oinfo->behaviours)
oinfo->has_unresolved = TRUE;
else
oinfo->has_unresolved = FALSE;
if (scriptable)
clutter_scriptable_set_name (scriptable, oinfo->id);
else
g_object_set_data_full (object, "clutter-script-name",
g_strdup (oinfo->id),
g_free);
return object;
}
static void
construct_each_object (gpointer key,
gpointer value,
gpointer data)
{
ClutterScript *script = data;
ObjectInfo *oinfo = value;
if (!oinfo->object)
oinfo->object = clutter_script_construct_object (script, oinfo);
}
static void
json_parse_end (JsonParser *parser,
gpointer user_data)
{
ClutterScript *script = user_data;
ClutterScriptPrivate *priv = script->priv;
g_hash_table_foreach (priv->objects, construct_each_object, script);
}
static GType
clutter_script_real_get_type_from_name (ClutterScript *script,
const gchar *type_name)
{
GType gtype;
gtype = g_type_from_name (type_name);
if (gtype != G_TYPE_INVALID)
return gtype;
return clutter_script_get_type_from_class (type_name);
}
void
property_info_free (gpointer data)
{
if (G_LIKELY (data))
{
PropertyInfo *pinfo = data;
if (pinfo->pspec)
g_param_spec_unref (pinfo->pspec);
g_free (pinfo->name);
g_slice_free (PropertyInfo, pinfo);
}
}
void
object_info_free (gpointer data)
{
if (G_LIKELY (data))
{
ObjectInfo *oinfo = data;
g_free (oinfo->id);
g_free (oinfo->class_name);
g_free (oinfo->type_func);
g_list_foreach (oinfo->properties, (GFunc) property_info_free, NULL);
g_list_free (oinfo->properties);
/* these are ids */
g_list_foreach (oinfo->children, (GFunc) g_free, NULL);
g_list_free (oinfo->children);
g_list_foreach (oinfo->behaviours, (GFunc) g_free, NULL);
g_list_free (oinfo->behaviours);
if (oinfo->is_toplevel && oinfo->object)
{
g_object_unref (oinfo->object);
oinfo->object = NULL;
}
if (oinfo->is_unmerged && oinfo->object)
{
clutter_actor_destroy (CLUTTER_ACTOR (oinfo->object));
oinfo->object = NULL;
}
g_slice_free (ObjectInfo, oinfo);
}
}
static void
clutter_script_finalize (GObject *gobject)
{
ClutterScriptPrivate *priv = CLUTTER_SCRIPT_GET_PRIVATE (gobject);
g_object_unref (priv->parser);
g_hash_table_destroy (priv->objects);
g_free (priv->filename);
G_OBJECT_CLASS (clutter_script_parent_class)->finalize (gobject);
}
static void
clutter_script_class_init (ClutterScriptClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
g_type_class_add_private (klass, sizeof (ClutterScriptPrivate));
klass->get_type_from_name = clutter_script_real_get_type_from_name;
gobject_class->finalize = clutter_script_finalize;
}
static void
clutter_script_init (ClutterScript *script)
{
ClutterScriptPrivate *priv;
script->priv = priv = CLUTTER_SCRIPT_GET_PRIVATE (script);
priv->parser = json_parser_new ();
g_signal_connect (priv->parser,
"object-end", G_CALLBACK (json_object_end),
script);
g_signal_connect (priv->parser,
"parse-end", G_CALLBACK (json_parse_end),
script);
priv->is_filename = FALSE;
priv->last_merge_id = 0;
priv->objects = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free,
object_info_free);
}
/**
* clutter_script_new:
*
* Creates a new #ClutterScript instance. #ClutterScript can be used
* to load objects definitions for scenegraph elements, like actors,
* or behavioural elements, like behaviours and timelines. The
* definitions must be encoded using the JavaScript Object Notation (JSON)
* language.
*
* Return value: the newly created #ClutterScript instance. Use
* g_object_unref() when done.
*
* Since: 0.6
*/
ClutterScript *
clutter_script_new (void)
{
return g_object_new (CLUTTER_TYPE_SCRIPT, NULL);
}
/**
* clutter_script_load_from_file:
* @script: a #ClutterScript
* @filename: the full path to the definition file
* @error: return location for a #GError, or %NULL
*
* Loads the definitions from @filename into @script and merges with
* the currently loaded ones, if any.
*
* Return value: on error, zero is returned and @error is set
* accordingly. On success, the merge id for the UI definitions is
* returned. You can use the merge id with clutter_script_unmerge().
*
* Since: 0.6
*/
guint
clutter_script_load_from_file (ClutterScript *script,
const gchar *filename,
GError **error)
{
ClutterScriptPrivate *priv;
GError *internal_error;
g_return_val_if_fail (CLUTTER_IS_SCRIPT (script), 0);
g_return_val_if_fail (filename != NULL, 0);
priv = script->priv;
g_free (priv->filename);
priv->filename = g_strdup (filename);
priv->is_filename = TRUE;
priv->last_merge_id += 1;
internal_error = NULL;
json_parser_load_from_file (priv->parser, filename, &internal_error);
if (internal_error)
{
g_propagate_error (error, internal_error);
priv->last_merge_id -= 1;
return 0;
}
return priv->last_merge_id;
}
/**
* clutter_script_load_from_data:
* @script: a #ClutterScript
* @data: a buffer containing the definitions
* @length: the length of the buffer, or -1 if @data is a NUL-terminated
* buffer
* @error: return location for a #GError, or %NULL
*
* Loads the definitions from @data into @script and merges with
* the currently loaded ones, if any.
*
* Return value: on error, zero is returned and @error is set
* accordingly. On success, the merge id for the UI definitions is
* returned. You can use the merge id with clutter_script_unmerge().
*
* Since: 0.6
*/
guint
clutter_script_load_from_data (ClutterScript *script,
const gchar *data,
gsize length,
GError **error)
{
ClutterScriptPrivate *priv;
GError *internal_error;
g_return_val_if_fail (CLUTTER_IS_SCRIPT (script), 0);
g_return_val_if_fail (data != NULL, 0);
priv = script->priv;
g_free (priv->filename);
priv->filename = NULL;
priv->is_filename = FALSE;
priv->last_merge_id += 1;
internal_error = NULL;
json_parser_load_from_data (priv->parser, data, length, &internal_error);
if (internal_error)
{
g_propagate_error (error, internal_error);
priv->last_merge_id -= 1;
return 0;
}
return priv->last_merge_id;
}
/**
* clutter_script_get_object:
* @script: a #ClutterScript
* @name: the name of the object to retrieve
*
* Retrieves the object bound to @name. This function does not increment
* the reference count of the returned object.
*
* Return value: the named object, or %NULL if no object with the
* given name was available
*
* Since: 0.6
*/
GObject *
clutter_script_get_object (ClutterScript *script,
const gchar *name)
{
ObjectInfo *oinfo;
g_return_val_if_fail (CLUTTER_IS_SCRIPT (script), NULL);
g_return_val_if_fail (name != NULL, NULL);
oinfo = g_hash_table_lookup (script->priv->objects, name);
if (!oinfo)
return NULL;
return clutter_script_construct_object (script, oinfo);
}
static gint
clutter_script_get_objects_valist (ClutterScript *script,
const gchar *first_name,
va_list args)
{
gint retval = 0;
const gchar *name;
name = first_name;
while (name)
{
GObject **obj = NULL;
obj = va_arg (args, GObject**);
*obj = clutter_script_get_object (script, name);
if (*obj)
retval += 1;
name = va_arg (args, gchar*);
}
return retval;
}
/**
* clutter_script_get_objects:
* @script: a #ClutterScript
* @first_name: the name of the first object to retrieve
* @Varargs: return location for a #GObject, then additional names, ending
* with %NULL
*
* Retrieves a list of objects for the given names. After @script, object
* names/return location pairs should be listed, with a %NULL pointer
* ending the list, like:
*
* <informalexample><programlisting>
* GObject *my_label, *a_button, *main_timeline;
*
* clutter_script_get_objects (script,
* "my-label", &amp;my_label,
* "a-button", &amp;a_button,
* "main-timeline", &amp;main_timeline,
* NULL);
* </programlisting</informalexample>
*
* Note: This function does not increment the reference count of the
* returned objects.
*
* Return value: the number of objects returned.
*
* Since: 0.6
*/
gint
clutter_script_get_objects (ClutterScript *script,
const gchar *first_name,
...)
{
gint retval;
va_list var_args;
g_return_val_if_fail (CLUTTER_IS_SCRIPT (script), 0);
g_return_val_if_fail (first_name != NULL, 0);
va_start (var_args, first_name);
retval = clutter_script_get_objects_valist (script, first_name, var_args);
va_end (var_args);
return retval;
}
typedef struct {
ClutterScript *script;
guint merge_id;
GSList *ids;
} UnmergeData;
static void
remove_by_merge_id (gpointer key,
gpointer value,
gpointer data)
{
gchar *name = key;
ObjectInfo *oinfo = value;
UnmergeData *unmerge_data = data;
if (oinfo->merge_id == unmerge_data->merge_id)
{
CLUTTER_NOTE (SCRIPT,
"Unmerging object (id:%s, type:%s, merge-id:%d)",
oinfo->id,
oinfo->class_name,
oinfo->merge_id);
unmerge_data->ids = g_slist_prepend (unmerge_data->ids, g_strdup (name));
oinfo->is_unmerged = TRUE;
}
}
/**
* clutter_script_unmerge_objects:
* @script: a #ClutterScript
* @merge_id: merge id returned when loading a UI definition
*
* Unmerges the objects identified by @merge_id.
*
* Since: 0.6
*/
void
clutter_script_unmerge_objects (ClutterScript *script,
guint merge_id)
{
ClutterScriptPrivate *priv;
UnmergeData data;
GSList *l;
g_return_if_fail (CLUTTER_IS_SCRIPT (script));
g_return_if_fail (merge_id > 0);
priv = script->priv;
data.script = script;
data.merge_id = merge_id;
data.ids = NULL;
g_hash_table_foreach (priv->objects, remove_by_merge_id, &data);
for (l = data.ids; l != NULL; l = l->next)
g_hash_table_remove (priv->objects, l->data);
g_slist_foreach (data.ids, (GFunc) g_free, NULL);
g_slist_free (data.ids);
clutter_script_ensure_objects (script);
}
/**
* clutter_script_ensure_object:
* @script: a #ClutterScript
*
* Ensure that every object defined inside @script is correctly
* constructed. You should rarely need to use this function.
*
* Since: 0.6
*/
void
clutter_script_ensure_objects (ClutterScript *script)
{
ClutterScriptPrivate *priv;
g_return_if_fail (CLUTTER_IS_SCRIPT (script));
priv = script->priv;
g_hash_table_foreach (priv->objects, construct_each_object, script);
}
/**
* clutter_script_get_type_from_name:
* @script: a #ClutterScript
* @type_name: name of the type to look up
*
* Looks up a type by name, using the virtual function that
* #ClutterScript has for that purpose. This function should
* rarely be used.
*
* Return value: the type for the requested type name, or
* %G_TYPE_INVALID if not corresponding type was found.
*
* Since: 0.6
*/
GType
clutter_script_get_type_from_name (ClutterScript *script,
const gchar *type_name)
{
g_return_val_if_fail (CLUTTER_IS_SCRIPT (script), G_TYPE_INVALID);
g_return_val_if_fail (type_name != NULL, G_TYPE_INVALID);
return CLUTTER_SCRIPT_GET_CLASS (script)->get_type_from_name (script, type_name);
}
GQuark
clutter_script_error_quark (void)
{
return g_quark_from_static_string ("clutter-script-error");
}