hythmbox-2.98/plugins/visualizer/rb-visualizer-fullscreen.c

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 (&nothing);
 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 }