Initial Import
[profile/ivi/clutter-toys.git] / opt / opt-menu.c
1 /* -*- mode:C; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
2
3 #include "opt.h"
4
5 #define DEFAULT_FONT_SIZE 20
6 #define DEFAULT_FONT "Sans Bold 20"
7 #define ITEM_HEIGHT 24
8 #define TEXT_BORDER 4
9 #define MENU_BORDER 1
10
11 G_DEFINE_TYPE (OptMenu, opt_menu, CLUTTER_TYPE_GROUP);
12
13 static void opt_menu_up (OptMenu * menu);
14 static void opt_menu_down (OptMenu * menu);
15 static void opt_menu_activate (OptMenu * menu);
16 static void opt_menu_select_item (OptMenu * menu, gint slide_no);
17
18 struct OptMenuPrivate
19 {
20   guint             height;
21   gint              current_slide;
22   gint              active_item;
23   gint              item_count;
24   
25   ClutterColor      color_normal;
26   ClutterColor      color_sel;
27   ClutterColor      color_bg;
28
29   OptShow          *show;
30   ClutterActor     *background;
31   ClutterActor     *selection;
32
33   ClutterTimeline  *timeline;
34   ClutterAlpha     *alpha;
35   ClutterBehaviour *behaviour_s;
36   ClutterBehaviour *behaviour_o;
37   
38   gboolean          size_set;
39   gboolean          hiding;
40   guint             timeout_id;
41   gulong            button_release_signal_id;
42   gulong            key_release_signal_id;
43 };
44
45 /* Set sizes for background and selection -- called once the
46  * menu is fully populated
47  */
48 static void
49 opt_menu_init_size (OptMenu * menu)
50 {
51   guint width, height;
52   clutter_actor_get_size (CLUTTER_ACTOR (menu), &width, &height);
53
54   width += 2 * TEXT_BORDER;
55   
56   clutter_actor_set_size (CLUTTER_ACTOR (menu),
57                           width, height);
58   
59   clutter_actor_set_size (CLUTTER_ACTOR (menu->priv->background),
60                           width, height);
61
62   clutter_actor_set_size (CLUTTER_ACTOR (menu->priv->selection),
63                           width - 2 * MENU_BORDER, ITEM_HEIGHT);
64
65   menu->priv->height = height;
66   menu->priv->size_set = TRUE;
67 }
68
69 /* Input callbacks
70  */
71 static void 
72 opt_menu_key_release_cb (ClutterStage          *stage,
73                          ClutterKeyEvent       *kev,
74                          gpointer               user_data)
75 {
76   OptMenu  *menu = OPT_MENU (user_data);
77
78   if (!CLUTTER_ACTOR_IS_VISIBLE (CLUTTER_ACTOR (menu)))
79     return;
80
81   switch (clutter_key_event_symbol (kev))
82     {
83       case CLUTTER_Up:
84           opt_menu_up (menu);
85           break;
86       case CLUTTER_Down:
87           opt_menu_down (menu);
88           break;
89       case CLUTTER_Return:
90           opt_menu_activate (menu);
91           break;
92
93       default:
94           opt_menu_popdown (menu);
95           break;
96     }
97 }
98
99 static void
100 opt_menu_button_release_cb (ClutterStage       *stage,
101                             ClutterButtonEvent *bev,
102                             gpointer            user_data)
103 {
104   OptMenu  *menu = OPT_MENU (user_data);
105
106   if (!CLUTTER_ACTOR_IS_VISIBLE (CLUTTER_ACTOR (menu)))
107     return;
108
109   /* Allow a mouse wheel to control the menu (cannot handle
110    * buttons 1 and 3 here, because those are used to control the slides).
111    */
112
113   if (bev->button == 4)
114     opt_menu_up (menu);
115   else if (bev->button == 5)
116     opt_menu_down (menu);
117   else if (bev->button == 2)
118     opt_menu_activate (menu);
119 }
120
121 static void 
122 opt_menu_finalize (GObject *object)
123 {
124   OptMenu *self = OPT_MENU(object); 
125
126   g_object_unref (G_OBJECT (self->priv->behaviour_s));
127   g_object_unref (G_OBJECT (self->priv->behaviour_o));
128   g_object_unref (G_OBJECT (self->priv->timeline));
129   
130   if (self->priv)
131     {
132       g_free(self->priv);
133       self->priv = NULL;
134     }
135
136   G_OBJECT_CLASS (opt_menu_parent_class)->finalize (object);
137 }
138
139 static void
140 opt_menu_class_init (OptMenuClass *klass)
141 {
142   GObjectClass * object_class = (GObjectClass*) klass;
143   object_class->finalize = opt_menu_finalize;
144 }
145
146 static void
147 opt_menu_init (OptMenu *self)
148 {
149   OptMenuPrivate *priv = g_new0 (OptMenuPrivate, 1);
150   self->priv = priv;
151 }
152
153 static void
154 opt_menu_hide_cb (ClutterTimeline * timeline, gpointer data)
155 {
156   OptMenu  *menu = OPT_MENU (data);
157
158   if (CLUTTER_ACTOR_IS_VISIBLE (CLUTTER_ACTOR (menu)) &&
159       menu->priv->hiding)
160     {
161       ClutterActor * stage = clutter_stage_get_default();
162
163       clutter_actor_hide_all (CLUTTER_ACTOR (menu));
164       clutter_group_remove (CLUTTER_GROUP (stage), CLUTTER_ACTOR (menu));
165       opt_menu_select_item (menu, 0);
166       menu->priv->hiding = FALSE;
167       
168       if (menu->priv->timeout_id)
169         {
170           g_source_remove (menu->priv->timeout_id);
171           menu->priv->timeout_id = 0;
172         }
173     }
174 }
175
176 OptMenu*
177 opt_menu_new (OptShow * show)
178 {
179   OptMenu *menu = g_object_new (OPT_TYPE_MENU, NULL);
180
181   /* TODO -- maybe allow these to be customised
182    */
183   menu->priv->color_normal.red   = 0xff;
184   menu->priv->color_normal.green = 0xff;
185   menu->priv->color_normal.blue  = 0xff;
186   menu->priv->color_normal.alpha = 0xff;
187
188   menu->priv->color_sel.red   = 0;
189   menu->priv->color_sel.green = 0;
190   menu->priv->color_sel.blue  = 0;
191   menu->priv->color_sel.alpha = 0xff;
192
193   menu->priv->color_bg.red   = 0x7f;
194   menu->priv->color_bg.green = 0x7f;
195   menu->priv->color_bg.blue  = 0x7f;
196   menu->priv->color_bg.alpha = 0xcf;
197
198   menu->priv->show = show;
199
200   menu->priv->background =
201     clutter_rectangle_new_with_color (&menu->priv->color_bg);
202   
203   clutter_rectangle_set_border_color(CLUTTER_RECTANGLE(menu->priv->background),
204                                      &menu->priv->color_normal);
205   clutter_rectangle_set_border_width(CLUTTER_RECTANGLE(menu->priv->background),
206                                      1);
207   
208   clutter_group_add (CLUTTER_GROUP (menu),
209                      CLUTTER_ACTOR (menu->priv->background));
210
211   menu->priv->selection =
212     clutter_rectangle_new_with_color (&menu->priv->color_normal);
213
214   clutter_group_add (CLUTTER_GROUP (menu),
215                      CLUTTER_ACTOR (menu->priv->selection));
216   clutter_actor_set_position (CLUTTER_ACTOR (menu->priv->selection),
217                               MENU_BORDER, 0);
218
219   menu->priv->timeline = clutter_timeline_new (10, 26);
220
221   g_signal_connect (menu->priv->timeline, "completed",
222                     G_CALLBACK (opt_menu_hide_cb), menu);
223   
224   menu->priv->alpha = clutter_alpha_new_full (menu->priv->timeline,
225                                               CLUTTER_LINEAR);
226
227   menu->priv->behaviour_s =
228     clutter_behaviour_scale_new (menu->priv->alpha,
229                                  0.0, 0.0, 1.0, 1.0); 
230
231   clutter_behaviour_apply (menu->priv->behaviour_s, CLUTTER_ACTOR (menu));
232
233   menu->priv->behaviour_o =
234     clutter_behaviour_opacity_new (menu->priv->alpha, 0x00, 0xff); 
235
236   clutter_behaviour_apply (menu->priv->behaviour_o, CLUTTER_ACTOR (menu));
237   
238   return menu;
239 }
240
241 /*
242  * Adjusts the postition of the menu if the selected item is
243  * off screen
244  */
245 static void
246 opt_menu_adjust_postion (OptMenu * menu)
247 {
248   if (menu->priv->height > CLUTTER_STAGE_HEIGHT ())
249     {
250       gint x           = clutter_actor_get_x (CLUTTER_ACTOR (menu));
251       gint y           = clutter_actor_get_y (CLUTTER_ACTOR (menu));
252       gint item_offset = menu->priv->active_item * ITEM_HEIGHT + y;
253
254       if (item_offset < 0)
255         {
256           /* attemp to shift the item to the middle of screen, but no so that
257            * the the menu would detach from the top of stage
258            */
259           gint screen_itms = CLUTTER_STAGE_HEIGHT () / ITEM_HEIGHT;
260           gint shift = ITEM_HEIGHT * screen_itms / 2 - item_offset;
261
262           y += shift;
263
264           if (shift > 0)
265             y = 0;
266         }
267       else if (item_offset > CLUTTER_STAGE_HEIGHT () - ITEM_HEIGHT)
268         {
269           /* attemp to shift the item to the middle of screen, but no so that
270            * the the menu would detach from the bottom of stage
271            */
272           gint screen_itms = CLUTTER_STAGE_HEIGHT () / ITEM_HEIGHT;
273           gint shift = ITEM_HEIGHT * screen_itms / 2 + item_offset;
274           gint max_shft = (menu->priv->item_count - screen_itms)*ITEM_HEIGHT;
275           
276           if (shift > max_shft)
277             shift = max_shft;
278           
279           y -= shift;
280         }
281
282       clutter_actor_set_position (CLUTTER_ACTOR (menu), x, y);
283     }
284 }
285
286 /*
287  * Selects nth item in the menu
288  */
289 static void
290 opt_menu_select_item (OptMenu * menu, gint slide_no)
291 {
292   if (slide_no < 0 || slide_no >= menu->priv->item_count)
293     return;
294   
295   if (menu->priv->active_item != slide_no)
296     {
297       /* Plus two, because the first two children are the background
298        * and selection rectangles
299        */
300       ClutterActor * active =
301         clutter_group_get_nth_child (CLUTTER_GROUP (menu),
302                                      menu->priv->active_item + 2);
303   
304       clutter_text_set_color (CLUTTER_TEXT (active),
305                               &menu->priv->color_normal);
306
307       active = clutter_group_get_nth_child (CLUTTER_GROUP (menu),
308                                             slide_no + 2);
309   
310       clutter_text_set_color (CLUTTER_TEXT (active), &menu->priv->color_sel);
311
312       clutter_actor_set_position (CLUTTER_ACTOR (menu->priv->selection),
313                                   MENU_BORDER, slide_no * ITEM_HEIGHT);
314       
315       menu->priv->active_item = slide_no;
316
317       opt_menu_adjust_postion (menu);
318     }
319 }
320
321 /*
322  * Callback to automatically close the menu after given period of inactivity
323  */
324 static gboolean
325 opt_menu_timeout_cb (gpointer data)
326 {
327   OptMenu * menu = data;
328
329   opt_menu_popdown (menu);
330   menu->priv->timeout_id = 0;
331   
332   return FALSE;
333 }
334
335 /*
336  * move one item up in the menu
337  */
338 static void
339 opt_menu_up (OptMenu * menu)
340 {
341   opt_menu_select_item (menu, menu->priv->active_item - 1);
342
343   if (menu->priv->timeout_id)
344     {
345       g_source_remove (menu->priv->timeout_id);
346       menu->priv->timeout_id = g_timeout_add (5000, opt_menu_timeout_cb, menu);
347     }
348 }
349
350 /* move one item down in the menu */
351 static void
352 opt_menu_down (OptMenu * menu)
353 {
354   opt_menu_select_item (menu, menu->priv->active_item + 1);
355
356   if (menu->priv->timeout_id)
357     {
358       g_source_remove (menu->priv->timeout_id);
359       menu->priv->timeout_id = g_timeout_add (5000, opt_menu_timeout_cb, menu);
360     }
361 }
362
363 /*
364  * Jump to the slide represented by the active menu item
365  */
366 static void
367 opt_menu_activate (OptMenu * menu)
368 {
369   int step = menu->priv->active_item - menu->priv->current_slide;
370
371   opt_menu_popdown (menu);
372   
373   if (step)
374     opt_show_skip (menu->priv->show, step);
375 }
376
377 /*
378  *  Called when we mode to a different slide
379  */
380 void
381 opt_menu_set_current_slide (OptMenu * menu, gint slide_no)
382 {
383   opt_menu_select_item (menu, slide_no);
384   menu->priv->current_slide = slide_no;
385 }
386
387 /*
388  * Adds a slide to the menu
389  */
390 void
391 opt_menu_add_slide (OptMenu * menu, OptSlide * slide)
392 {
393   static gint y = 0;
394   
395   gchar              * text = NULL;
396   const gchar        * font = DEFAULT_FONT;
397   const ClutterText  * title = CLUTTER_TEXT (opt_slide_get_title (slide));
398   ClutterActor       * label;
399   
400   if (title)
401     text = g_strdup_printf ("Slide %d: %s", menu->priv->item_count + 1,
402                             clutter_text_get_text ((ClutterText*)title));
403   else
404     text = g_strdup_printf ("Slide %d", menu->priv->item_count + 1);
405
406   if (!menu->priv->item_count)
407     label = clutter_text_new_full (font, text, &menu->priv->color_sel);
408   else
409     label = clutter_text_new_full (font, text, &menu->priv->color_normal);
410   
411   g_free (text);
412   
413   clutter_actor_set_position (label, TEXT_BORDER, y);
414   y += ITEM_HEIGHT;
415       
416   clutter_group_add (CLUTTER_GROUP (menu), label);
417   menu->priv->item_count++;
418 }
419
420 /*
421  * Shows menu
422  */
423 void
424 opt_menu_pop (OptMenu * menu)
425 {
426   if (!CLUTTER_ACTOR_IS_VISIBLE (CLUTTER_ACTOR (menu)))
427     {
428       guint width, height;
429       
430       ClutterActor * stage = clutter_stage_get_default();
431
432       if (!menu->priv->size_set)
433         opt_menu_init_size (menu);
434       
435       clutter_actor_get_size (CLUTTER_ACTOR (menu), &width, &height);
436
437       clutter_actor_set_scale (CLUTTER_ACTOR (menu), 0.0, 0.0);
438       clutter_timeline_set_direction (menu->priv->timeline,
439                                       CLUTTER_TIMELINE_FORWARD);
440       
441       clutter_group_add (CLUTTER_GROUP(stage), CLUTTER_ACTOR(menu));
442
443       clutter_actor_set_position (CLUTTER_ACTOR (menu), 0, 0);
444
445       /* Connect up for input event */
446       menu->priv->button_release_signal_id =
447         g_signal_connect (stage, "button-release-event",
448                           G_CALLBACK (opt_menu_button_release_cb), menu);
449       menu->priv->key_release_signal_id =
450         g_signal_connect (stage, "key-release-event",
451                           G_CALLBACK (opt_menu_key_release_cb), menu);
452
453       opt_menu_select_item (menu, menu->priv->current_slide);
454       clutter_actor_show_all (CLUTTER_ACTOR (menu));
455
456       menu->priv->timeout_id = g_timeout_add (5000, opt_menu_timeout_cb, menu);
457       menu->priv->hiding = FALSE;
458       clutter_timeline_rewind (menu->priv->timeline);
459       clutter_timeline_start (menu->priv->timeline);
460     }
461 }
462
463 /*
464  * Hides menu, if shown
465  */
466 void
467 opt_menu_popdown (OptMenu * menu)
468 {
469   if (CLUTTER_ACTOR_IS_VISIBLE (CLUTTER_ACTOR (menu)))
470   {
471     ClutterActor * stage = clutter_stage_get_default();
472
473     if (menu->priv->button_release_signal_id)
474       {
475         g_signal_handler_disconnect (stage,
476                                      menu->priv->button_release_signal_id);
477         menu->priv->button_release_signal_id = 0;
478       }
479
480     if (menu->priv->key_release_signal_id)
481       {
482         g_signal_handler_disconnect (stage,
483                                      menu->priv->key_release_signal_id);
484         menu->priv->key_release_signal_id = 0;
485       }
486   
487     clutter_actor_set_scale (CLUTTER_ACTOR (menu), 1.0, 1.0);
488     clutter_timeline_set_direction (menu->priv->timeline,
489                                     CLUTTER_TIMELINE_BACKWARD);
490     
491     menu->priv->hiding = TRUE;
492     clutter_timeline_rewind (menu->priv->timeline);
493     clutter_timeline_start (menu->priv->timeline);
494   }
495 }
496