No issues found
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2 *
3 * Copyright (C) 2010 Jonathan Matthew <jonathan@d14n.org>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * The Rhythmbox authors hereby grant permission for non-GPL compatible
11 * GStreamer plugins to be used and distributed together with GStreamer
12 * and Rhythmbox. This permission is above and beyond the permissions granted
13 * by the GPL license by which Rhythmbox is covered. If you modify this code
14 * you may extend this exception to your version of the code, but you are not
15 * obligated to do so. If you do not wish to do so, delete this exception
16 * statement from your version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program; if not, write to the Free Software
25 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
26 */
27
28 #include "config.h"
29
30 #include <glib/gi18n.h>
31 #include <clutter-gtk/clutter-gtk.h>
32 #include <mx/mx.h>
33
34 #include "rb-visualizer-fullscreen.h"
35
36 #include <shell/rb-shell-player.h>
37 #include <rhythmdb/rhythmdb.h>
38 #include <lib/rb-file-helpers.h>
39 #include <lib/rb-util.h>
40 #include <lib/rb-debug.h>
41 #include <metadata/rb-ext-db.h>
42
43 #define MAX_IMAGE_HEIGHT 128 /* should be style-controlled, but it's tricky */
44 #define FULLSCREEN_BORDER_WIDTH 32 /* this should be style-controlled too */
45
46 #define TRACK_INFO_DATA "rb-track-info-actor"
47 #define CONTROLS_DATA "rb-controls-actor"
48
49 static MxStyle *style = NULL;
50
51 void
52 rb_visualizer_fullscreen_load_style (GObject *plugin)
53 {
54 char *file;
55
56 if (style == NULL) {
57 style = mx_style_new ();
58
59 file = rb_find_plugin_data_file (plugin, "visualizer.css");
60 if (file != NULL) {
61 mx_style_load_from_file (style, file, NULL);
62 g_free (file);
63 }
64 }
65 }
66
67 /* cover art display */
68
69 /* use a 'missing image' image instead? */
70 static void
71 set_blank_image (MxFrame *frame)
72 {
73 ClutterActor *blank;
74 ClutterColor nothing = { 0, 0, 0, 0 };
75
76 blank = clutter_rectangle_new_with_color (¬hing);
77 clutter_actor_set_height (blank, MAX_IMAGE_HEIGHT);
78 clutter_actor_set_width (blank, MAX_IMAGE_HEIGHT);
79 mx_bin_set_child (MX_BIN (frame), blank);
80 }
81
82 static void
83 art_cb (RBExtDBKey *key, const char *filename, GValue *data, MxFrame *frame)
84 {
85 ClutterActor *image;
86 GdkPixbuf *pixbuf;
87
88 if (data == NULL || G_VALUE_HOLDS (data, GDK_TYPE_PIXBUF) == FALSE) {
89 return;
90 }
91
92 clutter_threads_enter ();
93
94 image = gtk_clutter_texture_new ();
95 pixbuf = GDK_PIXBUF (g_value_get_object (data));
96 gtk_clutter_texture_set_from_pixbuf (GTK_CLUTTER_TEXTURE (image), pixbuf, NULL);
97 if (clutter_actor_get_height (image) > MAX_IMAGE_HEIGHT) {
98 clutter_actor_set_height (image, MAX_IMAGE_HEIGHT);
99 clutter_texture_set_keep_aspect_ratio (CLUTTER_TEXTURE (image), TRUE);
100 }
101 if (clutter_actor_get_width (image) > MAX_IMAGE_HEIGHT) {
102 clutter_actor_set_width (image, MAX_IMAGE_HEIGHT);
103 }
104 mx_bin_set_child (MX_BIN (frame), image);
105 clutter_actor_show_all (CLUTTER_ACTOR (frame));
106
107 clutter_threads_leave ();
108 }
109
110 static void
111 request_cover_art (MxFrame *frame, RhythmDBEntry *entry)
112 {
113 RBExtDBKey *key;
114 RBExtDB *art_store;
115
116 art_store = rb_ext_db_new ("album-art");
117
118 key = rhythmdb_entry_create_ext_db_key (entry, RHYTHMDB_PROP_ALBUM);
119 rb_ext_db_request (art_store, key, (RBExtDBRequestCallback) art_cb, g_object_ref (frame), g_object_unref);
120 rb_ext_db_key_free (key);
121
122 g_object_unref (art_store);
123 }
124
125 static void
126 cover_art_entry_changed_cb (RBShellPlayer *player, RhythmDBEntry *entry, MxFrame *frame)
127 {
128 clutter_threads_enter ();
129 set_blank_image (frame);
130 clutter_actor_show_all (CLUTTER_ACTOR (frame));
131 clutter_threads_leave ();
132
133 request_cover_art (frame, entry);
134 }
135
136 /* track info display */
137
138 static void
139 get_artist_album_templates (const char *artist,
140 const char *album,
141 const char **artist_template,
142 const char **album_template)
143 {
144 PangoDirection tag_dir;
145 PangoDirection template_dir;
146
147 /* Translators: by Artist */
148 *artist_template = _("by <i>%s</i>");
149 /* Translators: from Album */
150 *album_template = _("from <i>%s</i>");
151
152 /* find the direction (left-to-right or right-to-left) of the
153 * track's tags and the localized templates
154 */
155 if (artist != NULL && artist[0] != '\0') {
156 tag_dir = pango_find_base_dir (artist, -1);
157 template_dir = pango_find_base_dir (*artist_template, -1);
158 } else if (album != NULL && album[0] != '\0') {
159 tag_dir = pango_find_base_dir (album, -1);
160 template_dir = pango_find_base_dir (*album_template, -1);
161 } else {
162 return;
163 }
164
165 /* if the track's tags and the localized templates have a different
166 * direction, switch to direction-neutral templates in order to improve
167 * display.
168 * text can have a neutral direction, this condition only applies when
169 * both directions are defined and they are conflicting.
170 * https://bugzilla.gnome.org/show_bug.cgi?id=609767
171 */
172 if (((tag_dir == PANGO_DIRECTION_LTR) && (template_dir == PANGO_DIRECTION_RTL)) ||
173 ((tag_dir == PANGO_DIRECTION_RTL) && (template_dir == PANGO_DIRECTION_LTR))) {
174 /* these strings should not be localized, they must be
175 * locale-neutral and direction-neutral
176 */
177 *artist_template = "<i>%s</i>";
178 *album_template = "/ <i>%s</i>";
179 }
180 }
181
182 static void
183 str_append_printf_escaped (GString *str, const char *format, ...)
184 {
185 va_list args;
186 char *bit;
187
188 va_start (args, format);
189 bit = g_markup_vprintf_escaped (format, args);
190 va_end (args);
191
192 g_string_append (str, bit);
193 g_free (bit);
194 }
195
196 static void
197 update_track_info (MxLabel *label, RhythmDB *db, RhythmDBEntry *entry, const char *streaming_title)
198 {
199 const char *title;
200 ClutterActor *text;
201 GString *str;
202
203 text = mx_label_get_clutter_text (label);
204
205 str = g_string_sized_new (100);
206 if (entry == NULL) {
207 g_string_append_printf (str, "<big>%s</big>", _("Not Playing"));
208 } else {
209 title = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE);
210
211 if (streaming_title) {
212 str_append_printf_escaped (str, "<big>%s</big>\n", streaming_title);
213 str_append_printf_escaped (str, _("from <i>%s</i>"), title);
214 } else {
215 const char *artist_template = NULL;
216 const char *album_template = NULL;
217 const char *artist;
218 const char *album;
219 gboolean space = FALSE;
220
221 artist = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ARTIST);
222 album = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM);
223 get_artist_album_templates (artist, album, &artist_template, &album_template);
224
225 str_append_printf_escaped (str, "<big>%s</big>\n", title);
226
227 if (album != NULL && album[0] != '\0') {
228 str_append_printf_escaped (str, album_template, album);
229 space = TRUE;
230 }
231 if (artist != NULL && artist[0] != '\0') {
232 if (space) {
233 g_string_append_c (str, ' ');
234 }
235 str_append_printf_escaped (str, artist_template, artist);
236 }
237 }
238 }
239
240 /* tiny bit of extra padding */
241 g_string_append (str, " ");
242 clutter_text_set_markup (CLUTTER_TEXT (text), str->str);
243 clutter_text_set_ellipsize (CLUTTER_TEXT (text), PANGO_ELLIPSIZE_NONE);
244 g_string_free (str, TRUE);
245 }
246
247 static void
248 update_track_info_lock (MxLabel *label, RhythmDB *db, RhythmDBEntry *entry, const char *streaming_title)
249 {
250 clutter_threads_enter ();
251 update_track_info (label, db, entry, streaming_title);
252 clutter_threads_leave ();
253 }
254
255 static void
256 playing_song_changed_cb (RBShellPlayer *player, RhythmDBEntry *entry, ClutterActor *label)
257 {
258 RhythmDB *db;
259
260 g_object_get (player, "db", &db, NULL);
261 update_track_info_lock (MX_LABEL (label), db, entry, NULL);
262 g_object_unref (db);
263 }
264
265 static void
266 entry_changed_cb (RhythmDB *db, RhythmDBEntry *entry, GArray *changes, ClutterActor *label)
267 {
268 int i;
269 /* somehow check entry == playing entry */
270
271 for (i = 0; i < changes->len; i++) {
272 GValue *v = &g_array_index (changes, GValue, i);
273 RhythmDBEntryChange *change = g_value_get_boxed (v);
274 switch (change->prop) {
275 case RHYTHMDB_PROP_TITLE:
276 case RHYTHMDB_PROP_ARTIST:
277 case RHYTHMDB_PROP_ALBUM:
278 update_track_info_lock (MX_LABEL (label), db, entry, NULL);
279 return;
280
281 default:
282 break;
283 }
284 }
285 }
286
287 static void
288 streaming_title_notify_cb (RhythmDB *db, RhythmDBEntry *entry, const char *field, GValue *metadata, ClutterActor *label)
289 {
290 if (G_VALUE_HOLDS_STRING (metadata)) {
291 update_track_info_lock (MX_LABEL (label), db, entry, g_value_get_string (metadata));
292 }
293 }
294
295
296 /* elapsed time / duration display */
297
298 static void
299 update_elapsed (ClutterActor *label, RBShellPlayer *player, guint elapsed)
300 {
301 long duration;
302 char *str;
303
304 duration = rb_shell_player_get_playing_song_duration (player);
305 str = rb_make_elapsed_time_string (elapsed, duration, FALSE);
306 mx_label_set_text (MX_LABEL (label), str);
307 g_free (str);
308 }
309
310 static void
311 elapsed_changed_cb (RBShellPlayer *player, guint elapsed, ClutterActor *label)
312 {
313 clutter_threads_enter ();
314 update_elapsed (label, player, elapsed);
315 clutter_threads_leave ();
316 }
317
318
319 static ClutterActor *
320 create_track_info (RBShell *shell)
321 {
322 RBShellPlayer *player;
323 RhythmDB *db;
324 ClutterActor *box;
325 ClutterActor *box2;
326 ClutterActor *widget;
327 ClutterActor *frame;
328 RhythmDBEntry *entry;
329 GValue *value;
330 guint elapsed;
331
332 g_object_get (shell, "shell-player", &player, "db", &db, NULL);
333 entry = rb_shell_player_get_playing_entry (player);
334
335 box = mx_box_layout_new ();
336 mx_box_layout_set_orientation (MX_BOX_LAYOUT (box), MX_ORIENTATION_HORIZONTAL);
337 mx_box_layout_set_spacing (MX_BOX_LAYOUT (box), 16);
338 mx_stylable_set_style_class (MX_STYLABLE (box), "TrackInfoBox");
339 mx_stylable_set_style (MX_STYLABLE (box), style);
340
341 /* XXX rtl? */
342
343 /* image container */
344 frame = mx_frame_new ();
345 mx_stylable_set_style_class (MX_STYLABLE (frame), "TrackInfoImage");
346 mx_stylable_set_style (MX_STYLABLE (frame), style);
347 mx_box_layout_add_actor (MX_BOX_LAYOUT (box), frame, 0);
348 clutter_container_child_set (CLUTTER_CONTAINER (box), frame,
349 "expand", FALSE,
350 NULL);
351 set_blank_image (MX_FRAME (frame));
352 clutter_actor_show_all (CLUTTER_ACTOR (frame));
353
354 g_signal_connect_object (player, "playing-song-changed", G_CALLBACK (cover_art_entry_changed_cb), frame, 0);
355 request_cover_art (MX_FRAME (frame), entry);
356
357 box2 = mx_box_layout_new ();
358 mx_box_layout_set_orientation (MX_BOX_LAYOUT (box2), MX_ORIENTATION_VERTICAL);
359 mx_box_layout_set_spacing (MX_BOX_LAYOUT (box2), 16);
360 mx_stylable_set_style (MX_STYLABLE (box2), style);
361 mx_box_layout_add_actor (MX_BOX_LAYOUT (box), box2, 1);
362 clutter_container_child_set (CLUTTER_CONTAINER (box), box2,
363 "expand", TRUE,
364 "x-fill", TRUE,
365 "y-fill", TRUE,
366 "y-align", MX_ALIGN_MIDDLE,
367 NULL);
368
369 /* track info */
370 widget = mx_label_new ();
371 mx_stylable_set_style_class (MX_STYLABLE (widget), "TrackInfoText");
372 mx_stylable_set_style (MX_STYLABLE (widget), style);
373 mx_box_layout_add_actor (MX_BOX_LAYOUT (box2), widget, 1);
374 clutter_container_child_set (CLUTTER_CONTAINER (box2), widget,
375 "expand", FALSE,
376 "x-fill", TRUE,
377 "y-fill", TRUE,
378 "y-align", MX_ALIGN_MIDDLE,
379 NULL);
380
381 g_signal_connect_object (player, "playing-song-changed", G_CALLBACK (playing_song_changed_cb), widget, 0);
382 g_signal_connect_object (db, "entry-changed", G_CALLBACK (entry_changed_cb), widget, 0);
383 g_signal_connect_object (db, "entry-extra-metadata-notify::" RHYTHMDB_PROP_STREAM_SONG_TITLE, G_CALLBACK (streaming_title_notify_cb), widget, 0);
384
385 value = rhythmdb_entry_request_extra_metadata (db, entry, RHYTHMDB_PROP_STREAM_SONG_TITLE);
386 if (value != NULL) {
387 update_track_info (MX_LABEL (widget), db, entry, g_value_get_string (value));
388 g_value_unset (value);
389 g_free (value);
390 } else {
391 update_track_info (MX_LABEL (widget), db, entry, NULL);
392 }
393
394 /* elapsed/duration */
395 widget = mx_label_new ();
396 mx_stylable_set_style_class (MX_STYLABLE (widget), "TrackTimeText");
397 mx_stylable_set_style (MX_STYLABLE (widget), style);
398 mx_box_layout_add_actor (MX_BOX_LAYOUT (box2), widget, 2);
399 clutter_container_child_set (CLUTTER_CONTAINER (box2), widget,
400 "expand", FALSE,
401 "x-fill", TRUE,
402 "y-fill", TRUE,
403 "y-align", MX_ALIGN_MIDDLE,
404 NULL);
405
406 g_signal_connect_object (player, "elapsed-changed", G_CALLBACK (elapsed_changed_cb), widget, 0);
407 if (rb_shell_player_get_playing_time (player, &elapsed, NULL)) {
408 update_elapsed (widget, player, elapsed);
409 }
410
411 rhythmdb_entry_unref (entry);
412 g_object_unref (player);
413 g_object_unref (db);
414 return box;
415 }
416
417 static ClutterActor *
418 create_button (const char *button_style, const char *icon_style, const char *icon_name)
419 {
420 ClutterActor *widget;
421 ClutterActor *icon;
422
423 icon = mx_icon_new ();
424 mx_stylable_set_style_class (MX_STYLABLE (icon), icon_style);
425 mx_stylable_set_style (MX_STYLABLE (icon), style);
426 mx_icon_set_icon_name (MX_ICON (icon), icon_name);
427 mx_icon_set_icon_size (MX_ICON (icon), 64);
428
429 widget = mx_button_new ();
430 mx_stylable_set_style_class (MX_STYLABLE (widget), button_style);
431 mx_stylable_set_style (MX_STYLABLE (widget), style);
432 mx_bin_set_child (MX_BIN (widget), icon);
433
434 return widget;
435 }
436
437 static void
438 next_clicked_cb (MxButton *button, RBShellPlayer *player)
439 {
440 clutter_threads_leave ();
441 rb_shell_player_do_next (player, NULL);
442 clutter_threads_enter ();
443 }
444
445 static void
446 prev_clicked_cb (MxButton *button, RBShellPlayer *player)
447 {
448 clutter_threads_leave ();
449 rb_shell_player_do_previous (player, NULL);
450 clutter_threads_enter ();
451 }
452
453 static void
454 playpause_clicked_cb (MxButton *button, RBShellPlayer *player)
455 {
456 clutter_threads_leave ();
457 rb_shell_player_playpause (player, FALSE, NULL);
458 clutter_threads_enter ();
459 }
460
461 static void
462 update_playing (MxButton *button, gboolean playing)
463 {
464 ClutterActor *child;
465
466 child = mx_bin_get_child (MX_BIN (button));
467 if (playing) {
468 mx_stylable_set_style_class (MX_STYLABLE (button), "PauseButton");
469 mx_icon_set_icon_name (MX_ICON (child), "media-playback-pause");
470 } else {
471 mx_stylable_set_style_class (MX_STYLABLE (button), "PlayButton");
472 mx_icon_set_icon_name (MX_ICON (child), "media-playback-start");
473 }
474 /* stop button? meh */
475 }
476
477 static void
478 playing_changed_cb (RBShellPlayer *player, gboolean playing, MxButton *button)
479 {
480
481 clutter_threads_enter ();
482 update_playing (button, playing);
483 clutter_threads_leave ();
484 }
485
486 static ClutterActor *
487 create_controls (RBShell *shell)
488 {
489 RBShellPlayer *player;
490 ClutterActor *box;
491 ClutterActor *button;
492 int pos;
493 gboolean playing;
494
495 g_object_get (shell, "shell-player", &player, NULL);
496
497 box = mx_box_layout_new ();
498 mx_box_layout_set_orientation (MX_BOX_LAYOUT (box), MX_ORIENTATION_HORIZONTAL);
499 mx_box_layout_set_spacing (MX_BOX_LAYOUT (box), 16);
500 mx_stylable_set_style_class (MX_STYLABLE (box), "ControlsBox");
501 mx_stylable_set_style (MX_STYLABLE (box), style);
502 clutter_actor_set_reactive (box, TRUE);
503
504 /* XXX rtl? */
505 pos = 0;
506 button = create_button ("PrevButton", "PrevButtonIcon", "media-skip-backward");
507 g_signal_connect_object (button, "clicked", G_CALLBACK (prev_clicked_cb), player, 0);
508 mx_box_layout_add_actor (MX_BOX_LAYOUT (box), button, pos++);
509
510 button = create_button ("PlayPauseButton", "PlayPauseButtonIcon", "media-playback-start");
511 g_signal_connect_object (button, "clicked", G_CALLBACK (playpause_clicked_cb), player, 0);
512 g_signal_connect_object (player, "playing-changed", G_CALLBACK (playing_changed_cb), button, 0);
513 g_object_get (player, "playing", &playing, NULL);
514 update_playing (MX_BUTTON (button), playing);
515 mx_box_layout_add_actor (MX_BOX_LAYOUT (box), button, pos++);
516
517 button = create_button ("NextButton", "NextButtonIcon", "media-skip-forward");
518 g_signal_connect_object (button, "clicked", G_CALLBACK (next_clicked_cb), player, 0);
519 mx_box_layout_add_actor (MX_BOX_LAYOUT (box), button, pos++);
520
521 g_object_unref (player);
522 return box;
523 }
524
525 static gboolean
526 hide_controls_cb (ClutterActor *controls)
527 {
528 if (clutter_actor_has_pointer (controls) == FALSE) {
529 g_object_set_data (G_OBJECT (controls), "hide-controls-id", NULL);
530
531 clutter_actor_hide (controls);
532
533 clutter_stage_hide_cursor (CLUTTER_STAGE (clutter_actor_get_stage (controls)));
534 }
535 return FALSE;
536 }
537
538 static void
539 start_hide_timer (ClutterActor *controls)
540 {
541 guint hide_controls_id;
542
543 hide_controls_id = g_timeout_add_seconds (5, (GSourceFunc) hide_controls_cb, controls);
544 g_object_set_data (G_OBJECT (controls), "hide-controls-id", GUINT_TO_POINTER (hide_controls_id));
545 }
546
547 static void
548 stop_hide_timer (ClutterActor *controls)
549 {
550 guint hide_controls_id;
551
552 hide_controls_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (controls), "hide-controls-id"));
553 if (hide_controls_id != 0) {
554 g_source_remove (hide_controls_id);
555 }
556 }
557
558 static gboolean
559 stage_motion_event_cb (ClutterActor *stage, ClutterEvent *event, ClutterActor *controls)
560 {
561 if (g_object_get_data (G_OBJECT (controls), "cursor-in-controls") != NULL) {
562 rb_debug ("bleep");
563 return FALSE;
564 }
565
566 clutter_stage_show_cursor (CLUTTER_STAGE (stage));
567
568 clutter_actor_show (controls);
569
570 stop_hide_timer (controls);
571 start_hide_timer (controls);
572
573 return FALSE;
574 }
575
576 static gboolean
577 controls_enter_event_cb (ClutterActor *controls, ClutterEvent *event, gpointer data)
578 {
579 rb_debug ("bloop");
580 stop_hide_timer (controls);
581 g_object_set_data (G_OBJECT (controls), "cursor-in-controls", GINT_TO_POINTER (1));
582 return FALSE;
583 }
584
585 static gboolean
586 controls_leave_event_cb (ClutterActor *controls, ClutterEvent *event, gpointer data)
587 {
588 rb_debug ("blip");
589 start_hide_timer (controls);
590 g_object_set_data (G_OBJECT (controls), "cursor-in-controls", NULL);
591 return FALSE;
592 }
593
594 void
595 rb_visualizer_fullscreen_add_widgets (GtkWidget *window, ClutterActor *stage, RBShell *shell)
596 {
597 ClutterActor *track_info;
598 ClutterActor *controls;
599 GdkScreen *screen;
600 GdkRectangle geom;
601 int x;
602 int y;
603 int monitor;
604
605 clutter_threads_enter ();
606
607 /* get geometry for the monitor we're going to appear on */
608 screen = gtk_widget_get_screen (window);
609 monitor = gdk_screen_get_monitor_at_window (screen, gtk_widget_get_window (window));
610 gdk_screen_get_monitor_geometry (screen, monitor, &geom);
611
612 /* create and place the track info display */
613 track_info = create_track_info (shell);
614
615 clutter_container_add_actor (CLUTTER_CONTAINER (stage), track_info);
616 g_object_set_data (G_OBJECT (stage), TRACK_INFO_DATA, track_info);
617
618 /* XXX rtl? */
619 clutter_actor_set_position (track_info, FULLSCREEN_BORDER_WIDTH, FULLSCREEN_BORDER_WIDTH);
620
621 /* create and place the playback controls */
622 controls = create_controls (shell);
623 clutter_container_add_actor (CLUTTER_CONTAINER (stage), controls);
624 g_object_set_data (G_OBJECT (stage), CONTROLS_DATA, controls);
625
626 /* put this bit somewhere near the bottom */
627 /* XXX rtl */
628 x = FULLSCREEN_BORDER_WIDTH;
629 y = geom.height - (clutter_actor_get_height (controls) + FULLSCREEN_BORDER_WIDTH);
630 clutter_actor_set_position (controls, x, y);
631
632 /* hide mouse cursor when not moving, hide playback controls when mouse not moving
633 * and outside them
634 */
635 g_signal_connect_object (stage, "motion-event", G_CALLBACK (stage_motion_event_cb), controls, 0);
636 g_signal_connect (controls, "leave-event", G_CALLBACK (controls_leave_event_cb), NULL);
637 g_signal_connect (controls, "enter-event", G_CALLBACK (controls_enter_event_cb), NULL);
638 start_hide_timer (controls);
639
640 clutter_threads_leave ();
641 }
642
643 void
644 rb_visualizer_fullscreen_stop (ClutterActor *stage)
645 {
646 ClutterActor *controls;
647
648 controls = CLUTTER_ACTOR (g_object_get_data (G_OBJECT (stage), CONTROLS_DATA));
649 stop_hide_timer (controls);
650 }