mutter/clutter/clutter-script.c
Emmanuele Bassi 6c01499abf 2007-10-25 Emmanuele Bassi <ebassi@openedhand.com>
* clutter/Makefile.am:
	* clutter/clutter.h:
	* clutter/clutter-scriptable.[ch]: Add the ClutterScriptable
	interface; by implementing this interface, a class can
	override the UI definition parsing and transform complex data
	types into GObject properties, or allow custom properties.

	* clutter/clutter-script.c:
	* clutter/clutter-script-parser.c:
	* clutter/clutter-script-private.h: Rearrange the code and
	use the ClutterScriptable interface to parse and build the
	custom properties. This cleans up the code and also it makes
	it more reliable (the complex type parsing is now done using
	the target type and not just the name of the property).
2007-10-25 14:34:54 +00:00

1575 lines
43 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 (oinfo->type_func)
oinfo->gtype = clutter_script_get_type_from_symbol (oinfo->type_func);
else
oinfo->gtype = clutter_script_get_type_from_class (oinfo->class_name);
}
if (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);
}
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));
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 GList *
clutter_script_get_objects_valist (ClutterScript *script,
const gchar *first_name,
va_list args)
{
GList *retval = NULL;
const gchar *name;
name = first_name;
while (name)
{
retval =
g_list_prepend (retval, clutter_script_get_object (script, name));
name = va_arg (args, gchar*);
}
return g_list_reverse (retval);
}
/**
* clutter_script_get_objects:
* @script: a #ClutterScript
* @first_name: the name of the first object to retrieve
* @Varargs: a %NULL-terminated list of names
*
* Retrieves a list of objects for the given names. This function does
* not increment the reference count of the returned objects.
*
* Return value: a newly allocated #GList containing the found objects,
* or %NULL. Use g_list_free() when done using it.
*
* Since: 0.6
*/
GList *
clutter_script_get_objects (ClutterScript *script,
const gchar *first_name,
...)
{
GList *retval = NULL;
va_list var_args;
g_return_val_if_fail (CLUTTER_IS_SCRIPT (script), NULL);
g_return_val_if_fail (first_name != NULL, NULL);
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);
}
GQuark
clutter_script_error_quark (void)
{
return g_quark_from_static_string ("clutter-script-error");
}