From 8a537b62992a9f4d72d41c42bb7d7176afeac748 Mon Sep 17 00:00:00 2001 From: Emmanuele Bassi Date: Mon, 8 Dec 2008 13:57:10 +0000 Subject: [PATCH] 2008-12-08 Emmanuele Bassi * clutter/Makefile.am: * clutter/clutter.h: Add ClutterBindingPool to the build. * clutter/clutter-binding-pool.c: * clutter/clutter-binding-pool.h: Add ClutterBindingPool, a data structure meant to hold (key symbol, modifiers) pairs and associate them to a closure. The ClutterBindingPool can be used to install key bindings for actors and then execute closures inside the key-press-event signal handlers, removing the need for big switch() or if() blocks for each key. * clutter/clutter-event.c: Consistently use "key symbol" instead of "key value". * clutter/clutter-event.h: Add more modifier masks. * clutter/clutter-marshal.list: * tests/conform/Makefile.am: * tests/conform/test-binding-pool.c: * tests/conform/test-conform-main.c: Add ClutterBindingPool conformance test. * tests/interactive/Makefile.am: * tests/interactive/test-binding-pool.c: Add interactive test (and example code) for the ClutterBindingPool usage. --- ChangeLog | 29 ++ clutter/Makefile.am | 2 + clutter/clutter-binding-pool.c | 694 ++++++++++++++++++++++++++ clutter/clutter-binding-pool.h | 96 ++++ clutter/clutter-event.c | 6 +- clutter/clutter-event.h | 17 +- clutter/clutter-marshal.list | 1 + clutter/clutter.h | 1 + tests/conform/Makefile.am | 3 +- tests/conform/test-binding-pool.c | 313 ++++++++++++ tests/conform/test-conform-main.c | 2 + tests/interactive/Makefile.am | 3 +- tests/interactive/test-binding-pool.c | 313 ++++++++++++ 13 files changed, 1474 insertions(+), 6 deletions(-) create mode 100644 clutter/clutter-binding-pool.c create mode 100644 clutter/clutter-binding-pool.h create mode 100644 tests/conform/test-binding-pool.c create mode 100644 tests/interactive/test-binding-pool.c diff --git a/ChangeLog b/ChangeLog index 5de0a1142..ff4e24eea 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,32 @@ +2008-12-08 Emmanuele Bassi + + * clutter/Makefile.am: + * clutter/clutter.h: Add ClutterBindingPool to the build. + + * clutter/clutter-binding-pool.c: + * clutter/clutter-binding-pool.h: Add ClutterBindingPool, a data + structure meant to hold (key symbol, modifiers) pairs and associate + them to a closure. The ClutterBindingPool can be used to install + key bindings for actors and then execute closures inside the + key-press-event signal handlers, removing the need for big + switch() or if() blocks for each key. + + * clutter/clutter-event.c: Consistently use "key symbol" instead + of "key value". + + * clutter/clutter-event.h: Add more modifier masks. + + * clutter/clutter-marshal.list: + + * tests/conform/Makefile.am: + * tests/conform/test-binding-pool.c: + * tests/conform/test-conform-main.c: Add ClutterBindingPool + conformance test. + + * tests/interactive/Makefile.am: + * tests/interactive/test-binding-pool.c: Add interactive test (and + example code) for the ClutterBindingPool usage. + 2008-12-08 Neil Roberts * clutter/clutter-main.c (_clutter_do_pick): Restore the GL_DITHER diff --git a/clutter/Makefile.am b/clutter/Makefile.am index ee6c19a35..97c9907ad 100644 --- a/clutter/Makefile.am +++ b/clutter/Makefile.am @@ -56,6 +56,7 @@ source_h = \ $(srcdir)/clutter-behaviour-path.h \ $(srcdir)/clutter-behaviour-rotate.h \ $(srcdir)/clutter-behaviour-scale.h \ + $(srcdir)/clutter-binding-pool.h \ $(srcdir)/clutter-child-meta.h \ $(srcdir)/clutter-clone-texture.h \ $(srcdir)/clutter-color.h \ @@ -146,6 +147,7 @@ source_c = \ clutter-behaviour-rotate.c \ clutter-behaviour-scale.c \ clutter-bezier.c \ + clutter-binding-pool.c \ clutter-child-meta.c \ clutter-clone-texture.c \ clutter-color.c \ diff --git a/clutter/clutter-binding-pool.c b/clutter/clutter-binding-pool.c new file mode 100644 index 000000000..8b567b28d --- /dev/null +++ b/clutter/clutter-binding-pool.c @@ -0,0 +1,694 @@ +/* + * Clutter. + * + * An OpenGL based 'interactive canvas' library. + * + * Copyright (C) 2008 Intel Corporation. + * + * Authored By: Emmanuele Bassi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +/** + * SECTION:clutter-binding-pool + * @short_description: Pool for key bindings + * + * #ClutterBindingPool is a data structure holding a set of key bindings. + * Each key binding associates a key symbol (eventually with modifiers) + * to an action. A callback function is associated to each action. + * + * For a given key symbol and modifier mask combination there can be only one + * action; for each action there can be only one callback. There can be + * multiple actions with the same name, and the same callback can be used + * to handle multiple key bindings. + * + * Actors requiring key bindings should create a new #ClutterBindingPool + * inside their class initialization function and then install actions + * like this: + * + * |[ + * static void + * foo_class_init (FooClass *klass) + * { + * ClutterBindingPool *binding_pool; + * + * binding_pool = clutter_binding_pool_get_for_class (klass); + * + * clutter_binding_pool_install_action (binding_pool, "move-up", + * CLUTTER_Up, 0, + * G_CALLBACK (foo_action_move_up), + * NULL, NULL); + * clutter_binding_pool_install_action (binding_pool, "move-up", + * CLUTTER_KP_Up, 0, + * G_CALLBACK (foo_action_move_up), + * NULL, NULL); + * } + * ]| + * + * The callback has a signature of: + * + * |[ + * gboolean (* callback) (GObject *instance, + * const gchar *action_name, + * guint key_val, + * ClutterModifierType modifiers, + * gpointer user_data); + * ]| + * + * The actor should then override the #ClutterActor::key-press-event and + * use clutter_binding_pool_activate() to match a #ClutterKeyEvent structure + * to one of the actions: + * + * |[ + * ClutterBindingPool *pool; + * + * /* retrieve the binding pool for the type of the actor */ + * pool = clutter_binding_pool_find (G_OBJECT_TYPE_NAME (actor)); + * + * /* activate any callback matching the key symbol and modifiers + * * mask of the key event. the returned value can be directly + * * used to signal that the actor has handled the event. + * */ + * return clutter_binding_pool_activate (pool, G_OBJECT (actor), + * key_event->keyval, + * key_event->modifier_state); + * ]| + * + * The clutter_binding_pool_activate() function will return %FALSE if + * no action for the given key binding was found, if the action was + * blocked (using clutter_binding_pool_block_action()) or if the + * key binding handler returned %FALSE. + * + * #ClutterBindingPool is available since Clutter 1.0 + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "clutter-binding-pool.h" +#include "clutter-debug.h" +#include "clutter-enum-types.h" +#include "clutter-marshal.h" +#include "clutter-private.h" + +#define BINDING_MOD_MASK ((CLUTTER_SHIFT_MASK | \ + CLUTTER_CONTROL_MASK | \ + CLUTTER_MOD1_MASK | \ + CLUTTER_SUPER_MASK | \ + CLUTTER_HYPER_MASK | \ + CLUTTER_META_MASK) | CLUTTER_RELEASE_MASK) + +typedef struct _ClutterBindingEntry ClutterBindingEntry; + +static GSList *binding_pools = NULL; +static GQuark key_class_bindings = 0; + +struct _ClutterBindingPool +{ + gchar *name; /* interned string, do not free */ + + GSList *entries; + GHashTable *entries_hash; +}; + +struct _ClutterBindingEntry +{ + gchar *name; /* interned string, do not free */ + + guint key_val; + ClutterModifierType modifiers; + + GClosure *closure; + + guint is_blocked : 1; +}; + +static guint +binding_entry_hash (gconstpointer v) +{ + const ClutterBindingEntry *e = v; + guint h; + + h = e->key_val; + h ^= e->modifiers; + + return h; +} + +static gint +binding_entry_compare (gconstpointer v1, + gconstpointer v2) +{ + const ClutterBindingEntry *e1 = v1; + const ClutterBindingEntry *e2 = v2; + + return (e1->key_val == e2->key_val && e1->modifiers == e2->modifiers); +} + +static ClutterBindingEntry * +binding_entry_new (const gchar *name, + guint key_val, + ClutterModifierType modifiers) +{ + ClutterBindingEntry *entry; + + modifiers = modifiers & BINDING_MOD_MASK; + + entry = g_slice_new (ClutterBindingEntry); + entry->key_val = key_val; + entry->modifiers = modifiers; + entry->name = (gchar *) g_intern_string (name); + entry->closure = NULL; + entry->is_blocked = FALSE; + + return entry; +} + +static ClutterBindingEntry * +binding_pool_lookup_entry (ClutterBindingPool *pool, + guint key_val, + ClutterModifierType modifiers) +{ + ClutterBindingEntry lookup_entry = { 0, }; + + lookup_entry.key_val = key_val; + lookup_entry.modifiers = modifiers; + + return g_hash_table_lookup (pool->entries_hash, &lookup_entry); +} + +static void +binding_entry_free (gpointer data) +{ + if (G_LIKELY (data)) + { + ClutterBindingEntry *entry = data; + + g_closure_unref (entry->closure); + + g_slice_free (ClutterBindingEntry, entry); + } +} + +static void +binding_pool_free (gpointer data) +{ + if (G_LIKELY (data)) + { + ClutterBindingPool *pool = data; + + /* remove from the pools */ + binding_pools = g_slist_remove (binding_pools, pool); + + g_hash_table_destroy (pool->entries_hash); + + g_slist_foreach (pool->entries, (GFunc) binding_entry_free, NULL); + g_slist_free (pool->entries); + + g_slice_free (ClutterBindingPool, pool); + } +} + +/** + * clutter_binding_pool_new: + * @name: the name of the binding pool + * + * Creates a new #ClutterBindingPool that can be used to store + * key bindings for an actor. The @name must be a unique identifier + * for the binding pool, so that clutter_binding_pool_find() will + * be able to return the correct binding pool. + * + * Return value: the newly created binding pool with the given + * name. The binding pool is owned by Clutter and should not + * be freed directly + * + * Since: 1.0 + */ +ClutterBindingPool * +clutter_binding_pool_new (const gchar *name) +{ + ClutterBindingPool *pool; + + g_return_val_if_fail (name != NULL, NULL); + + pool = clutter_binding_pool_find (name); + if (G_UNLIKELY (pool)) + { + g_warning ("A binding pool named '%s' is already present " + "in the binding pools list", + pool->name); + return NULL; + } + + pool = g_slice_new (ClutterBindingPool); + pool->name = (gchar *) g_intern_string (name); + pool->entries = NULL; + pool->entries_hash = g_hash_table_new (binding_entry_hash, + binding_entry_compare); + + binding_pools = g_slist_prepend (binding_pools, pool); + + return pool; +} + +/** + * clutter_binding_pool_get_for_class: + * @klass: a #GObjectClass pointer + * + * Retrieves the #ClutterBindingPool for the given #GObject class + * and, eventually, creates it. This function is a wrapper around + * clutter_binding_pool_new() and uses the class type name as the + * unique name for the binding pool. + * + * Calling this function multiple times will return the same + * #ClutterBindingPool. + * + * A binding pool for a class can also be retrieved using + * clutter_binding_pool_find() with the class type name: + * + * |[ + * pool = clutter_binding_pool_find (G_OBJECT_TYPE_NAME (instance)); + * ]| + * + * Return value: the binding pool for the given class. The returned + * #ClutterBindingPool is owned by Clutter and should not be freed + * directly + * + * Since: 1.0 + */ +ClutterBindingPool * +clutter_binding_pool_get_for_class (gpointer klass) +{ + ClutterBindingPool *pool; + + g_return_val_if_fail (G_IS_OBJECT_CLASS (klass), NULL); + + if (G_UNLIKELY (key_class_bindings == 0)) + key_class_bindings = g_quark_from_static_string ("clutter-bindings-set"); + + pool = g_dataset_id_get_data (klass, key_class_bindings); + if (pool) + return pool; + + pool = clutter_binding_pool_new (G_OBJECT_CLASS_NAME (klass)); + g_dataset_id_set_data_full (klass, key_class_bindings, + pool, + binding_pool_free); + + return pool; +} + +/** + * clutter_binding_pool_find: + * @name: the name of the binding pool to find + * + * Finds the #ClutterBindingPool with @name. + * + * Return value: a pointer to the #ClutterBindingPool, or %NULL + * + * Since: 1.0 + */ +ClutterBindingPool * +clutter_binding_pool_find (const gchar *name) +{ + GSList *l; + + g_return_val_if_fail (name != NULL, NULL); + + for (l = binding_pools; l != NULL; l = l->next) + { + ClutterBindingPool *pool = l->data; + + if (g_str_equal (pool->name, (gpointer) name)) + return pool; + } + + return NULL; +} + +/** + * clutter_binding_pool_install_action: + * @pool: a #ClutterBindingPool + * @action_name: the name of the action + * @key_val: key symbol + * @modifiers: bitmask of modifiers + * @callback: function to be called when the action is activated + * @data: data to be passed to @callback + * @notify: function to be called when the action is removed + * from the pool + * + * Installs a new action inside a #ClutterBindingPool. The action + * is bound to @key_val and @modifiers. + * + * The same action name can be used for multiple @key_val, @modifiers + * pairs. + * + * When an action has been activated using clutter_binding_pool_activate() + * the passed @callback will be invoked (with @data). + * + * Actions can be blocked with clutter_binding_pool_block_action() + * and then unblocked using clutter_binding_pool_unblock_action(). + * + * Since: 1.0 + */ +void +clutter_binding_pool_install_action (ClutterBindingPool *pool, + const gchar *action_name, + guint key_val, + ClutterModifierType modifiers, + GCallback callback, + gpointer data, + GDestroyNotify notify) +{ + ClutterBindingEntry *entry; + GClosure *closure; + + g_return_if_fail (pool != NULL); + g_return_if_fail (action_name != NULL); + g_return_if_fail (key_val != 0); + g_return_if_fail (callback != NULL); + + entry = binding_pool_lookup_entry (pool, key_val, modifiers); + if (G_UNLIKELY (entry)) + { + g_warning ("There already is an action '%s' for the given " + "key symbol of %d (modifiers: %d) installed inside " + "the binding pool.", + entry->name, + entry->key_val, entry->modifiers); + return; + } + else + entry = binding_entry_new (action_name, key_val, modifiers); + + closure = g_cclosure_new (callback, data, (GClosureNotify) notify); + entry->closure = g_closure_ref (closure); + g_closure_sink (closure); + + if (G_CLOSURE_NEEDS_MARSHAL (closure)) + { + GClosureMarshal marshal; + + marshal = clutter_marshal_BOOLEAN__STRING_UINT_ENUM; + g_closure_set_marshal (closure, marshal); + } + + pool->entries = g_slist_prepend (pool->entries, entry); + g_hash_table_insert (pool->entries_hash, entry, entry); +} + +/** + * clutter_binding_pool_install_closure: + * @pool: a #ClutterBindingPool + * @action_name: the name of the action + * @key_val: key symbol + * @modifiers: bitmask of modifiers + * @closure: a #GClosure + * + * A #GClosure variant of clutter_binding_pool_install_action(). + * + * Installs a new action inside a #ClutterBindingPool. The action + * is bound to @key_val and @modifiers. + * + * The same action name can be used for multiple @key_val, @modifiers + * pairs. + * + * When an action has been activated using clutter_binding_pool_activate() + * the passed @closure will be invoked. + * + * Actions can be blocked with clutter_binding_pool_block_action() + * and then unblocked using clutter_binding_pool_unblock_action(). + * + * Since: 1.0 + */ +void +clutter_binding_pool_install_closure (ClutterBindingPool *pool, + const gchar *action_name, + guint key_val, + ClutterModifierType modifiers, + GClosure *closure) +{ + ClutterBindingEntry *entry; + + g_return_if_fail (pool != NULL); + g_return_if_fail (action_name != NULL); + g_return_if_fail (key_val != 0); + g_return_if_fail (closure != NULL); + + entry = binding_pool_lookup_entry (pool, key_val, modifiers); + if (G_UNLIKELY (entry)) + { + g_warning ("There already is an action '%s' for the given " + "key symbol of %d (modifiers: %d) installed inside " + "the binding pool.", + entry->name, + entry->key_val, entry->modifiers); + return; + } + else + entry = binding_entry_new (action_name, key_val, modifiers); + + entry->closure = g_closure_ref (closure); + g_closure_sink (closure); + + if (G_CLOSURE_NEEDS_MARSHAL (closure)) + { + GClosureMarshal marshal; + + marshal = clutter_marshal_BOOLEAN__STRING_UINT_ENUM; + g_closure_set_marshal (closure, marshal); + } + + pool->entries = g_slist_prepend (pool->entries, entry); + g_hash_table_insert (pool->entries_hash, entry, entry); +} + +gchar ** +clutter_binding_pool_list_actions (ClutterBindingPool *pool) +{ + return NULL; +} + +/** + * clutter_binding_pool_find_action: + * @pool: a #ClutterBindingPool + * @key_val: a key symbol + * @modifiers: a bitmask for the modifiers + * + * Retrieves the name of the action matching the given key symbol + * and modifiers bitmask. + * + * Return value: the name of the action, if found, or %NULL. The + * returned string is owned by the binding pool and should never + * be modified or freed + * + * Since: 1.0 + */ +G_CONST_RETURN gchar * +clutter_binding_pool_find_action (ClutterBindingPool *pool, + guint key_val, + ClutterModifierType modifiers) +{ + ClutterBindingEntry *entry; + + g_return_val_if_fail (pool != NULL, NULL); + g_return_val_if_fail (key_val != 0, NULL); + + entry = binding_pool_lookup_entry (pool, key_val, modifiers); + if (!entry) + return NULL; + + return entry->name; +} + +/** + * clutter_binding_pool_remove_action: + * @pool: a #ClutterBindingPool + * @key_val: a key symbol + * @modifiers: a bitmask for the modifiers + * + * Removes the action matching the given @key_val, @modifiers pair, + * if any exists. + * + * Since: 1.0 + */ +void +clutter_binding_pool_remove_action (ClutterBindingPool *pool, + guint key_val, + ClutterModifierType modifiers) +{ + ClutterBindingEntry remove_entry = { 0, }; + + g_return_if_fail (pool != NULL); + g_return_if_fail (key_val != 0); + + modifiers = modifiers & BINDING_MOD_MASK; + + remove_entry.key_val = key_val; + remove_entry.modifiers = modifiers; + + g_hash_table_remove (pool->entries_hash, &remove_entry); +} + +static gboolean +clutter_binding_entry_invoke (ClutterBindingEntry *entry, + GObject *gobject) +{ + GValue params[4] = { { 0, }, { 0, }, { 0, }, { 0, } }; + GValue result = { 0, }; + gboolean retval = TRUE; + + g_value_init (¶ms[0], G_TYPE_OBJECT); + g_value_set_object (¶ms[0], gobject); + + g_value_init (¶ms[1], G_TYPE_STRING); + g_value_set_string (¶ms[1], entry->name); + + g_value_init (¶ms[2], G_TYPE_UINT); + g_value_set_uint (¶ms[2], entry->key_val); + + g_value_init (¶ms[3], CLUTTER_TYPE_MODIFIER_TYPE); + g_value_set_flags (¶ms[3], entry->modifiers); + + g_value_init (&result, G_TYPE_BOOLEAN); + + g_closure_invoke (entry->closure, &result, 4, params, NULL); + + retval = g_value_get_boolean (&result); + + g_value_unset (&result); + + g_value_unset (¶ms[0]); + g_value_unset (¶ms[1]); + g_value_unset (¶ms[2]); + g_value_unset (¶ms[3]); + + return retval; +} + +/** + * clutter_binding_pool_activate: + * @pool: a #ClutterBindingPool + * @key_val: the key symbol + * @modifiers: bitmask for the modifiers + * @gobject: a #GObject + * + * Activates the callback associated to the action that is + * bound to the @key_val and @modifiers pair. + * + * The callback has the following signature: + * + * |[ + * void (* callback) (GObject *gobject, + * const gchar *action_name, + * guint key_val, + * ClutterModifierType modifiers, + * gpointer user_data); + * ]| + * + * Where the #GObject instance is @gobject and the user data + * is the one passed when installing the action with + * clutter_binding_pool_install_action(). + * + * If the action bound to the @key_val, @modifiers pair has been + * blocked using clutter_binding_pool_block_action(), the callback + * will not be invoked, and this function will return %FALSE. + * + * Return value: %TRUE if an action was found and was activated + * + * Since: 1.0 + */ +gboolean +clutter_binding_pool_activate (ClutterBindingPool *pool, + guint key_val, + ClutterModifierType modifiers, + GObject *gobject) +{ + ClutterBindingEntry *entry = NULL; + + g_return_val_if_fail (pool != NULL, FALSE); + g_return_val_if_fail (key_val != 0, FALSE); + g_return_val_if_fail (G_IS_OBJECT (gobject), FALSE); + + modifiers = (modifiers & BINDING_MOD_MASK); + + entry = binding_pool_lookup_entry (pool, key_val, modifiers); + if (!entry) + return FALSE; + + if (!entry->is_blocked) + return clutter_binding_entry_invoke (entry, gobject); + + return FALSE; +} + +/** + * clutter_binding_pool_block_action: + * @pool: a #ClutterBindingPool + * @action_name: an action name + * + * Blocks all the actions with name @action_name inside @pool. + * + * Since: 1.0 + */ +void +clutter_binding_pool_block_action (ClutterBindingPool *pool, + const gchar *action_name) +{ + GSList *l; + + g_return_if_fail (pool != NULL); + g_return_if_fail (action_name != NULL); + + for (l = pool->entries; l != NULL; l = l->next) + { + ClutterBindingEntry *entry = l->data; + + if (g_str_equal (entry->name, (gpointer) action_name)) + entry->is_blocked = TRUE; + } +} + +/** + * clutter_binding_pool_unblock_action: + * @pool: a #ClutterBindingPool + * @action_name: an action name + * + * Unblockes all the actions with name @action_name inside @pool. + * + * Unblocking an action does not cause the callback bound to it to + * be invoked in case clutter_binding_pool_activate() was called on + * an action previously blocked with clutter_binding_pool_block_action(). + * + * Since: 1.0 + */ +void +clutter_binding_pool_unblock_action (ClutterBindingPool *pool, + const gchar *action_name) +{ + GSList *l; + + g_return_if_fail (pool != NULL); + g_return_if_fail (action_name != NULL); + + for (l = pool->entries; l != NULL; l = l->next) + { + ClutterBindingEntry *entry = l->data; + + if (g_str_equal (entry->name, (gpointer) action_name)) + entry->is_blocked = FALSE; + } +} diff --git a/clutter/clutter-binding-pool.h b/clutter/clutter-binding-pool.h new file mode 100644 index 000000000..0fa0712a8 --- /dev/null +++ b/clutter/clutter-binding-pool.h @@ -0,0 +1,96 @@ +/* + * Clutter. + * + * An OpenGL based 'interactive canvas' library. + * + * Copyright (C) 2008 Intel Corporation. + * + * Authored By: Emmanuele Bassi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#if !defined(__CLUTTER_H_INSIDE__) && !defined(CLUTTER_COMPILATION) +#error "Only can be included directly." +#endif + +#ifndef __CLUTTER_BINDING_POOL_H__ +#define __CLUTTER_BINDING_POOL_H__ + +#include +#include + +G_BEGIN_DECLS + +typedef struct _ClutterBindingPool ClutterBindingPool; + +/** + * ClutterBindingActionFunc: + * @gobject: a #GObject + * @action_name: the name of the action + * @key_val: the key symbol + * @modifiers: bitmask of the modifier flags + * + * The prototype for the callback function registered with + * clutter_binding_pool_install_action() and invoked by + * clutter_binding_pool_activate(). + * + * The function should return %TRUE if the key binding was + * handled, and return %FALSE otherwise. + * + * Since: 1.0 + */ +typedef gboolean (* ClutterBindingActionFunc) (GObject *gobject, + const gchar *action_name, + guint key_val, + ClutterModifierType modifiers); + +ClutterBindingPool * clutter_binding_pool_new (const gchar *name); +ClutterBindingPool * clutter_binding_pool_get_for_class (gpointer klass); +ClutterBindingPool * clutter_binding_pool_find (const gchar *name); + +void clutter_binding_pool_install_action (ClutterBindingPool *pool, + const gchar *action_name, + guint key_val, + ClutterModifierType modifiers, + GCallback callback, + gpointer data, + GDestroyNotify notify); +void clutter_binding_pool_install_closure (ClutterBindingPool *pool, + const gchar *action_name, + guint key_val, + ClutterModifierType modifiers, + GClosure *closure); + +gchar ** clutter_binding_pool_list_actions (ClutterBindingPool *pool); +G_CONST_RETURN gchar *clutter_binding_pool_find_action (ClutterBindingPool *pool, + guint key_val, + ClutterModifierType modifiers); +void clutter_binding_pool_remove_action (ClutterBindingPool *pool, + guint key_val, + ClutterModifierType modifiers); + +gboolean clutter_binding_pool_activate (ClutterBindingPool *pool, + guint key_val, + ClutterModifierType modifiers, + GObject *gobject); + +void clutter_binding_pool_block_action (ClutterBindingPool *pool, + const gchar *action_name); +void clutter_binding_pool_unblock_action (ClutterBindingPool *pool, + const gchar *action_name); + +G_END_DECLS + +#endif /* __CLUTTER_BINDING_POOL_H__ */ diff --git a/clutter/clutter-event.c b/clutter/clutter-event.c index 87bfa3844..b03b0dfc7 100644 --- a/clutter/clutter-event.c +++ b/clutter/clutter-event.c @@ -224,9 +224,9 @@ clutter_button_event_button (ClutterButtonEvent *buttev) * clutter_key_event_symbol: * @keyev: A #ClutterKeyEvent * - * Retrieves the value of the key that caused @keyev. + * Retrieves the symbol of the key that caused @keyev. * - * Return value: The keysym representing the key + * Return value: The key symbol representing the key */ guint clutter_key_event_symbol (ClutterKeyEvent *keyev) @@ -273,7 +273,7 @@ clutter_key_event_unicode (ClutterKeyEvent *keyev) /** * clutter_keysym_to_unicode: - * @keyval: a clutter key symbol + * @keyval: a key symbol * * Convert from a Clutter key symbol to the corresponding ISO10646 (Unicode) * character. diff --git a/clutter/clutter-event.h b/clutter/clutter-event.h index 7ad6c7ea8..4c557ca3a 100644 --- a/clutter/clutter-event.h +++ b/clutter/clutter-event.h @@ -68,6 +68,11 @@ G_BEGIN_DECLS * @CLUTTER_BUTTON3_MASK: Mask applied by the third pointer button * @CLUTTER_BUTTON4_MASK: Mask applied by the fourth pointer button * @CLUTTER_BUTTON5_MASK: Mask applied by the fifth pointer button + * @CLUTTER_SUPER_MASK: Mask applied by the Super key + * @CLUTTER_HYPER_MASK: Mask applied by the Hyper key + * @CLUTTER_META_MASK: Mask applied by the Meta key + * @CLUTTER_RELEASE_MASK: Mask applied during release + * @CLUTTER_MODIFIER_MASK: A mask covering all modifier types * * Masks applied to a #ClutterEvent by modifiers. * @@ -86,7 +91,17 @@ typedef enum { CLUTTER_BUTTON2_MASK = 1 << 9, CLUTTER_BUTTON3_MASK = 1 << 10, CLUTTER_BUTTON4_MASK = 1 << 11, - CLUTTER_BUTTON5_MASK = 1 << 12 + CLUTTER_BUTTON5_MASK = 1 << 12, + + /* bits 15 to 25 are currently unused; bit 29 is used internally */ + + CLUTTER_SUPER_MASK = 1 << 26, + CLUTTER_HYPER_MASK = 1 << 27, + CLUTTER_META_MASK = 1 << 28, + + CLUTTER_RELEASE_MASK = 1 << 30, + + CLUTTER_MODIFIER_MASK = 0x5c001fff } ClutterModifierType; /** diff --git a/clutter/clutter-marshal.list b/clutter/clutter-marshal.list index 3c1ac97b1..613d84a9e 100644 --- a/clutter/clutter-marshal.list +++ b/clutter/clutter-marshal.list @@ -1,4 +1,5 @@ BOOLEAN:BOXED +BOOLEAN:STRING,UINT,ENUM UINT:VOID VOID:BOXED VOID:INT diff --git a/clutter/clutter.h b/clutter/clutter.h index 25e8deda5..20d336d47 100644 --- a/clutter/clutter.h +++ b/clutter/clutter.h @@ -41,6 +41,7 @@ #include "clutter-behaviour-path.h" #include "clutter-behaviour-rotate.h" #include "clutter-behaviour-scale.h" +#include "clutter-binding-pool.h" #include "clutter-child-meta.h" #include "clutter-clone-texture.h" #include "clutter-deprecated.h" diff --git a/tests/conform/Makefile.am b/tests/conform/Makefile.am index ed6dd00e7..985d74125 100644 --- a/tests/conform/Makefile.am +++ b/tests/conform/Makefile.am @@ -21,7 +21,8 @@ test_conformance_SOURCES = \ test-clutter-fixed.c \ test-actor-invariants.c \ test-paint-opacity.c \ - test-backface-culling.c + test-backface-culling.c \ + test-binding-pool.c # For convenience, this provides a way to easily run individual unit tests: .PHONY: wrappers diff --git a/tests/conform/test-binding-pool.c b/tests/conform/test-binding-pool.c new file mode 100644 index 000000000..4a6f4ebaa --- /dev/null +++ b/tests/conform/test-binding-pool.c @@ -0,0 +1,313 @@ +#include + +#include + +#include +#include + +#include "test-conform-common.h" + +#define TYPE_KEY_GROUP (key_group_get_type ()) +#define KEY_GROUP(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), TYPE_KEY_GROUP, KeyGroup)) +#define IS_KEY_GROUP(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), TYPE_KEY_GROUP)) +#define KEY_GROUP_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), TYPE_KEY_GROUP, KeyGroupClass)) +#define IS_KEY_GROUP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), TYPE_KEY_GROUP)) + +typedef struct _KeyGroup KeyGroup; +typedef struct _KeyGroupClass KeyGroupClass; + +struct _KeyGroup +{ + ClutterGroup parent_instance; + + gint selected_index; +}; + +struct _KeyGroupClass +{ + ClutterGroupClass parent_class; + + void (* activate) (KeyGroup *group, + ClutterActor *child); +}; + +G_DEFINE_TYPE (KeyGroup, key_group, CLUTTER_TYPE_GROUP); + +enum +{ + ACTIVATE, + + LAST_SIGNAL +}; + +static guint group_signals[LAST_SIGNAL] = { 0, }; + +static gboolean +key_group_action_move_left (KeyGroup *self, + const gchar *action_name, + guint key_val, + ClutterModifierType modifiers) +{ + gint n_children; + + g_assert_cmpstr (action_name, ==, "move-left"); + g_assert_cmpint (key_val, ==, CLUTTER_Left); + + n_children = clutter_group_get_n_children (CLUTTER_GROUP (self)); + + self->selected_index -= 1; + + if (self->selected_index < 0) + self->selected_index = n_children - 1; + + return TRUE; +} + +static gboolean +key_group_action_move_right (KeyGroup *self, + const gchar *action_name, + guint key_val, + ClutterModifierType modifiers) +{ + gint n_children; + + g_assert_cmpstr (action_name, ==, "move-right"); + g_assert_cmpint (key_val, ==, CLUTTER_Right); + + n_children = clutter_group_get_n_children (CLUTTER_GROUP (self)); + + self->selected_index += 1; + + if (self->selected_index >= n_children) + self->selected_index = 0; + + return TRUE; +} + +static gboolean +key_group_action_activate (KeyGroup *self, + const gchar *action_name, + guint key_val, + ClutterModifierType modifiers) +{ + ClutterActor *child = NULL; + + g_assert_cmpstr (action_name, ==, "activate"); + g_assert (key_val == CLUTTER_Return || + key_val == CLUTTER_KP_Enter || + key_val == CLUTTER_ISO_Enter); + + if (self->selected_index == -1) + return FALSE; + + child = clutter_group_get_nth_child (CLUTTER_GROUP (self), + self->selected_index); + + if (child) + { + g_signal_emit (self, group_signals[ACTIVATE], 0, child); + return TRUE; + } + else + return FALSE; +} + +static gboolean +key_group_key_press (ClutterActor *actor, + ClutterKeyEvent *event) +{ + ClutterBindingPool *pool; + gboolean res; + + pool = clutter_binding_pool_find (G_OBJECT_TYPE_NAME (actor)); + g_assert (pool != NULL); + + res = clutter_binding_pool_activate (pool, + event->keyval, + event->modifier_state, + G_OBJECT (actor)); + + /* if we activate a key binding, redraw the actor */ + if (res) + clutter_actor_queue_redraw (actor); + + return res; +} + +static void +key_group_paint (ClutterActor *actor) +{ + KeyGroup *self = KEY_GROUP (actor); + GList *children, *l; + gint i; + + children = clutter_container_get_children (CLUTTER_CONTAINER (self)); + + for (l = children, i = 0; l != NULL; l = l->next, i++) + { + ClutterActor *child = l->data; + + /* paint the selection rectangle */ + if (i == self->selected_index) + { + ClutterActorBox box = { 0, }; + + clutter_actor_get_allocation_box (child, &box); + + box.x1 -= CLUTTER_UNITS_FROM_DEVICE (2); + box.y1 -= CLUTTER_UNITS_FROM_DEVICE (2); + box.x2 += CLUTTER_UNITS_FROM_DEVICE (2); + box.y2 += CLUTTER_UNITS_FROM_DEVICE (2); + + cogl_set_source_color4ub (255, 255, 0, 224); + cogl_rectangle (CLUTTER_UNITS_TO_DEVICE (box.x1), + CLUTTER_UNITS_TO_DEVICE (box.y1), + CLUTTER_UNITS_TO_DEVICE (box.x2 - box.x1), + CLUTTER_UNITS_TO_DEVICE (box.y2 - box.y1)); + } + + if (CLUTTER_ACTOR_IS_VISIBLE (child)) + clutter_actor_paint (child); + } + + g_list_free (children); +} + +static void +key_group_finalize (GObject *gobject) +{ + G_OBJECT_CLASS (key_group_parent_class)->finalize (gobject); +} + +static void +key_group_class_init (KeyGroupClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + ClutterBindingPool *binding_pool; + + gobject_class->finalize = key_group_finalize; + + actor_class->paint = key_group_paint; + actor_class->key_press_event = key_group_key_press; + + group_signals[ACTIVATE] = + g_signal_new (g_intern_static_string ("activate"), + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (KeyGroupClass, activate), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + CLUTTER_TYPE_ACTOR); + + binding_pool = clutter_binding_pool_get_for_class (klass); + + clutter_binding_pool_install_action (binding_pool, "move-right", + CLUTTER_Right, 0, + G_CALLBACK (key_group_action_move_right), + NULL, NULL); + clutter_binding_pool_install_action (binding_pool, "move-left", + CLUTTER_Left, 0, + G_CALLBACK (key_group_action_move_left), + NULL, NULL); + clutter_binding_pool_install_action (binding_pool, "activate", + CLUTTER_Return, 0, + G_CALLBACK (key_group_action_activate), + NULL, NULL); + clutter_binding_pool_install_action (binding_pool, "activate", + CLUTTER_KP_Enter, 0, + G_CALLBACK (key_group_action_activate), + NULL, NULL); + clutter_binding_pool_install_action (binding_pool, "activate", + CLUTTER_ISO_Enter, 0, + G_CALLBACK (key_group_action_activate), + NULL, NULL); +} + +static void +key_group_init (KeyGroup *self) +{ + self->selected_index = -1; +} + +static void +init_event (ClutterKeyEvent *event) +{ + event->type = CLUTTER_KEY_PRESS; + event->time = 0; /* not needed */ + event->flags = CLUTTER_EVENT_FLAG_SYNTHETIC; + event->stage = NULL; /* not needed */ + event->source = NULL; /* not needed */ + event->modifier_state = 0; + event->hardware_keycode = 0; /* not needed */ +} + +static void +send_keyval (KeyGroup *group, int keyval) +{ + ClutterKeyEvent event; + + init_event (&event); + event.keyval = keyval; + event.unicode_value = 0; /* should be ignored for cursor keys etc. */ + + clutter_actor_event (CLUTTER_ACTOR (group), (ClutterEvent *) &event, FALSE); +} + +static void +on_activate (KeyGroup *key_group, + ClutterActor *child, + gpointer data) +{ + gint index = GPOINTER_TO_INT (data); + + g_assert_cmpint (key_group->selected_index, ==, index); +} + +void +test_binding_pool (TestConformSimpleFixture *fixture, + gconstpointer data) +{ + KeyGroup *key_group = g_object_new (TYPE_KEY_GROUP, NULL); + + clutter_container_add (CLUTTER_CONTAINER (key_group), + g_object_new (CLUTTER_TYPE_RECTANGLE, + "width", 50, + "height", 50, + "x", 0, "y", 0, + NULL), + g_object_new (CLUTTER_TYPE_RECTANGLE, + "width", 50, + "height", 50, + "x", 75, "y", 0, + NULL), + g_object_new (CLUTTER_TYPE_RECTANGLE, + "width", 50, + "height", 50, + "x", 150, "y", 0, + NULL), + NULL); + + g_assert_cmpint (key_group->selected_index, ==, -1); + + send_keyval (key_group, CLUTTER_Left); + g_assert_cmpint (key_group->selected_index, ==, 2); + + send_keyval (key_group, CLUTTER_Left); + g_assert_cmpint (key_group->selected_index, ==, 1); + + send_keyval (key_group, CLUTTER_Right); + g_assert_cmpint (key_group->selected_index, ==, 2); + + send_keyval (key_group, CLUTTER_Right); + g_assert_cmpint (key_group->selected_index, ==, 0); + + g_signal_connect (key_group, + "activate", G_CALLBACK (on_activate), + GINT_TO_POINTER (0)); + + send_keyval (key_group, CLUTTER_Return); + + clutter_actor_destroy (CLUTTER_ACTOR (key_group)); +} diff --git a/tests/conform/test-conform-main.c b/tests/conform/test-conform-main.c index 611bb48e5..ff9535baf 100644 --- a/tests/conform/test-conform-main.c +++ b/tests/conform/test-conform-main.c @@ -107,5 +107,7 @@ main (int argc, char **argv) TEST_CONFORM_SIMPLE ("/path", test_path); + TEST_CONFORM_SIMPLE ("/binding-pool", test_binding_pool); + return g_test_run (); } diff --git a/tests/interactive/Makefile.am b/tests/interactive/Makefile.am index ede802a6b..dcc4e673a 100644 --- a/tests/interactive/Makefile.am +++ b/tests/interactive/Makefile.am @@ -37,7 +37,8 @@ UNIT_TESTS = \ test-texture-quality.c \ test-layout.c \ test-animation.c \ - test-easing.c + test-easing.c \ + test-binding-pool.c if X11_TESTS UNIT_TESTS += test-pixmap.c diff --git a/tests/interactive/test-binding-pool.c b/tests/interactive/test-binding-pool.c new file mode 100644 index 000000000..8fbcb4816 --- /dev/null +++ b/tests/interactive/test-binding-pool.c @@ -0,0 +1,313 @@ +#include +#include + +#include +#include + +#include +#include + +#define TYPE_KEY_GROUP (key_group_get_type ()) +#define KEY_GROUP(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), TYPE_KEY_GROUP, KeyGroup)) +#define IS_KEY_GROUP(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), TYPE_KEY_GROUP)) +#define KEY_GROUP_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), TYPE_KEY_GROUP, KeyGroupClass)) +#define IS_KEY_GROUP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), TYPE_KEY_GROUP)) + +typedef struct _KeyGroup KeyGroup; +typedef struct _KeyGroupClass KeyGroupClass; + +struct _KeyGroup +{ + ClutterGroup parent_instance; + + gint selected_index; +}; + +struct _KeyGroupClass +{ + ClutterGroupClass parent_class; + + void (* activate) (KeyGroup *group, + ClutterActor *child); +}; + +G_DEFINE_TYPE (KeyGroup, key_group, CLUTTER_TYPE_GROUP); + +enum +{ + ACTIVATE, + + LAST_SIGNAL +}; + +static guint group_signals[LAST_SIGNAL] = { 0, }; + +static gboolean +key_group_action_move_left (KeyGroup *self, + const gchar *action_name, + guint key_val, + ClutterModifierType modifiers) +{ + gint n_children; + + g_debug ("%s: activated '%s' (k:%d, m:%d)", + G_STRLOC, + action_name, + key_val, + modifiers); + + n_children = clutter_group_get_n_children (CLUTTER_GROUP (self)); + + self->selected_index -= 1; + + if (self->selected_index < 0) + self->selected_index = n_children - 1; + + return TRUE; +} + +static gboolean +key_group_action_move_right (KeyGroup *self, + const gchar *action_name, + guint key_val, + ClutterModifierType modifiers) +{ + gint n_children; + + g_debug ("%s: activated '%s' (k:%d, m:%d)", + G_STRLOC, + action_name, + key_val, + modifiers); + + n_children = clutter_group_get_n_children (CLUTTER_GROUP (self)); + + self->selected_index += 1; + + if (self->selected_index >= n_children) + self->selected_index = 0; + + return TRUE; +} + +static gboolean +key_group_action_activate (KeyGroup *self, + const gchar *action_name, + guint key_val, + ClutterModifierType modifiers) +{ + ClutterActor *child = NULL; + + g_debug ("%s: activated '%s' (k:%d, m:%d)", + G_STRLOC, + action_name, + key_val, + modifiers); + + if (self->selected_index == -1) + return FALSE; + + child = clutter_group_get_nth_child (CLUTTER_GROUP (self), + self->selected_index); + + if (child) + { + g_signal_emit (self, group_signals[ACTIVATE], 0, child); + return TRUE; + } + else + return FALSE; +} + +static gboolean +key_group_key_press (ClutterActor *actor, + ClutterKeyEvent *event) +{ + ClutterBindingPool *pool; + gboolean res; + + pool = clutter_binding_pool_find (G_OBJECT_TYPE_NAME (actor)); + g_assert (pool != NULL); + + res = clutter_binding_pool_activate (pool, + event->keyval, + event->modifier_state, + G_OBJECT (actor)); + + /* if we activate a key binding, redraw the actor */ + if (res) + clutter_actor_queue_redraw (actor); + + return res; +} + +static void +key_group_paint (ClutterActor *actor) +{ + KeyGroup *self = KEY_GROUP (actor); + GList *children, *l; + gint i; + + children = clutter_container_get_children (CLUTTER_CONTAINER (self)); + + for (l = children, i = 0; l != NULL; l = l->next, i++) + { + ClutterActor *child = l->data; + + /* paint the selection rectangle */ + if (i == self->selected_index) + { + ClutterActorBox box = { 0, }; + + clutter_actor_get_allocation_box (child, &box); + + box.x1 -= CLUTTER_UNITS_FROM_DEVICE (2); + box.y1 -= CLUTTER_UNITS_FROM_DEVICE (2); + box.x2 += CLUTTER_UNITS_FROM_DEVICE (2); + box.y2 += CLUTTER_UNITS_FROM_DEVICE (2); + + cogl_set_source_color4ub (255, 255, 0, 224); + cogl_rectangle (CLUTTER_UNITS_TO_DEVICE (box.x1), + CLUTTER_UNITS_TO_DEVICE (box.y1), + CLUTTER_UNITS_TO_DEVICE (box.x2 - box.x1), + CLUTTER_UNITS_TO_DEVICE (box.y2 - box.y1)); + } + + if (CLUTTER_ACTOR_IS_VISIBLE (child)) + clutter_actor_paint (child); + } + + g_list_free (children); +} + +static void +key_group_finalize (GObject *gobject) +{ + G_OBJECT_CLASS (key_group_parent_class)->finalize (gobject); +} + +static void +key_group_class_init (KeyGroupClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + ClutterBindingPool *binding_pool; + + gobject_class->finalize = key_group_finalize; + + actor_class->paint = key_group_paint; + actor_class->key_press_event = key_group_key_press; + + group_signals[ACTIVATE] = + g_signal_new (g_intern_static_string ("activate"), + G_OBJECT_CLASS_TYPE (gobject_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (KeyGroupClass, activate), + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + CLUTTER_TYPE_ACTOR); + + binding_pool = clutter_binding_pool_get_for_class (klass); + + clutter_binding_pool_install_action (binding_pool, "move-right", + CLUTTER_Right, 0, + G_CALLBACK (key_group_action_move_right), + NULL, NULL); + clutter_binding_pool_install_action (binding_pool, "move-left", + CLUTTER_Left, 0, + G_CALLBACK (key_group_action_move_left), + NULL, NULL); + clutter_binding_pool_install_action (binding_pool, "activate", + CLUTTER_Return, 0, + G_CALLBACK (key_group_action_activate), + NULL, NULL); + clutter_binding_pool_install_action (binding_pool, "activate", + CLUTTER_KP_Enter, 0, + G_CALLBACK (key_group_action_activate), + NULL, NULL); + clutter_binding_pool_install_action (binding_pool, "activate", + CLUTTER_ISO_Enter, 0, + G_CALLBACK (key_group_action_activate), + NULL, NULL); +} + +static void +key_group_init (KeyGroup *self) +{ + self->selected_index = -1; +} + +static void +on_key_group_activate (KeyGroup *group, + ClutterActor *child) +{ + g_print ("Child '%d' activated!\n", clutter_actor_get_gid (child)); +} + +G_MODULE_EXPORT int +test_binding_pool_main (int argc, char *argv[]) +{ + ClutterActor *stage, *key_group; + ClutterColor red_color = { 255, 0, 0, 255 }; + ClutterColor green_color = { 0, 255, 0, 255 }; + ClutterColor blue_color = { 0, 0, 255, 255 }; + gint group_x, group_y; + + clutter_init (&argc, &argv); + + stage = clutter_stage_get_default (); + g_signal_connect (stage, + "button-press-event", G_CALLBACK (clutter_main_quit), + NULL); + + key_group = g_object_new (TYPE_KEY_GROUP, NULL); + clutter_container_add_actor (CLUTTER_CONTAINER (stage), key_group); + + /* add three rectangles to the key group */ + clutter_container_add (CLUTTER_CONTAINER (key_group), + g_object_new (CLUTTER_TYPE_RECTANGLE, + "color", &red_color, + "width", 50, + "height", 50, + "x", 0, + "y", 0, + NULL), + g_object_new (CLUTTER_TYPE_RECTANGLE, + "color", &green_color, + "width", 50, + "height", 50, + "x", 75, + "y", 0, + NULL), + g_object_new (CLUTTER_TYPE_RECTANGLE, + "color", &blue_color, + "width", 50, + "height", 50, + "x", 150, + "y", 0, + NULL), + NULL); + + g_signal_connect (key_group, + "activate", G_CALLBACK (on_key_group_activate), + NULL); + + group_x = + (clutter_actor_get_width (stage) - clutter_actor_get_width (key_group)) + / 2; + group_y = + (clutter_actor_get_height (stage) - clutter_actor_get_height (key_group)) + / 2; + + clutter_actor_set_position (key_group, group_x, group_y); + clutter_actor_set_reactive (key_group, TRUE); + + clutter_stage_set_key_focus (CLUTTER_STAGE (stage), key_group); + + clutter_actor_show (stage); + + clutter_main (); + + return EXIT_SUCCESS; +}