b0e785c6c2
When binding models to actors to map items to children we don't often need the full control of a function; in many cases we just need to specify the type of the child we want to construct and the properties on both the item and the child that we want to bind. We should provide a simple convenience function that does all this for us.
523 lines
15 KiB
C
523 lines
15 KiB
C
#include <glib-object.h>
|
|
#include <gio/gio.h>
|
|
#include <clutter/clutter.h>
|
|
|
|
/* {{{ MenuItemModel */
|
|
|
|
/* This is our "model" of a Menu item; it has a "label" property, and
|
|
* a "selected" state property. The user is supposed to operate on the
|
|
* model instance, and change its state.
|
|
*/
|
|
|
|
#define EXAMPLE_TYPE_MENU_ITEM_MODEL (example_menu_item_model_get_type ())
|
|
|
|
G_DECLARE_FINAL_TYPE (ExampleMenuItemModel, example_menu_item_model, EXAMPLE, MENU_ITEM_MODEL, GObject)
|
|
|
|
struct _ExampleMenuItemModel
|
|
{
|
|
GObject parent_instance;
|
|
|
|
char *label;
|
|
|
|
gboolean selected;
|
|
};
|
|
|
|
struct _ExampleMenuItemModelClass
|
|
{
|
|
GObjectClass parent_class;
|
|
};
|
|
|
|
enum {
|
|
MENU_ITEM_MODEL_PROP_LABEL = 1,
|
|
MENU_ITEM_MODEL_PROP_SELECTED,
|
|
MENU_ITEM_MODEL_N_PROPS
|
|
};
|
|
|
|
static GParamSpec *menu_item_model_props[MENU_ITEM_MODEL_N_PROPS] = { NULL, };
|
|
|
|
G_DEFINE_TYPE (ExampleMenuItemModel, example_menu_item_model, G_TYPE_OBJECT)
|
|
|
|
static void
|
|
example_menu_item_model_finalize (GObject *gobject)
|
|
{
|
|
ExampleMenuItemModel *self = (ExampleMenuItemModel *) gobject;
|
|
|
|
g_free (self->label);
|
|
|
|
G_OBJECT_CLASS (example_menu_item_model_parent_class)->finalize (gobject);
|
|
}
|
|
|
|
static void
|
|
example_menu_item_model_set_property (GObject *gobject,
|
|
guint prop_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
ExampleMenuItemModel *self = (ExampleMenuItemModel *) gobject;
|
|
|
|
switch (prop_id)
|
|
{
|
|
case MENU_ITEM_MODEL_PROP_LABEL:
|
|
g_free (self->label);
|
|
self->label = g_value_dup_string (value);
|
|
break;
|
|
|
|
case MENU_ITEM_MODEL_PROP_SELECTED:
|
|
self->selected = g_value_get_boolean (value);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
example_menu_item_model_get_property (GObject *gobject,
|
|
guint prop_id,
|
|
GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
ExampleMenuItemModel *self = (ExampleMenuItemModel *) gobject;
|
|
|
|
switch (prop_id)
|
|
{
|
|
case MENU_ITEM_MODEL_PROP_LABEL:
|
|
g_value_set_string (value, self->label);
|
|
break;
|
|
|
|
case MENU_ITEM_MODEL_PROP_SELECTED:
|
|
g_value_set_boolean (value, self->selected);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
example_menu_item_model_class_init (ExampleMenuItemModelClass *klass)
|
|
{
|
|
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
|
|
|
|
gobject_class->set_property = example_menu_item_model_set_property;
|
|
gobject_class->get_property = example_menu_item_model_get_property;
|
|
gobject_class->finalize = example_menu_item_model_finalize;
|
|
|
|
menu_item_model_props[MENU_ITEM_MODEL_PROP_LABEL] =
|
|
g_param_spec_string ("label", NULL, NULL,
|
|
NULL,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
|
|
menu_item_model_props[MENU_ITEM_MODEL_PROP_SELECTED] =
|
|
g_param_spec_boolean ("selected", NULL, NULL,
|
|
FALSE,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
|
|
g_object_class_install_properties (gobject_class, MENU_ITEM_MODEL_N_PROPS, menu_item_model_props);
|
|
}
|
|
|
|
static void
|
|
example_menu_item_model_init (ExampleMenuItemModel *self)
|
|
{
|
|
}
|
|
/* }}} */
|
|
|
|
/* {{{ MenuItemView */
|
|
|
|
/* This is our "view" of a Menu item; it changes state depending on whether
|
|
* the "selected" property is set. The "view" reflects the state of the
|
|
* "model" instance, though it has no direct connection to it.
|
|
*/
|
|
#define EXAMPLE_TYPE_MENU_ITEM_VIEW (example_menu_item_view_get_type ())
|
|
|
|
G_DECLARE_FINAL_TYPE (ExampleMenuItemView, example_menu_item_view, EXAMPLE, MENU_ITEM_VIEW, ClutterText)
|
|
|
|
struct _ExampleMenuItemView
|
|
{
|
|
ClutterText parent_instance;
|
|
|
|
gboolean is_selected;
|
|
};
|
|
|
|
struct _ExampleMenuItemViewClass
|
|
{
|
|
ClutterTextClass parent_class;
|
|
};
|
|
|
|
G_DEFINE_TYPE (ExampleMenuItemView, example_menu_item_view, CLUTTER_TYPE_TEXT)
|
|
|
|
enum {
|
|
MENU_ITEM_VIEW_PROP_SELECTED = 1,
|
|
MENU_ITEM_VIEW_N_PROPS
|
|
};
|
|
|
|
static GParamSpec *menu_item_view_props[MENU_ITEM_VIEW_N_PROPS] = { NULL, };
|
|
|
|
static void
|
|
example_menu_item_view_set_selected (ExampleMenuItemView *self,
|
|
gboolean selected)
|
|
{
|
|
selected = !!selected;
|
|
if (self->is_selected == selected)
|
|
return;
|
|
|
|
self->is_selected = selected;
|
|
|
|
if (self->is_selected)
|
|
clutter_text_set_color (CLUTTER_TEXT (self), CLUTTER_COLOR_LightSkyBlue);
|
|
else
|
|
clutter_text_set_color (CLUTTER_TEXT (self), CLUTTER_COLOR_White);
|
|
|
|
g_object_notify_by_pspec (G_OBJECT (self), menu_item_view_props[MENU_ITEM_VIEW_PROP_SELECTED]);
|
|
}
|
|
|
|
static void
|
|
example_menu_item_view_set_property (GObject *gobject,
|
|
guint prop_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
switch (prop_id)
|
|
{
|
|
case MENU_ITEM_VIEW_PROP_SELECTED:
|
|
example_menu_item_view_set_selected (EXAMPLE_MENU_ITEM_VIEW (gobject),
|
|
g_value_get_boolean (value));
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
example_menu_item_view_get_property (GObject *gobject,
|
|
guint prop_id,
|
|
GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
switch (prop_id)
|
|
{
|
|
case MENU_ITEM_VIEW_PROP_SELECTED:
|
|
g_value_set_boolean (value, EXAMPLE_MENU_ITEM_VIEW (gobject)->is_selected);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
example_menu_item_view_class_init (ExampleMenuItemViewClass *klass)
|
|
{
|
|
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
|
|
|
|
gobject_class->set_property = example_menu_item_view_set_property;
|
|
gobject_class->get_property = example_menu_item_view_get_property;
|
|
|
|
menu_item_view_props[MENU_ITEM_VIEW_PROP_SELECTED] =
|
|
g_param_spec_boolean ("selected", NULL, NULL,
|
|
FALSE,
|
|
G_PARAM_READWRITE |
|
|
G_PARAM_STATIC_STRINGS);
|
|
|
|
g_object_class_install_properties (gobject_class, MENU_ITEM_VIEW_N_PROPS, menu_item_view_props);
|
|
}
|
|
|
|
static void
|
|
example_menu_item_view__transition_stopped (ClutterActor *actor,
|
|
const char *transition,
|
|
gboolean is_finished)
|
|
{
|
|
clutter_actor_set_scale (actor, 1.0, 1.0);
|
|
clutter_actor_set_opacity (actor, 255);
|
|
}
|
|
|
|
static void
|
|
example_menu_item_view_init (ExampleMenuItemView *self)
|
|
{
|
|
ClutterText *text = CLUTTER_TEXT (self);
|
|
ClutterActor *actor = CLUTTER_ACTOR (self);
|
|
ClutterTransition *scalex_trans, *scaley_trans, *fade_trans;
|
|
ClutterTransition *group;
|
|
|
|
clutter_text_set_font_name (text, "Sans Bold 24px");
|
|
clutter_text_set_color (text, CLUTTER_COLOR_White);
|
|
|
|
clutter_actor_set_margin_left (actor, 12);
|
|
clutter_actor_set_margin_right (actor, 12);
|
|
|
|
clutter_actor_set_pivot_point (actor, 0.5, 0.5);
|
|
|
|
scalex_trans = clutter_property_transition_new ("scale-x");
|
|
clutter_transition_set_from (scalex_trans, G_TYPE_FLOAT, 1.0);
|
|
clutter_transition_set_to (scalex_trans, G_TYPE_FLOAT, 3.0);
|
|
|
|
scaley_trans = clutter_property_transition_new ("scale-y");
|
|
clutter_transition_set_from (scaley_trans, G_TYPE_FLOAT, 1.0);
|
|
clutter_transition_set_to (scaley_trans, G_TYPE_FLOAT, 3.0);
|
|
|
|
fade_trans = clutter_property_transition_new ("opacity");
|
|
clutter_transition_set_to (fade_trans, G_TYPE_UINT, 0);
|
|
|
|
group = clutter_transition_group_new ();
|
|
clutter_transition_group_add_transition (CLUTTER_TRANSITION_GROUP (group), scalex_trans);
|
|
clutter_transition_group_add_transition (CLUTTER_TRANSITION_GROUP (group), scaley_trans);
|
|
clutter_transition_group_add_transition (CLUTTER_TRANSITION_GROUP (group), fade_trans);
|
|
clutter_timeline_set_duration (CLUTTER_TIMELINE (group), 250);
|
|
clutter_timeline_set_progress_mode (CLUTTER_TIMELINE (group), CLUTTER_EASE_OUT);
|
|
|
|
clutter_actor_add_transition (actor, "activateTransition", group);
|
|
g_object_unref (group);
|
|
|
|
clutter_timeline_stop (CLUTTER_TIMELINE (group));
|
|
|
|
g_signal_connect (actor, "transition-stopped",
|
|
G_CALLBACK (example_menu_item_view__transition_stopped),
|
|
group);
|
|
}
|
|
|
|
static void
|
|
example_menu_item_view_activate (ExampleMenuItemView *self)
|
|
{
|
|
ClutterTransition *t;
|
|
|
|
t = clutter_actor_get_transition (CLUTTER_ACTOR (self), "activateTransition");
|
|
clutter_timeline_start (CLUTTER_TIMELINE (t));
|
|
}
|
|
|
|
/* }}} */
|
|
|
|
/* {{{ Menu */
|
|
|
|
/* This is our container actor, which binds the GListStore with the
|
|
* ExampleMenuItemModel instances to the ExampleMenuItemView actors
|
|
*/
|
|
|
|
#define EXAMPLE_TYPE_MENU (example_menu_get_type ())
|
|
|
|
G_DECLARE_FINAL_TYPE (ExampleMenu, example_menu, EXAMPLE, MENU, ClutterActor)
|
|
|
|
struct _ExampleMenu
|
|
{
|
|
ClutterActor parent_instance;
|
|
|
|
int current_idx;
|
|
};
|
|
|
|
struct _ExampleMenuClass
|
|
{
|
|
ClutterActorClass parent_class;
|
|
};
|
|
|
|
G_DEFINE_TYPE (ExampleMenu, example_menu, CLUTTER_TYPE_ACTOR)
|
|
|
|
static void
|
|
example_menu_class_init (ExampleMenuClass *klass)
|
|
{
|
|
}
|
|
|
|
static void
|
|
example_menu_init (ExampleMenu *self)
|
|
{
|
|
ClutterActor *actor = CLUTTER_ACTOR (self);
|
|
ClutterLayoutManager *layout;
|
|
|
|
layout = clutter_box_layout_new ();
|
|
clutter_box_layout_set_orientation (CLUTTER_BOX_LAYOUT (layout), CLUTTER_ORIENTATION_VERTICAL);
|
|
clutter_box_layout_set_spacing (CLUTTER_BOX_LAYOUT (layout), 12);
|
|
|
|
clutter_actor_set_layout_manager (actor, layout);
|
|
clutter_actor_set_background_color (actor, CLUTTER_COLOR_Black);
|
|
|
|
self->current_idx = -1;
|
|
}
|
|
|
|
static ClutterActor *
|
|
example_menu_select_item (ExampleMenu *self,
|
|
int idx)
|
|
{
|
|
ClutterActor *item;
|
|
|
|
/* Any change in the view is reflected into the model */
|
|
|
|
if (idx == self->current_idx)
|
|
return clutter_actor_get_child_at_index (CLUTTER_ACTOR (self), self->current_idx);
|
|
|
|
item = clutter_actor_get_child_at_index (CLUTTER_ACTOR (self), self->current_idx);
|
|
if (item != NULL)
|
|
example_menu_item_view_set_selected ((ExampleMenuItemView *) item, FALSE);
|
|
|
|
if (idx < 0)
|
|
idx = clutter_actor_get_n_children (CLUTTER_ACTOR (self)) - 1;
|
|
else if (idx >= clutter_actor_get_n_children (CLUTTER_ACTOR (self)))
|
|
idx = 0;
|
|
|
|
self->current_idx = idx;
|
|
|
|
item = clutter_actor_get_child_at_index (CLUTTER_ACTOR (self), self->current_idx);
|
|
if (item != NULL)
|
|
example_menu_item_view_set_selected ((ExampleMenuItemView *) item, TRUE);
|
|
|
|
return item;
|
|
}
|
|
|
|
static ClutterActor *
|
|
example_menu_select_next (ExampleMenu *self)
|
|
{
|
|
return example_menu_select_item (self, self->current_idx + 1);
|
|
}
|
|
|
|
static ClutterActor *
|
|
example_menu_select_prev (ExampleMenu *self)
|
|
{
|
|
return example_menu_select_item (self, self->current_idx - 1);
|
|
}
|
|
|
|
static void
|
|
example_menu_activate_item (ExampleMenu *self)
|
|
{
|
|
ClutterActor *child;
|
|
|
|
child = clutter_actor_get_child_at_index (CLUTTER_ACTOR (self),
|
|
self->current_idx);
|
|
if (child == NULL)
|
|
return;
|
|
|
|
example_menu_item_view_activate ((ExampleMenuItemView *) child);
|
|
}
|
|
|
|
/* }}} */
|
|
|
|
/* {{{ main */
|
|
static gboolean
|
|
on_key_press (ClutterActor *stage,
|
|
ClutterEvent *event)
|
|
{
|
|
ClutterActor *scroll = clutter_actor_get_first_child (stage);
|
|
ClutterActor *menu = clutter_actor_get_first_child (scroll);
|
|
ClutterActor *item = NULL;
|
|
guint key = clutter_event_get_key_symbol (event);
|
|
ClutterPoint p;
|
|
|
|
switch (key)
|
|
{
|
|
case CLUTTER_KEY_q:
|
|
clutter_main_quit ();
|
|
break;
|
|
|
|
case CLUTTER_KEY_Up:
|
|
item = example_menu_select_prev ((ExampleMenu *) menu);
|
|
clutter_actor_get_position (item, &p.x, &p.y);
|
|
break;
|
|
|
|
case CLUTTER_KEY_Down:
|
|
item = example_menu_select_next ((ExampleMenu *) menu);
|
|
clutter_actor_get_position (item, &p.x, &p.y);
|
|
break;
|
|
|
|
case CLUTTER_KEY_Return:
|
|
case CLUTTER_KEY_KP_Enter:
|
|
example_menu_activate_item ((ExampleMenu *) menu);
|
|
break;
|
|
}
|
|
|
|
if (item != NULL)
|
|
clutter_scroll_actor_scroll_to_point (CLUTTER_SCROLL_ACTOR (scroll), &p);
|
|
|
|
return CLUTTER_EVENT_PROPAGATE;
|
|
}
|
|
|
|
static void
|
|
on_model_item_selection (GObject *model_item,
|
|
GParamSpec *pspec,
|
|
gpointer data)
|
|
{
|
|
char *label = NULL;
|
|
gboolean is_selected = FALSE;
|
|
|
|
g_object_get (model_item, "label", &label, "selected", &is_selected, NULL);
|
|
|
|
if (is_selected)
|
|
g_print ("Item '%s' selected!\n", label);
|
|
|
|
g_free (label);
|
|
}
|
|
|
|
static ClutterActor *
|
|
create_menu_actor (void)
|
|
{
|
|
/* Our store of menu item models */
|
|
GListStore *model = g_list_store_new (EXAMPLE_TYPE_MENU_ITEM_MODEL);
|
|
ClutterActor *menu = g_object_new (EXAMPLE_TYPE_MENU, NULL);
|
|
int i;
|
|
|
|
/* Populate the model */
|
|
for (i = 0; i < 12; i++)
|
|
{
|
|
char *label = g_strdup_printf ("Option %02d", i + 1);
|
|
|
|
ExampleMenuItemModel *item = g_object_new (EXAMPLE_TYPE_MENU_ITEM_MODEL,
|
|
"label", label,
|
|
NULL);
|
|
|
|
g_list_store_append (model, item);
|
|
|
|
g_signal_connect (item, "notify::selected",
|
|
G_CALLBACK (on_model_item_selection),
|
|
NULL);
|
|
|
|
g_object_unref (item);
|
|
g_free (label);
|
|
}
|
|
|
|
/* Bind the list of menu item models to the menu actor; this will
|
|
* create ClutterActor views of each item in the model, and add them
|
|
* to the menu actor
|
|
*/
|
|
clutter_actor_bind_model_with_properties (menu, G_LIST_MODEL (model),
|
|
EXAMPLE_TYPE_MENU_ITEM_VIEW,
|
|
"label", "text", G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE,
|
|
"selected", "selected", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE,
|
|
NULL);
|
|
|
|
/* We don't need a pointer to the model any more, so we transfer ownership
|
|
* to the menu actor; this means that the model will go away when the menu
|
|
* actor is destroyed
|
|
*/
|
|
g_object_unref (model);
|
|
|
|
/* Select the first item in the menu */
|
|
example_menu_select_item ((ExampleMenu *) menu, 0);
|
|
|
|
return menu;
|
|
}
|
|
|
|
/* The scrolling container for the menu */
|
|
static ClutterActor *
|
|
create_scroll_actor (void)
|
|
{
|
|
ClutterActor *menu = clutter_scroll_actor_new ();
|
|
clutter_actor_set_name (menu, "scroll");
|
|
clutter_scroll_actor_set_scroll_mode (CLUTTER_SCROLL_ACTOR (menu),
|
|
CLUTTER_SCROLL_VERTICALLY);
|
|
clutter_actor_set_easing_duration (menu, 250);
|
|
clutter_actor_add_child (menu, create_menu_actor ());
|
|
|
|
return menu;
|
|
}
|
|
|
|
int
|
|
main (int argc, char *argv[])
|
|
{
|
|
ClutterActor *stage, *menu;
|
|
|
|
if (clutter_init (&argc, &argv) != CLUTTER_INIT_SUCCESS)
|
|
return 1;
|
|
|
|
stage = clutter_stage_new ();
|
|
clutter_stage_set_title (CLUTTER_STAGE (stage), "Actor Model");
|
|
clutter_stage_set_user_resizable (CLUTTER_STAGE (stage), TRUE);
|
|
g_signal_connect (stage, "destroy", G_CALLBACK (clutter_main_quit), NULL);
|
|
g_signal_connect (stage, "key-press-event", G_CALLBACK (on_key_press), NULL);
|
|
clutter_actor_show (stage);
|
|
|
|
#define PADDING 18.f
|
|
|
|
menu = create_scroll_actor ();
|
|
clutter_actor_set_position (menu, 0, PADDING);
|
|
clutter_actor_add_constraint (menu, clutter_align_constraint_new (stage, CLUTTER_ALIGN_X_AXIS, 0.5));
|
|
clutter_actor_add_constraint (menu, clutter_bind_constraint_new (stage, CLUTTER_BIND_HEIGHT, -PADDING * 2));
|
|
clutter_actor_add_child (stage, menu);
|
|
|
|
clutter_main ();
|
|
|
|
return 0;
|
|
}
|
|
/* }}} */
|