[rhythmbox] visualizer: new clutter-based visualizer plugin



commit 082eaf958b60e3830f1607114353d777c930d616
Author: Jonathan Matthew <jonathan d14n org>
Date:   Mon Jul 25 00:09:32 2011 +1000

    visualizer: new clutter-based visualizer plugin
    
    Better code, less horrible UI, and somewhat more interesting
    fullscreen mode than the old one.

 configure.ac                                  |   27 +
 data/org.gnome.rhythmbox.gschema.xml          |   18 +
 plugins/Makefile.am                           |    4 +
 plugins/visualizer/Makefile.am                |   80 +++
 plugins/visualizer/button-active.png          |  Bin 0 -> 569 bytes
 plugins/visualizer/button-disabled.png        |  Bin 0 -> 569 bytes
 plugins/visualizer/button-focus.png           |  Bin 0 -> 701 bytes
 plugins/visualizer/button-hover.png           |  Bin 0 -> 605 bytes
 plugins/visualizer/button.png                 |  Bin 0 -> 605 bytes
 plugins/visualizer/rb-visualizer-fullscreen.c |  666 +++++++++++++++++++++++++
 plugins/visualizer/rb-visualizer-fullscreen.h |   45 ++
 plugins/visualizer/rb-visualizer-menu.c       |  191 +++++++
 plugins/visualizer/rb-visualizer-menu.h       |   54 ++
 plugins/visualizer/rb-visualizer-page.c       |  429 ++++++++++++++++
 plugins/visualizer/rb-visualizer-page.h       |   82 +++
 plugins/visualizer/rb-visualizer-plugin.c     |  482 ++++++++++++++++++
 plugins/visualizer/visualizer-box.png         |  Bin 0 -> 330 bytes
 plugins/visualizer/visualizer-ui.xml          |   13 +
 plugins/visualizer/visualizer.css             |   56 ++
 plugins/visualizer/visualizer.plugin.in       |    8 +
 po/POTFILES.in                                |    5 +
 21 files changed, 2160 insertions(+), 0 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index de3b6ed..2287cf8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -734,6 +734,32 @@ AC_SUBST(DMAPSHARING_CFLAGS)
 AC_SUBST(DMAPSHARING_LIBS)
 
 dnl ================================================================
+dnl clutter for visualizer plugin
+dnl ================================================================
+AC_ARG_ENABLE(visualizer,
+              AC_HELP_STRING([--disable-visualizer],
+                             [Disable visualizer plugin support]),,
+              enable_visualizer=auto)
+if test "x$enable_visualizer" != "xno"; then
+	PKG_CHECK_MODULES(CLUTTER,
+			  clutter-1.0 >= 1.2			\
+			  clutter-x11-1.0 >= 1.2		\
+			  clutter-gst-1.0 >= 1.0		\
+			  clutter-gtk-1.0 >= 1.0		\
+			  mx-1.0 >= 1.0.1,
+			  have_clutter=yes,
+			  have_clutter=no)
+	if test "x$have_clutter" = "xno" -a "x$enable_visualizer" = "xyes"; then
+		AC_MSG_ERROR([Visualizer support explicitly requested, but clutter couldn't be found])
+	fi
+fi
+
+AM_CONDITIONAL(USE_CLUTTER, test x"$have_clutter" = "xyes")
+
+AC_SUBST(CLUTTER_CFLAGS)
+AC_SUBST(CLUTTER_LIBS)
+
+dnl ================================================================
 dnl Dependencies for Last.fm plugin
 dnl ================================================================
 AC_ARG_ENABLE(lastfm,
@@ -837,6 +863,7 @@ plugins/mpris/Makefile
 plugins/dbus-media-server/Makefile
 plugins/rbzeitgeist/Makefile
 plugins/notification/Makefile
+plugins/visualizer/Makefile
 bindings/Makefile
 bindings/vala/Makefile
 bindings/gi/Makefile
diff --git a/data/org.gnome.rhythmbox.gschema.xml b/data/org.gnome.rhythmbox.gschema.xml
index b080d8d..48d715e 100644
--- a/data/org.gnome.rhythmbox.gschema.xml
+++ b/data/org.gnome.rhythmbox.gschema.xml
@@ -410,4 +410,22 @@
       </description>
     </key>
   </schema>
+
+  <enum id="org.gnome.rhythmbox.plugins.visualizer.quality">
+    <value nick="low" value="0"/>
+    <value nick="medium" value="1"/>
+    <value nick="high" value="2"/>
+  </enum>
+  <schema id="org.gnome.rhythmbox.plugins.visualizer" path="/org/gnome/rhythmbox/plugins/visualizer/">
+    <key name="vis-plugin" type="s">
+      <default>'goom'</default>
+      <summary>GStreamer element to use for visual effects</summary>
+      <description>The name of the GStreamer element to use for visual effects.</description>
+    </key>
+    <key name="quality" enum="org.gnome.rhythmbox.plugins.visualizer.quality">
+      <default>'medium'</default>
+      <summary>The frame rate and size to use for visual effects</summary>
+      <description>The frame rate and size to use for visual effects</description>
+    </key>
+  </schema>
 </schemalist>
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 6d42f16..8aadf9b 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -58,6 +58,10 @@ if ENABLE_FM_RADIO
 SUBDIRS += fmradio
 endif
 
+if USE_CLUTTER
+SUBDIRS += visualizer
+endif
+
 if ENABLE_LASTFM
 SUBDIRS += audioscrobbler
 endif
diff --git a/plugins/visualizer/Makefile.am b/plugins/visualizer/Makefile.am
new file mode 100644
index 0000000..f2da2a6
--- /dev/null
+++ b/plugins/visualizer/Makefile.am
@@ -0,0 +1,80 @@
+bULL =
+
+plugindir = $(PLUGINDIR)/visualizer
+plugin_LTLIBRARIES = libvisualizer.la
+
+libvisualizer_la_SOURCES =			\
+	rb-visualizer-menu.c				\
+	rb-visualizer-menu.h				\
+	rb-visualizer-page.c				\
+	rb-visualizer-page.h				\
+	rb-visualizer-fullscreen.c			\
+	rb-visualizer-fullscreen.h			\
+	rb-visualizer-plugin.c				\
+	$(NULL)
+
+libvisualizer_la_LDFLAGS = $(PLUGIN_LIBTOOL_FLAGS)
+libvisualizer_la_LIBTOOLFLAGS = --tag=disable-static
+
+libvisualizer_la_LIBADD = 			\
+	$(top_builddir)/shell/librhythmbox-core.la	\
+	-lgstinterfaces-0.10				\
+	$(CLUTTER_LIBS)					\
+	$(RHYTHMBOX_LIBS)
+
+INCLUDES = 						\
+        -DGNOMELOCALEDIR=\""$(datadir)/locale"\"        \
+	-DG_LOG_DOMAIN=\"Rhythmbox\"		 	\
+	-I$(top_srcdir) 				\
+	-I$(top_builddir)				\
+	-DPIXMAP_DIR=\""$(datadir)/pixmaps"\"		\
+	-DSHARE_DIR=\"$(pkgdatadir)\"                   \
+	-DDATADIR=\""$(datadir)"\"			\
+	-DPLUGIN_SRC_DIR=\""$(ROOT_UNINSTALLED_DIR)/plugins/visualizer"\" \
+	$(RHYTHMBOX_CFLAGS)				\
+	$(CLUTTER_CFLAGS)					\
+	-D_XOPEN_SOURCE -D_BSD_SOURCE
+
+mxthemedir = $(plugindir)
+mxtheme_DATA = 						\
+	visualizer.css					\
+	button-active.png				\
+	button-disabled.png				\
+	button-focus.png				\
+	button-hover.png				\
+	button.png					\
+	visualizer-box.png
+
+
+#themedir = $(pkgdatadir)/icons/hicolor
+#size = 22x22
+#context = actions
+
+#icondir = $(themedir)/$(size)/$(context)
+#icon_DATA = icons/hicolor/$(size)/$(context)/visualization.png
+
+plugin_in_files = visualizer.plugin.in
+
+%.plugin: %.plugin.in $(INTLTOOL_MERGE) $(wildcard $(top_srcdir)/po/*po) ; $(INTLTOOL_MERGE) $(top_srcdir)/po $< $@ -d -u -c $(top_builddir)/po/.intltool-merge-cache
+
+BUILT_SOURCES =							\
+	$(plugin_in_files:.plugin.in=.plugin) 	\
+	$(NULL)
+
+plugin_DATA = 			\
+	$(BUILT_SOURCES)	\
+	$(NULL)
+
+EXTRA_DIST = 			\
+	$(icon_DATA)		\
+	$(mxtheme_DATA)		\
+	$(plugin_in_files)	\
+	$(NULL)
+
+CLEANFILES = 			\
+	$(BUILT_SOURCES)	\
+	$(NULL)
+
+DISTCLEANFILES =		\
+	$(BUILT_SOURCES)	\
+	$(NULL)
diff --git a/plugins/visualizer/button-active.png b/plugins/visualizer/button-active.png
new file mode 100644
index 0000000..575faf2
Binary files /dev/null and b/plugins/visualizer/button-active.png differ
diff --git a/plugins/visualizer/button-disabled.png b/plugins/visualizer/button-disabled.png
new file mode 100644
index 0000000..6f58eaa
Binary files /dev/null and b/plugins/visualizer/button-disabled.png differ
diff --git a/plugins/visualizer/button-focus.png b/plugins/visualizer/button-focus.png
new file mode 100644
index 0000000..eda1ab1
Binary files /dev/null and b/plugins/visualizer/button-focus.png differ
diff --git a/plugins/visualizer/button-hover.png b/plugins/visualizer/button-hover.png
new file mode 100644
index 0000000..3412587
Binary files /dev/null and b/plugins/visualizer/button-hover.png differ
diff --git a/plugins/visualizer/button.png b/plugins/visualizer/button.png
new file mode 100644
index 0000000..5bd2fc5
Binary files /dev/null and b/plugins/visualizer/button.png differ
diff --git a/plugins/visualizer/rb-visualizer-fullscreen.c b/plugins/visualizer/rb-visualizer-fullscreen.c
new file mode 100644
index 0000000..2973e5e
--- /dev/null
+++ b/plugins/visualizer/rb-visualizer-fullscreen.c
@@ -0,0 +1,666 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2010  Jonathan Matthew <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <clutter-gtk/clutter-gtk.h>
+#include <mx/mx.h>
+
+#include "rb-visualizer-fullscreen.h"
+
+#include <shell/rb-shell-player.h>
+#include <rhythmdb/rhythmdb.h>
+#include <lib/rb-file-helpers.h>
+#include <lib/rb-util.h>
+#include <lib/rb-debug.h>
+
+#define MAX_IMAGE_HEIGHT		128		/* should be style-controlled, but it's tricky */
+#define FULLSCREEN_BORDER_WIDTH		32		/* this should be style-controlled too */
+
+#define TRACK_INFO_DATA		"rb-track-info-actor"
+#define CONTROLS_DATA		"rb-controls-actor"
+
+static MxStyle *style = NULL;
+
+void
+rb_visualizer_fullscreen_load_style (GObject *plugin)
+{
+	char *file;
+
+	if (style == NULL) {
+		style = mx_style_new ();
+
+		file = rb_find_plugin_data_file (plugin, "visualizer.css");
+		if (file != NULL) {
+			mx_style_load_from_file (style, file, NULL);
+			g_free (file);
+		}
+	}
+}
+
+/* cover art display */
+
+static gboolean
+has_art_provider (RhythmDB *db)
+{
+	GQuark detail = g_quark_from_static_string (RHYTHMDB_PROP_COVER_ART);
+	GQuark uridetail = g_quark_from_static_string (RHYTHMDB_PROP_COVER_ART_URI);
+	guint id = g_signal_lookup ("entry-extra-metadata-request", RHYTHMDB_TYPE);
+	return g_signal_has_handler_pending (db, id, detail, TRUE) ||
+	       g_signal_has_handler_pending (db, id, uridetail, TRUE);
+}
+
+static void
+set_blank_image (MxFrame *frame)
+{
+	ClutterActor *blank;
+	ClutterColor nothing = { 0, 0, 0, 0 };
+
+	blank = clutter_rectangle_new_with_color (&nothing);
+	clutter_actor_set_height (blank, MAX_IMAGE_HEIGHT);
+	clutter_actor_set_width (blank, MAX_IMAGE_HEIGHT);
+	mx_bin_set_child (MX_BIN (frame), blank);
+}
+
+static void
+cover_art_notify_cb (RhythmDB *db, RhythmDBEntry *entry, const char *field, GValue *metadata, MxFrame *frame)
+{
+	clutter_threads_enter ();
+
+	if (entry != g_object_get_data (G_OBJECT (frame), "rb-playing-entry"))
+		return;
+
+	if (G_VALUE_HOLDS (metadata, GDK_TYPE_PIXBUF)) {
+		GdkPixbuf *pixbuf;
+
+		pixbuf = GDK_PIXBUF (g_value_get_object (metadata));
+		if (pixbuf != NULL) {
+			ClutterActor *image;
+
+			image = gtk_clutter_texture_new ();
+			gtk_clutter_texture_set_from_pixbuf (GTK_CLUTTER_TEXTURE (image), pixbuf, NULL);
+			if (clutter_actor_get_height (image) > MAX_IMAGE_HEIGHT) {
+				clutter_actor_set_height (image, MAX_IMAGE_HEIGHT);
+				clutter_texture_set_keep_aspect_ratio (CLUTTER_TEXTURE (image), TRUE);
+			}
+			if (clutter_actor_get_width (image) > MAX_IMAGE_HEIGHT) {
+				clutter_actor_set_width (image, MAX_IMAGE_HEIGHT);
+			}
+			mx_bin_set_child (MX_BIN (frame), image);
+			clutter_actor_show_all (CLUTTER_ACTOR (frame));
+		}
+	} else if (has_art_provider (db)) {
+		set_blank_image (frame);
+		clutter_actor_show_all (CLUTTER_ACTOR (frame));
+	} else {
+		mx_bin_set_child (MX_BIN (frame), NULL);
+		clutter_actor_hide_all (CLUTTER_ACTOR (frame));
+	}
+	clutter_threads_leave ();
+}
+
+static void
+cover_art_entry_changed_cb (RBShellPlayer *player, RhythmDBEntry *entry, MxFrame *frame)
+{
+	RhythmDB *db;
+	clutter_threads_enter ();
+	g_object_get (player, "db", &db, NULL);
+	if (has_art_provider (db)) {
+		set_blank_image (frame);
+		clutter_actor_show_all (CLUTTER_ACTOR (frame));
+	} else {
+		mx_bin_set_child (MX_BIN (frame), NULL);
+		clutter_actor_hide_all (CLUTTER_ACTOR (frame));
+	}
+	g_object_unref (db);
+	clutter_threads_leave ();
+
+	if (entry != NULL) {
+		g_object_set_data_full (G_OBJECT (frame),
+					"rb-playing-entry",
+					rhythmdb_entry_ref (entry),
+					(GDestroyNotify) rhythmdb_entry_unref);
+	} else {
+		g_object_set_data (G_OBJECT (frame), "rb-playing-entry", NULL);
+	}
+}
+
+/* track info display */
+
+static void
+get_artist_album_templates (const char *artist,
+			    const char *album,
+			    const char **artist_template,
+			    const char **album_template)
+{
+	PangoDirection tag_dir;
+	PangoDirection template_dir;
+
+	/* Translators: by Artist */
+	*artist_template = _("by <i>%s</i>");
+	/* Translators: from Album */
+	*album_template = _("from <i>%s</i>");
+
+	/* find the direction (left-to-right or right-to-left) of the
+	 * track's tags and the localized templates
+	 */
+	if (artist != NULL && artist[0] != '\0') {
+		tag_dir = pango_find_base_dir (artist, -1);
+		template_dir = pango_find_base_dir (*artist_template, -1);
+	} else if (album != NULL && album[0] != '\0') {
+		tag_dir = pango_find_base_dir (album, -1);
+		template_dir = pango_find_base_dir (*album_template, -1);
+	} else {
+		return;
+	}
+
+	/* if the track's tags and the localized templates have a different
+	 * direction, switch to direction-neutral templates in order to improve
+	 * display.
+	 * text can have a neutral direction, this condition only applies when
+	 * both directions are defined and they are conflicting.
+	 * https://bugzilla.gnome.org/show_bug.cgi?id=609767
+	 */
+	if (((tag_dir == PANGO_DIRECTION_LTR) && (template_dir == PANGO_DIRECTION_RTL)) ||
+	    ((tag_dir == PANGO_DIRECTION_RTL) && (template_dir == PANGO_DIRECTION_LTR))) {
+		/* these strings should not be localized, they must be
+		 * locale-neutral and direction-neutral
+		 */
+		*artist_template = "<i>%s</i>";
+		*album_template = "/ <i>%s</i>";
+	}
+}
+
+static void
+str_append_printf_escaped (GString *str, const char *format, ...)
+{
+	va_list args;
+	char *bit;
+
+	va_start (args, format);
+	bit = g_markup_vprintf_escaped (format, args);
+	va_end (args);
+
+	g_string_append (str, bit);
+	g_free (bit);
+}
+
+static void
+update_track_info (MxLabel *label, RhythmDB *db, RhythmDBEntry *entry, const char *streaming_title)
+{
+	const char *title;
+	ClutterActor *text;
+	GString *str;
+
+	clutter_threads_enter ();
+	text = mx_label_get_clutter_text (label);
+
+	str = g_string_sized_new (100);
+	if (entry == NULL) {
+		g_string_append_printf (str, "<big>%s</big>", _("Not Playing"));
+	} else {
+		title = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE);
+
+		if (streaming_title) {
+			str_append_printf_escaped (str, "<big>%s</big>\n", streaming_title);
+			str_append_printf_escaped (str, _("from <i>%s</i>"), title);
+		} else {
+			const char *artist_template = NULL;
+			const char *album_template = NULL;
+			const char *artist;
+			const char *album;
+			gboolean space = FALSE;
+
+			artist = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ARTIST);
+			album = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM);
+			get_artist_album_templates (artist, album, &artist_template, &album_template);
+
+			str_append_printf_escaped (str, "<big>%s</big>\n", title);
+
+			if (album != NULL && album[0] != '\0') {
+				str_append_printf_escaped (str, album_template, album);
+				space = TRUE;
+			}
+			if (artist != NULL && artist[0] != '\0') {
+				if (space) {
+					g_string_append_c (str, ' ');
+				}
+				str_append_printf_escaped (str, artist_template, artist);
+			}
+		}
+	}
+
+	/* tiny bit of extra padding */
+	g_string_append (str, "  ");
+	clutter_text_set_markup (CLUTTER_TEXT (text), str->str);
+	clutter_text_set_ellipsize (CLUTTER_TEXT (text), PANGO_ELLIPSIZE_NONE);
+	clutter_threads_leave ();
+	g_string_free (str, TRUE);
+}
+
+static void
+playing_song_changed_cb (RBShellPlayer *player, RhythmDBEntry *entry, ClutterActor *label)
+{
+	RhythmDB *db;
+
+	g_object_get (player, "db", &db, NULL);
+	update_track_info (MX_LABEL (label), db, entry, NULL);
+	g_object_unref (db);
+}
+
+static void
+entry_changed_cb (RhythmDB *db, RhythmDBEntry *entry, GValueArray *changes, ClutterActor *label)
+{
+	int i;
+	/* somehow check entry == playing entry */
+
+	for (i = 0; i < changes->n_values; i++) {
+		GValue *v = g_value_array_get_nth (changes, i);
+		RhythmDBEntryChange *change = g_value_get_boxed (v);
+		switch (change->prop) {
+		case RHYTHMDB_PROP_TITLE:
+		case RHYTHMDB_PROP_ARTIST:
+		case RHYTHMDB_PROP_ALBUM:
+			update_track_info (MX_LABEL (label), db, entry, NULL);
+			return;
+
+		default:
+			break;
+		}
+	}
+}
+
+static void
+streaming_title_notify_cb (RhythmDB *db, RhythmDBEntry *entry, const char *field, GValue *metadata, ClutterActor *label)
+{
+	if (G_VALUE_HOLDS_STRING (metadata)) {
+		update_track_info (MX_LABEL (label), db, entry, g_value_get_string (metadata));
+	}
+}
+
+
+/* elapsed time / duration display */
+
+static void
+elapsed_changed_cb (RBShellPlayer *player, guint elapsed, ClutterActor *label)
+{
+	long duration;
+	char *str;
+
+	duration = rb_shell_player_get_playing_song_duration (player);
+	str = rb_make_elapsed_time_string (elapsed, duration, FALSE);
+	clutter_threads_enter ();
+
+	mx_label_set_text (MX_LABEL (label), str);
+
+	clutter_threads_leave ();
+
+	g_free (str);
+}
+
+
+static ClutterActor *
+create_track_info (RBShell *shell)
+{
+	RBShellPlayer *player;
+	RhythmDB *db;
+	ClutterActor *box;
+	ClutterActor *box2;
+	ClutterActor *widget;
+	ClutterActor *frame;
+	RhythmDBEntry *entry;
+	GValue *value;
+	guint elapsed;
+
+	g_object_get (shell, "shell-player", &player, "db", &db, NULL);
+	entry = rb_shell_player_get_playing_entry (player);
+
+	box = mx_box_layout_new ();
+	mx_box_layout_set_orientation (MX_BOX_LAYOUT (box), MX_ORIENTATION_HORIZONTAL);
+	mx_box_layout_set_spacing (MX_BOX_LAYOUT (box), 16);
+	mx_stylable_set_style_class (MX_STYLABLE (box), "TrackInfoBox");
+	mx_stylable_set_style (MX_STYLABLE (box), style);
+
+	/* XXX rtl? */
+
+	/* image container */
+	frame = mx_frame_new ();
+	mx_stylable_set_style_class (MX_STYLABLE (frame), "TrackInfoImage");
+	mx_stylable_set_style (MX_STYLABLE (frame), style);
+	mx_box_layout_add_actor (MX_BOX_LAYOUT (box), frame, 0);
+	clutter_container_child_set (CLUTTER_CONTAINER (box), frame,
+				     "expand", FALSE,
+				     NULL);
+
+	g_signal_connect_object (db, "entry-extra-metadata-notify::" RHYTHMDB_PROP_COVER_ART, G_CALLBACK (cover_art_notify_cb), frame, 0);
+	g_signal_connect_object (player, "playing-song-changed", G_CALLBACK (cover_art_entry_changed_cb), frame, 0);
+
+	/* request current image */
+	value = rhythmdb_entry_request_extra_metadata (db, entry, RHYTHMDB_PROP_COVER_ART);
+	cover_art_notify_cb (db, entry, RHYTHMDB_PROP_COVER_ART, value, MX_FRAME (frame));
+	if (value != NULL) {
+		g_value_unset (value);
+		g_free (value);
+	}
+
+	box2 = mx_box_layout_new ();
+	mx_box_layout_set_orientation (MX_BOX_LAYOUT (box2), MX_ORIENTATION_VERTICAL);
+	mx_box_layout_set_spacing (MX_BOX_LAYOUT (box2), 16);
+	mx_stylable_set_style (MX_STYLABLE (box2), style);
+	mx_box_layout_add_actor (MX_BOX_LAYOUT (box), box2, 1);
+	clutter_container_child_set (CLUTTER_CONTAINER (box), box2,
+				     "expand", TRUE,
+				     "x-fill", TRUE,
+				     "y-fill", TRUE,
+				     "y-align", MX_ALIGN_MIDDLE,
+				     NULL);
+
+	/* track info */
+	widget = mx_label_new ();
+	mx_stylable_set_style_class (MX_STYLABLE (widget), "TrackInfoText");
+	mx_stylable_set_style (MX_STYLABLE (widget), style);
+	mx_box_layout_add_actor (MX_BOX_LAYOUT (box2), widget, 1);
+	clutter_container_child_set (CLUTTER_CONTAINER (box2), widget,
+				     "expand", FALSE,
+				     "x-fill", TRUE,
+				     "y-fill", TRUE,
+				     "y-align", MX_ALIGN_MIDDLE,
+				     NULL);
+
+	g_signal_connect_object (player, "playing-song-changed", G_CALLBACK (playing_song_changed_cb), widget, 0);
+	g_signal_connect_object (db, "entry-changed", G_CALLBACK (entry_changed_cb), widget, 0);
+	g_signal_connect_object (db, "entry-extra-metadata-notify::" RHYTHMDB_PROP_STREAM_SONG_TITLE, G_CALLBACK (streaming_title_notify_cb), widget, 0);
+
+	value = rhythmdb_entry_request_extra_metadata (db, entry, RHYTHMDB_PROP_STREAM_SONG_TITLE);
+	if (value != NULL) {
+		update_track_info (MX_LABEL (widget), db, entry, g_value_get_string (value));
+		g_value_unset (value);
+		g_free (value);
+	} else {
+		update_track_info (MX_LABEL (widget), db, entry, NULL);
+	}
+
+	/* elapsed/duration */
+	widget = mx_label_new ();
+	mx_stylable_set_style_class (MX_STYLABLE (widget), "TrackTimeText");
+	mx_stylable_set_style (MX_STYLABLE (widget), style);
+	mx_box_layout_add_actor (MX_BOX_LAYOUT (box2), widget, 2);
+	clutter_container_child_set (CLUTTER_CONTAINER (box2), widget,
+				     "expand", FALSE,
+				     "x-fill", TRUE,
+				     "y-fill", TRUE,
+				     "y-align", MX_ALIGN_MIDDLE,
+				     NULL);
+
+	g_signal_connect_object (player, "elapsed-changed", G_CALLBACK (elapsed_changed_cb), widget, 0);
+	if (rb_shell_player_get_playing_time (player, &elapsed, NULL)) {
+		elapsed_changed_cb (player, elapsed, widget);
+	}
+
+	rhythmdb_entry_unref (entry);
+	g_object_unref (player);
+	g_object_unref (db);
+	return box;
+}
+
+static ClutterActor *
+create_button (const char *button_style, const char *icon_style, const char *icon_name)
+{
+	ClutterActor *widget;
+	ClutterActor *icon;
+
+	icon = mx_icon_new ();
+	mx_stylable_set_style_class (MX_STYLABLE (icon), icon_style);
+	mx_stylable_set_style (MX_STYLABLE (icon), style);
+	mx_icon_set_icon_name (MX_ICON (icon), icon_name);
+	mx_icon_set_icon_size (MX_ICON (icon), 64);
+
+	widget = mx_button_new ();
+	mx_stylable_set_style_class (MX_STYLABLE (widget), button_style);
+	mx_stylable_set_style (MX_STYLABLE (widget), style);
+	mx_bin_set_child (MX_BIN (widget), icon);
+
+	return widget;
+}
+
+static void
+next_clicked_cb (MxButton *button, RBShellPlayer *player)
+{
+	rb_shell_player_do_next (player, NULL);
+}
+
+static void
+prev_clicked_cb (MxButton *button, RBShellPlayer *player)
+{
+	rb_shell_player_do_previous (player, NULL);
+}
+
+static void
+playpause_clicked_cb (MxButton *button, RBShellPlayer *player)
+{
+	rb_shell_player_playpause (player, FALSE, NULL);
+}
+
+static void
+playing_changed_cb (RBShellPlayer *player, gboolean playing, MxButton *button)
+{
+	ClutterActor *child;
+
+	clutter_threads_enter ();
+	child = mx_bin_get_child (MX_BIN (button));
+	if (playing) {
+		mx_stylable_set_style_class (MX_STYLABLE (button), "PauseButton");
+		mx_icon_set_icon_name (MX_ICON (child), "media-playback-pause");
+	} else {
+		mx_stylable_set_style_class (MX_STYLABLE (button), "PlayButton");
+		mx_icon_set_icon_name (MX_ICON (child), "media-playback-start");
+	}
+	clutter_threads_leave ();
+
+	/* stop button?  meh */
+}
+
+static ClutterActor *
+create_controls (RBShell *shell)
+{
+	RBShellPlayer *player;
+	ClutterActor *box;
+	ClutterActor *button;
+	int pos;
+	gboolean playing;
+
+	g_object_get (shell, "shell-player", &player, NULL);
+
+	box = mx_box_layout_new ();
+	mx_box_layout_set_orientation (MX_BOX_LAYOUT (box), MX_ORIENTATION_HORIZONTAL);
+	mx_box_layout_set_spacing (MX_BOX_LAYOUT (box), 16);
+	mx_stylable_set_style_class (MX_STYLABLE (box), "ControlsBox");
+	mx_stylable_set_style (MX_STYLABLE (box), style);
+	clutter_actor_set_reactive (box, TRUE);
+
+	/* XXX rtl? */
+	pos = 0;
+	button = create_button ("PrevButton", "PrevButtonIcon", "media-skip-backward");
+	g_signal_connect_object (button, "clicked", G_CALLBACK (prev_clicked_cb), player, 0);
+	mx_box_layout_add_actor (MX_BOX_LAYOUT (box), button, pos++);
+
+	button = create_button ("PlayPauseButton", "PlayPauseButtonIcon", "media-playback-start");
+	g_signal_connect_object (button, "clicked", G_CALLBACK (playpause_clicked_cb), player, 0);
+	g_signal_connect_object (player, "playing-changed", G_CALLBACK (playing_changed_cb), button, 0);
+	g_object_get (player, "playing", &playing, NULL);
+	playing_changed_cb (player, playing, MX_BUTTON (button));
+	mx_box_layout_add_actor (MX_BOX_LAYOUT (box), button, pos++);
+
+	button = create_button ("NextButton", "NextButtonIcon", "media-skip-forward");
+	g_signal_connect_object (button, "clicked", G_CALLBACK (next_clicked_cb), player, 0);
+	mx_box_layout_add_actor (MX_BOX_LAYOUT (box), button, pos++);
+
+	g_object_unref (player);
+	return box;
+}
+
+static gboolean
+hide_controls_cb (ClutterActor *controls)
+{
+	rb_debug ("controls pseudo class: %s", mx_stylable_get_style_pseudo_class (MX_STYLABLE (controls)));
+	if (clutter_actor_has_pointer (controls) == FALSE) {
+		g_object_set_data (G_OBJECT (controls), "hide-controls-id", NULL);
+
+		clutter_actor_hide (controls);
+
+		clutter_stage_hide_cursor (CLUTTER_STAGE (clutter_actor_get_stage (controls)));
+	}
+	return FALSE;
+}
+
+static void
+start_hide_timer (ClutterActor *controls)
+{
+	guint hide_controls_id;
+
+	hide_controls_id = g_timeout_add_seconds (5, (GSourceFunc) hide_controls_cb, controls);
+	g_object_set_data (G_OBJECT (controls), "hide-controls-id", GUINT_TO_POINTER (hide_controls_id));
+}
+
+static void
+stop_hide_timer (ClutterActor *controls)
+{
+	guint hide_controls_id;
+
+	hide_controls_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (controls), "hide-controls-id"));
+	if (hide_controls_id != 0) {
+		g_source_remove (hide_controls_id);
+	}
+}
+
+static gboolean
+stage_motion_event_cb (ClutterActor *stage, ClutterEvent *event, ClutterActor *controls)
+{
+	if (g_object_get_data (G_OBJECT (controls), "cursor-in-controls") != NULL) {
+		rb_debug ("bleep");
+		return FALSE;
+	}
+
+	clutter_stage_show_cursor (CLUTTER_STAGE (stage));
+
+	clutter_actor_show (controls);
+
+	stop_hide_timer (controls);
+	start_hide_timer (controls);
+
+	return FALSE;
+}
+
+static gboolean
+controls_enter_event_cb (ClutterActor *controls, ClutterEvent *event, gpointer data)
+{
+	rb_debug ("bloop");
+	stop_hide_timer (controls);
+	g_object_set_data (G_OBJECT (controls), "cursor-in-controls", GINT_TO_POINTER (1));
+	return FALSE;
+}
+
+static gboolean
+controls_leave_event_cb (ClutterActor *controls, ClutterEvent *event, gpointer data)
+{
+	rb_debug ("blip");
+	start_hide_timer (controls);
+	g_object_set_data (G_OBJECT (controls), "cursor-in-controls", NULL);
+	return FALSE;
+}
+
+void
+rb_visualizer_fullscreen_add_widgets (GtkWidget *window, ClutterActor *stage, RBShell *shell)
+{
+	ClutterActor *track_info;
+	ClutterActor *controls;
+	GdkScreen *screen;
+	GdkRectangle geom;
+	int x;
+	int y;
+	int monitor;
+
+	clutter_threads_enter ();
+
+	/* get geometry for the monitor we're going to appear on */
+	screen = gtk_widget_get_screen (window);
+	monitor = gdk_screen_get_monitor_at_window (screen, gtk_widget_get_window (window));
+	gdk_screen_get_monitor_geometry (screen, monitor, &geom);
+
+	/* create and place the track info display */
+	track_info = create_track_info (shell);
+
+	clutter_container_add_actor (CLUTTER_CONTAINER (stage), track_info);
+	g_object_set_data (G_OBJECT (stage), TRACK_INFO_DATA, track_info);
+
+	/* XXX rtl? */
+	clutter_actor_set_position (track_info, FULLSCREEN_BORDER_WIDTH, FULLSCREEN_BORDER_WIDTH);
+
+	/* create and place the playback controls */
+	controls = create_controls (shell);
+	clutter_container_add_actor (CLUTTER_CONTAINER (stage), controls);
+	g_object_set_data (G_OBJECT (stage), CONTROLS_DATA, controls);
+
+	/* put this bit somewhere near the bottom */
+	/* XXX rtl */
+	x = FULLSCREEN_BORDER_WIDTH;
+	y = geom.height - (clutter_actor_get_height (controls) + FULLSCREEN_BORDER_WIDTH);
+	clutter_actor_set_position (controls, x, y);
+
+	/* hide mouse cursor when not moving, hide playback controls when mouse not moving
+	 * and outside them
+	 */
+	g_signal_connect_object (stage, "motion-event", G_CALLBACK (stage_motion_event_cb), controls, 0);
+	g_signal_connect (controls, "leave-event", G_CALLBACK (controls_leave_event_cb), NULL);
+	g_signal_connect (controls, "enter-event", G_CALLBACK (controls_enter_event_cb), NULL);
+	start_hide_timer (controls);
+
+	clutter_threads_leave ();
+}
+
+void
+rb_visualizer_fullscreen_remove_widgets (ClutterActor *stage)
+{
+	ClutterActor *track_info;
+	ClutterActor *controls;
+
+	clutter_threads_enter ();
+
+	track_info = CLUTTER_ACTOR (g_object_steal_data (G_OBJECT (stage), TRACK_INFO_DATA));
+	if (track_info != NULL) {
+		clutter_container_remove_actor (CLUTTER_CONTAINER (stage), track_info);
+	}
+
+	controls = CLUTTER_ACTOR (g_object_steal_data (G_OBJECT (stage), CONTROLS_DATA));
+	if (controls != NULL) {
+		stop_hide_timer (controls);
+		clutter_container_remove_actor (CLUTTER_CONTAINER (stage), controls);
+	}
+
+	clutter_threads_leave ();
+}
diff --git a/plugins/visualizer/rb-visualizer-fullscreen.h b/plugins/visualizer/rb-visualizer-fullscreen.h
new file mode 100644
index 0000000..9c520f0
--- /dev/null
+++ b/plugins/visualizer/rb-visualizer-fullscreen.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2010  Jonathan Matthew <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#ifndef RB_VISUALIZER_FULLSCREEN_H
+#define RB_VISUALIZER_FULLSCREEN_H
+
+#include <shell/rb-shell.h>
+
+#include <clutter/clutter.h>
+
+G_BEGIN_DECLS
+
+void		rb_visualizer_fullscreen_load_style (GObject *plugin);
+
+void		rb_visualizer_fullscreen_add_widgets (GtkWidget *window, ClutterActor *stage, RBShell *shell);
+void		rb_visualizer_fullscreen_remove_widgets (ClutterActor *stage);
+
+G_END_DECLS
+
+#endif /* RB_VISUALIZER_FULLSCREEN_H */
diff --git a/plugins/visualizer/rb-visualizer-menu.c b/plugins/visualizer/rb-visualizer-menu.c
new file mode 100644
index 0000000..760959e
--- /dev/null
+++ b/plugins/visualizer/rb-visualizer-menu.c
@@ -0,0 +1,191 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2010  Jonathan Matthew <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ */
+
+#include "config.h"
+
+#include <gtk/gtk.h>
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+#include <gst/gst.h>
+
+#include "rb-visualizer-menu.h"
+#include <lib/rb-debug.h>
+
+const VisualizerQuality rb_visualizer_quality[] = {
+	{ N_("Low quality"),	"low",	320,	240,	20, 	1 },
+	{ N_("Normal quality"),	"medium", 640,	480,	25,	1 },
+	{ N_("High quality"),	"high",	800,	600,	30,	1 }
+};
+
+static void
+set_check_item_foreach (GtkWidget *widget, GtkCheckMenuItem *item)
+{
+	GtkCheckMenuItem *check = GTK_CHECK_MENU_ITEM (widget);
+	gtk_check_menu_item_set_active (check, check == item);
+}
+
+static void
+quality_item_toggled_cb (GtkMenuItem *item, gpointer data)
+{
+	int index = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (item), "quality"));
+	GSettings *settings = g_object_get_data (G_OBJECT (item), "settings");
+
+	if (gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (item)) == FALSE) {
+		return;
+	}
+
+	rb_debug ("vis quality %d (%s) activated", index, rb_visualizer_quality[index].setting);
+	g_settings_set_string (settings, "quality", rb_visualizer_quality[index].setting);
+
+	g_signal_handlers_block_by_func (item, quality_item_toggled_cb, data);
+	gtk_container_foreach (GTK_CONTAINER (data),
+			       (GtkCallback) set_check_item_foreach,
+			       GTK_CHECK_MENU_ITEM (item));
+	g_signal_handlers_unblock_by_func (item, quality_item_toggled_cb, data);
+}
+
+static void
+vis_plugin_item_activate_cb (GtkMenuItem *item, gpointer data)
+{
+	const char *name = g_object_get_data (G_OBJECT (item), "element-name");
+	GSettings *settings = g_object_get_data (G_OBJECT (item), "settings");
+
+	if (gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (item)) == FALSE) {
+		return;
+	}
+
+	rb_debug ("vis element %s activated", name);
+	g_settings_set_string (settings, "vis-plugin", name);
+
+	g_signal_handlers_block_by_func (item, vis_plugin_item_activate_cb, data);
+	gtk_container_foreach (GTK_CONTAINER (data),
+			       (GtkCallback) set_check_item_foreach,
+			       GTK_CHECK_MENU_ITEM (item));
+	g_signal_handlers_unblock_by_func (item, vis_plugin_item_activate_cb, data);
+}
+
+static gboolean
+vis_plugin_filter (GstPluginFeature *feature, gpointer data)
+{
+	GstElementFactory *f;
+
+	if  (!GST_IS_ELEMENT_FACTORY (feature))
+		return FALSE;
+	f = GST_ELEMENT_FACTORY (feature);
+
+	return (g_strrstr (gst_element_factory_get_klass (f), "Visualization") != NULL);
+}
+
+GtkWidget *
+rb_visualizer_create_popup_menu (GtkToggleAction *fullscreen_action)
+{
+	GSettings *settings;
+	GtkWidget *menu;
+	GtkWidget *submenu;
+	GtkWidget *item;
+	GList *features;
+	GList *t;
+	char *active_element;
+	int quality;
+	int i;
+
+	menu = gtk_menu_new ();
+
+	settings = g_settings_new ("org.gnome.rhythmbox.plugins.visualizer");
+
+	/* fullscreen item */
+	item = gtk_action_create_menu_item (GTK_ACTION (fullscreen_action));
+	gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
+
+	/* quality submenu */
+	quality = g_settings_get_enum (settings, "quality");
+	submenu = gtk_menu_new ();
+	for (i = 0; i < G_N_ELEMENTS (rb_visualizer_quality); i++) {
+		item = gtk_check_menu_item_new_with_label (rb_visualizer_quality[i].name);
+
+		gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), (i == quality));
+
+		g_object_set_data (G_OBJECT (item), "quality", GINT_TO_POINTER (i));
+		g_object_set_data (G_OBJECT (item), "settings", settings);
+		g_signal_connect (item, "toggled", G_CALLBACK (quality_item_toggled_cb), submenu);
+		gtk_menu_shell_append (GTK_MENU_SHELL (submenu), item);
+	}
+
+	item = gtk_menu_item_new_with_mnemonic (_("_Quality"));
+	gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu);
+	gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
+
+	/* effect submenu */
+	submenu = gtk_menu_new ();
+
+	rb_debug ("building vis plugin list");
+	active_element = g_settings_get_string (settings, "vis-plugin");
+	features = gst_registry_feature_filter (gst_registry_get_default (),
+						vis_plugin_filter,
+						FALSE, NULL);
+	for (t = features; t != NULL; t = t->next) {
+		GstPluginFeature *f;
+		const char *name;
+		const char *element_name;
+
+		f = GST_PLUGIN_FEATURE (t->data);
+		name = gst_element_factory_get_longname (GST_ELEMENT_FACTORY (f));
+		element_name = gst_plugin_feature_get_name (f);
+		rb_debug ("adding visualizer element %s (%s)", element_name, name);
+
+		item = gtk_check_menu_item_new_with_label (name);
+		gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item),
+						g_strcmp0 (element_name, active_element) == 0);
+		g_object_set_data (G_OBJECT (item), "element-name", g_strdup (element_name));
+		g_object_set_data (G_OBJECT (item), "settings", settings);
+		gtk_menu_shell_append (GTK_MENU_SHELL (submenu), item);
+		g_signal_connect (item,
+				  "activate",
+				  G_CALLBACK (vis_plugin_item_activate_cb),
+				  submenu);
+	}
+	gst_plugin_feature_list_free (features);
+
+	item = gtk_menu_item_new_with_mnemonic (_("_Visual Effect"));
+	gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu);
+	gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
+
+	gtk_widget_show_all (menu);
+	return menu;
+}
+
+int
+rb_visualizer_menu_clip_quality (int value)
+{
+	if (value < 0) {
+		return 0;
+	} else if (value >= G_N_ELEMENTS (rb_visualizer_quality)) {
+		return G_N_ELEMENTS (rb_visualizer_quality) - 1;
+	} else {
+		return value;
+	}
+}
diff --git a/plugins/visualizer/rb-visualizer-menu.h b/plugins/visualizer/rb-visualizer-menu.h
new file mode 100644
index 0000000..d49cfae
--- /dev/null
+++ b/plugins/visualizer/rb-visualizer-menu.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2010  Jonathan Matthew <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#ifndef RB_VISUALIZER_MENU_H
+#define RB_VISUALIZER_MENU_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+/* quality settings */
+typedef struct {
+	const char *name;
+	const char *setting;
+	int width;
+	int height;
+	gint fps_n;
+	gint fps_d;
+} VisualizerQuality;
+
+extern const VisualizerQuality rb_visualizer_quality[];
+
+int	rb_visualizer_menu_clip_quality 	(int value);
+
+GtkWidget *rb_visualizer_create_popup_menu 	(GtkToggleAction *fullscreen_action);
+
+G_END_DECLS
+
+#endif /* RB_VISUALIZER_MENU_H */
diff --git a/plugins/visualizer/rb-visualizer-page.c b/plugins/visualizer/rb-visualizer-page.c
new file mode 100644
index 0000000..bc991d9
--- /dev/null
+++ b/plugins/visualizer/rb-visualizer-page.c
@@ -0,0 +1,429 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2010  Jonathan Matthew <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <glib.h>
+#include <glib/gi18n.h>
+
+#include "rb-visualizer-page.h"
+#include "rb-visualizer-fullscreen.h"
+
+#include <widgets/rb-dialog.h>
+#include <lib/rb-util.h>
+#include <lib/rb-debug.h>
+
+
+G_DEFINE_DYNAMIC_TYPE (RBVisualizerPage, rb_visualizer_page, RB_TYPE_DISPLAY_PAGE)
+
+enum {
+	PROP_0,
+	PROP_SINK,
+	PROP_FULLSCREEN_ACTION,
+	PROP_POPUP
+};
+
+enum {
+	START,
+	STOP,
+	FULLSCREEN,
+	LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = {0,};
+
+RBVisualizerPage *
+rb_visualizer_page_new (GObject *plugin, RBShell *shell, GtkToggleAction *fullscreen, GtkWidget *popup)
+{
+	GObject *page;
+	GdkPixbuf *pixbuf;
+	gint size;
+
+	gtk_icon_size_lookup (RB_SOURCE_ICON_SIZE, &size, NULL);
+	pixbuf = gtk_icon_theme_load_icon (gtk_icon_theme_get_default (),
+					   "visualization",
+					   size,
+					   0, NULL);
+
+	page = g_object_new (RB_TYPE_VISUALIZER_PAGE,
+			     "plugin", plugin,
+			     "shell", shell,
+			     "name", _("Visual Effects"),
+			     "pixbuf", pixbuf,
+			     "fullscreen-action", fullscreen,
+			     "popup", popup,
+			     NULL);
+	if (pixbuf != NULL) {
+		g_object_unref (pixbuf);
+	}
+
+	return RB_VISUALIZER_PAGE (page);
+}
+
+static void
+set_action_state (RBVisualizerPage *page, gboolean active)
+{
+	page->setting_state = TRUE;
+	g_object_set (page->fullscreen_action, "active", active, NULL);
+	page->setting_state = FALSE;
+}
+
+static void
+start_fullscreen (RBVisualizerPage *page)
+{
+	if (page->fullscreen == NULL) {
+		ClutterActor *stage;
+		GtkWindow *main_window;
+		RBShell *shell;
+		int x, y;
+
+		rb_debug ("starting fullscreen display");
+		g_object_get (page, "shell", &shell, NULL);
+		g_object_get (shell, "window", &main_window, NULL);
+
+		page->fullscreen = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+		gtk_window_set_skip_taskbar_hint (GTK_WINDOW (page->fullscreen), TRUE);
+
+		/* maybe need to block the sink? */
+
+		gtk_widget_reparent (page->embed, page->fullscreen);
+		gtk_widget_show_all (GTK_WIDGET (page->fullscreen));
+
+		gtk_window_get_position (main_window, &x, &y);
+		gtk_window_move (GTK_WINDOW (page->fullscreen), x, y);
+
+		gtk_window_fullscreen (GTK_WINDOW (page->fullscreen));
+		gtk_window_set_transient_for (GTK_WINDOW (page->fullscreen), main_window);
+		g_object_unref (main_window);
+
+		stage = gtk_clutter_embed_get_stage (GTK_CLUTTER_EMBED (page->embed));
+		rb_visualizer_fullscreen_add_widgets (page->fullscreen, stage, shell);
+		g_object_unref (shell);
+	}
+
+	set_action_state (page, TRUE);
+}
+
+static void
+stop_fullscreen (RBVisualizerPage *page)
+{
+	if (page->fullscreen != NULL) {
+		ClutterActor *stage;
+
+		rb_debug ("stopping fullscreen display");
+		gtk_widget_reparent (page->embed, GTK_WIDGET (page));
+		gtk_widget_destroy (GTK_WIDGET (page->fullscreen));
+		page->fullscreen = NULL;
+
+		stage = gtk_clutter_embed_get_stage (GTK_CLUTTER_EMBED (page->embed));
+		rb_visualizer_fullscreen_remove_widgets (stage);
+	}
+
+	set_action_state (page, FALSE);
+}
+
+static void
+toggle_fullscreen (RBVisualizerPage *page)
+{
+	if (page->fullscreen != NULL) {
+		stop_fullscreen (page);
+	} else {
+		start_fullscreen (page);
+	}
+}
+
+static void
+toggle_fullscreen_cb (GtkAction *action, RBVisualizerPage *page)
+{
+	if (page->setting_state == FALSE) {
+		toggle_fullscreen (page);
+	}
+}
+
+static gboolean
+stage_button_press_cb (ClutterActor *stage, ClutterEvent *event, RBVisualizerPage *page)
+{
+	if (event->button.button == 1 && event->button.click_count == 2) {
+		toggle_fullscreen (page);
+	} else if (event->button.button == 3) {
+		rb_display_page_show_popup (RB_DISPLAY_PAGE (page));
+	}
+
+	return FALSE;
+}
+
+static gboolean
+stage_key_release_cb (ClutterActor *stage, ClutterEvent *event, RBVisualizerPage *page)
+{
+	if (event->key.keyval == CLUTTER_KEY_Escape) {
+		stop_fullscreen (page);
+	}
+	return FALSE;
+}
+
+static void
+resize_sink_texture (ClutterActor *stage, ClutterActorBox *box, ClutterAllocationFlags flags, ClutterActor *texture)
+{
+	clutter_actor_set_size (texture, box->x2 - box->x1, box->y2 - box->y1);
+}
+
+
+static gboolean
+impl_show_popup (RBDisplayPage *page)
+{
+	RBVisualizerPage *vpage = RB_VISUALIZER_PAGE (page);
+	gtk_menu_popup (GTK_MENU (vpage->popup), NULL, NULL, NULL, NULL, 3, gtk_get_current_event_time ());
+	return TRUE;
+}
+
+static void
+impl_selected (RBDisplayPage *bpage)
+{
+	RBVisualizerPage *page = RB_VISUALIZER_PAGE (bpage);
+	ClutterActor *stage;
+
+	if (page->embed == NULL) {
+		page->embed = gtk_clutter_embed_new ();
+
+		stage = gtk_clutter_embed_get_stage (GTK_CLUTTER_EMBED (page->embed));
+		g_signal_connect_object (stage, "allocation-changed", G_CALLBACK (resize_sink_texture), page->texture, 0);
+		g_signal_connect_object (stage, "button-press-event", G_CALLBACK (stage_button_press_cb), page, 0);
+		g_signal_connect_object (stage, "key-release-event", G_CALLBACK (stage_key_release_cb), page, 0);
+		clutter_container_add (CLUTTER_CONTAINER (stage), page->texture, NULL);
+
+		gtk_box_pack_start (GTK_BOX (page), page->embed, TRUE, TRUE, 0);
+		gtk_widget_show_all (GTK_WIDGET (page));
+	}
+
+	g_signal_emit (page, signals[START], 0);
+}
+
+static void
+impl_deselected (RBDisplayPage *bpage)
+{
+	RBVisualizerPage *page = RB_VISUALIZER_PAGE (bpage);
+
+	if (page->fullscreen == NULL) {
+		g_signal_emit (page, signals[STOP], 0);
+	} else {
+		/* might as well leave it running.. */
+	}
+}
+
+static void
+impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+	RBVisualizerPage *page = RB_VISUALIZER_PAGE (object);
+
+	switch (prop_id) {
+	case PROP_SINK:
+		g_value_set_object (value, page->sink);
+		break;
+	case PROP_POPUP:
+		g_value_set_object (value, page->popup);
+		break;
+	case PROP_FULLSCREEN_ACTION:
+		g_value_set_object (value, page->fullscreen_action);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+	RBVisualizerPage *page = RB_VISUALIZER_PAGE (object);
+
+	switch (prop_id) {
+	case PROP_POPUP:
+		page->popup = g_value_get_object (value);
+		break;
+	case PROP_FULLSCREEN_ACTION:
+		page->fullscreen_action = g_value_get_object (value);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+impl_dispose (GObject *object)
+{
+	RBVisualizerPage *page = RB_VISUALIZER_PAGE (object);
+
+	if (page->embed != NULL) {
+		gtk_container_remove (GTK_CONTAINER (page), page->embed);
+		page->embed = NULL;
+	}
+	if (page->sink != NULL) {
+		g_object_unref (page->sink);
+		page->sink = NULL;
+	}
+	if (page->popup != NULL) {
+		g_object_unref (page->popup);
+		page->popup = NULL;
+	}
+
+	G_OBJECT_CLASS (rb_visualizer_page_parent_class)->dispose (object);
+}
+
+static void
+impl_constructed (GObject *object)
+{
+	RBVisualizerPage *page;
+	ClutterInitError err;
+	GstElement *realsink;
+	GstElement *capsfilter;
+	GstCaps *caps;
+	GstPad *pad;
+
+	RB_CHAIN_GOBJECT_METHOD (rb_visualizer_page_parent_class, constructed, object);
+	page = RB_VISUALIZER_PAGE (object);
+
+	err = gtk_clutter_init (NULL, NULL);
+	if (err != CLUTTER_INIT_SUCCESS) {
+		/* maybe do something more sensible here.  not sure if there are any user-recoverable
+		 * conditions that would cause clutter init to fail, though, so it may not be worth it.
+		 * as it is, we just won't add the page to the page tree.
+		 */
+		g_warning ("Unable to display visual effects due to Clutter init failure");
+		return;
+	}
+
+	page->texture = clutter_texture_new ();
+	clutter_texture_set_sync_size (CLUTTER_TEXTURE (page->texture), TRUE);
+	clutter_texture_set_keep_aspect_ratio (CLUTTER_TEXTURE (page->texture), TRUE);
+
+	page->sink = gst_bin_new (NULL);
+	g_object_ref (page->sink);
+
+	/* actual sink */
+	realsink = clutter_gst_video_sink_new (CLUTTER_TEXTURE (page->texture));
+
+	/* capsfilter to force rgb format (without this we end up using ayuv) */
+	capsfilter = gst_element_factory_make ("capsfilter", NULL);
+	caps = gst_caps_from_string ("video/x-raw-rgb,bpp=(int)24,depth=(int)24,"
+				     "endianness=(int)4321,red_mask=(int)16711680,"
+				     "green_mask=(int)65280,blue_mask=(int)255");
+	g_object_set (capsfilter, "caps", caps, NULL);
+	gst_caps_unref (caps);
+
+	gst_bin_add_many (GST_BIN (page->sink), capsfilter, realsink, NULL);
+	gst_element_link (capsfilter, realsink);
+
+	pad = gst_element_get_static_pad (capsfilter, "sink");
+	gst_element_add_pad (page->sink, gst_ghost_pad_new ("sink", pad));
+	gst_object_unref (pad);
+
+	g_signal_connect_object (page->fullscreen_action,
+				 "toggled",
+				 G_CALLBACK (toggle_fullscreen_cb),
+				 page, 0);
+}
+
+static void
+rb_visualizer_page_init (RBVisualizerPage *page)
+{
+}
+
+static void
+rb_visualizer_page_class_init (RBVisualizerPageClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+	RBDisplayPageClass *page_class = RB_DISPLAY_PAGE_CLASS (klass);
+
+	object_class->constructed = impl_constructed;
+	object_class->get_property = impl_get_property;
+	object_class->set_property = impl_set_property;
+	object_class->dispose = impl_dispose;
+
+	page_class->selected = impl_selected;
+	page_class->deselected = impl_deselected;
+	page_class->show_popup = impl_show_popup;
+
+	g_object_class_install_property (object_class,
+					 PROP_SINK,
+					 g_param_spec_object ("sink",
+							      "sink",
+							      "gstreamer sink element",
+							      GST_TYPE_ELEMENT,
+							      G_PARAM_READABLE));
+	g_object_class_install_property (object_class,
+					 PROP_POPUP,
+					 g_param_spec_object ("popup",
+							      "popup",
+							      "popup menu",
+							      GTK_TYPE_WIDGET,
+							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+	g_object_class_install_property (object_class,
+					 PROP_FULLSCREEN_ACTION,
+					 g_param_spec_object ("fullscreen-action",
+							      "fullscreen action",
+							      "GtkToggleAction for fullscreen",
+							      GTK_TYPE_TOGGLE_ACTION,
+							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+	signals[START] = g_signal_new ("start",
+				       RB_TYPE_VISUALIZER_PAGE,
+				       G_SIGNAL_RUN_LAST,
+				       0,
+				       NULL, NULL,
+				       g_cclosure_marshal_VOID__VOID,
+				       G_TYPE_NONE,
+				       0);
+	signals[STOP] = g_signal_new ("stop",
+				      RB_TYPE_VISUALIZER_PAGE,
+				      G_SIGNAL_RUN_LAST,
+				      0,
+				      NULL, NULL,
+				      g_cclosure_marshal_VOID__VOID,
+				      G_TYPE_NONE,
+				      0);
+	signals[FULLSCREEN] = g_signal_new_class_handler ("toggle-fullscreen",
+							  RB_TYPE_VISUALIZER_PAGE,
+							  G_SIGNAL_RUN_LAST,
+							  G_CALLBACK (toggle_fullscreen),
+							  NULL, NULL,
+							  g_cclosure_marshal_VOID__VOID,
+							  G_TYPE_NONE,
+							  0);
+}
+
+static void
+rb_visualizer_page_class_finalize (RBVisualizerPageClass *klass)
+{
+}
+
+void
+_rb_visualizer_page_register_type (GTypeModule *module)
+{
+	rb_visualizer_page_register_type (module);
+}
diff --git a/plugins/visualizer/rb-visualizer-page.h b/plugins/visualizer/rb-visualizer-page.h
new file mode 100644
index 0000000..a2524f2
--- /dev/null
+++ b/plugins/visualizer/rb-visualizer-page.h
@@ -0,0 +1,82 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2010  Jonathan Matthew <jonathan d14n org>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your version.
+ *
+ *  This program 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 General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#ifndef RB_VISUALIZER_PAGE_H
+#define RB_VISUALIZER_PAGE_H
+
+#include <shell/rb-shell.h>
+#include <sources/rb-display-page.h>
+
+#include <clutter/clutter.h>
+#include <clutter-gst/clutter-gst.h>
+#include <clutter-gtk/clutter-gtk.h>
+
+G_BEGIN_DECLS
+
+typedef struct _RBVisualizerPage RBVisualizerPage;
+typedef struct _RBVisualizerPageClass RBVisualizerPageClass;
+
+struct _RBVisualizerPage
+{
+	RBDisplayPage parent;
+
+	GtkWidget *embed;
+
+	GstElement *sink;
+	ClutterActor *texture;
+
+	GtkWidget *fullscreen;
+
+	GtkWidget *popup;
+	GtkToggleAction *fullscreen_action;
+	gboolean setting_state;
+};
+
+struct _RBVisualizerPageClass
+{
+	RBDisplayPageClass parent_class;
+};
+
+GType          rb_visualizer_page_get_type    (void);
+void           _rb_visualizer_page_register_type (GTypeModule *module);
+
+#define RB_TYPE_VISUALIZER_PAGE		(rb_visualizer_page_get_type ())
+#define RB_VISUALIZER_PAGE(o)		(G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_VISUALIZER_PAGE, RBVisualizerPage))
+#define RB_IS_VISUALIZER_PAGE(o)	(G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_VISUALIZER_PAGE))
+#define RB_VISUALIZER_PAGE_CLASS(k) 	(G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_VISUALIZER_PAGE, RBVisualizerPageClass))
+#define RB_IS_VISUALIZER_PAGE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_VISUALIZER_PAGE))
+#define RB_VISUALIZER_PAGE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_VISUALIZER_PAGE, RBVisualizerPageClass))
+
+RBVisualizerPage        *rb_visualizer_page_new              (GObject *plugin,
+							      RBShell *shell,
+							      GtkToggleAction *fullscreen,
+							      GtkWidget *popup);
+
+G_END_DECLS
+
+#endif /* RB_VISUALIZER_PAGE_H */
diff --git a/plugins/visualizer/rb-visualizer-plugin.c b/plugins/visualizer/rb-visualizer-plugin.c
new file mode 100644
index 0000000..c07c34c
--- /dev/null
+++ b/plugins/visualizer/rb-visualizer-plugin.c
@@ -0,0 +1,482 @@
+/*
+ * Copyright (C) 2010 Jonathan Matthew <jonathan d14n org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * The Rhythmbox authors hereby grant permission for non-GPL compatible
+ * GStreamer plugins to be used and distributed together with GStreamer
+ * and Rhythmbox. This permission is above and beyond the permissions granted
+ * by the GPL license by which Rhythmbox is covered. If you modify this code
+ * you may extend this exception to your version of the code, but you are not
+ * obligated to do so. If you do not wish to do so, delete this exception
+ * statement from your version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ */
+
+#include <config.h>
+
+#include <glib/gi18n-lib.h>
+#include <gmodule.h>
+#include <gtk/gtk.h>
+#include <glib.h>
+#include <glib-object.h>
+#include <libpeas/peas.h>
+
+#include <plugins/rb-plugin-macros.h>
+#include <shell/rb-shell-player.h>
+#include <sources/rb-display-page.h>
+#include <sources/rb-display-page-group.h>
+#include <sources/rb-display-page-model.h>
+#include <backends/rb-player.h>
+#include <backends/rb-player-gst-tee.h>
+#include <lib/rb-debug.h>
+
+#include "rb-visualizer-page.h"
+#include "rb-visualizer-fullscreen.h"
+#include "rb-visualizer-menu.h"
+
+#define RB_TYPE_VISUALIZER_PLUGIN		(rb_visualizer_plugin_get_type ())
+#define RB_VISUALIZER_PLUGIN(o)			(G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_VISUALIZER_PLUGIN, RBVisualizerPlugin))
+#define RB_VISUALIZER_PLUGIN_CLASS(k)		(G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_VISUALIZER_PLUGIN, RBVisualizerPluginClass))
+#define RB_IS_VISUALIZER_PLUGIN(o)		(G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_VISUALIZER_PLUGIN))
+#define RB_IS_VISUALIZER_PLUGIN_CLASS(k)	(G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_VISUALIZER_PLUGIN))
+#define RB_VISUALIZER_PLUGIN_GET_CLASS(o)	(G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_VISUALIZER_PLUGIN, RBVisualizerPluginClass))
+
+/* playbin2 flag(s) */
+#define PLAYBIN2_FLAG_VIS	0x08
+
+typedef struct
+{
+	PeasExtensionBase parent;
+
+	RBShellPlayer *shell_player;
+	RBPlayer *player;
+
+	/* pipeline stuff */
+	GstElement *visualizer;
+	GstElement *sink;
+
+	GstElement *identity;
+	GstElement *capsfilter;
+	GstElement *vis_plugin;
+
+	GstElement *playbin;
+	gulong playbin_notify_id;
+
+	/* ui */
+	RBVisualizerPage *page;
+
+	GSettings *settings;
+} RBVisualizerPlugin;
+
+typedef struct
+{
+	PeasExtensionBaseClass parent_class;
+} RBVisualizerPluginClass;
+
+
+G_MODULE_EXPORT void peas_register_types (PeasObjectModule *module);
+
+RB_DEFINE_PLUGIN(RB_TYPE_VISUALIZER_PLUGIN, RBVisualizerPlugin, rb_visualizer_plugin,)
+
+static void
+fixate_vis_caps (RBVisualizerPlugin *plugin)
+{
+	GstPad *pad;
+	GstCaps *caps = NULL;
+	const GstCaps *template_caps;
+
+	pad = gst_element_get_static_pad (plugin->vis_plugin, "src");
+	template_caps = gst_pad_get_pad_template_caps (pad);
+	gst_object_unref (pad);
+
+	if (template_caps == NULL) {
+		rb_debug ("vis element has no template caps?");
+		return;
+	}
+
+	caps = gst_caps_copy (template_caps);
+
+	if (gst_caps_is_fixed (caps) == FALSE) {
+		guint i;
+		char *dbg;
+		const VisualizerQuality *q = &rb_visualizer_quality[g_settings_get_enum (plugin->settings, "quality")];
+
+		rb_debug ("fixating caps towards %dx%d, %d/%d", q->width, q->height, q->fps_n, q->fps_d);
+		caps = gst_caps_make_writable (caps);
+		for (i = 0; i < gst_caps_get_size (caps); i++) {
+			GstStructure *s = gst_caps_get_structure (caps, i);
+
+			gst_structure_fixate_field_nearest_int (s, "width", q->width);
+			gst_structure_fixate_field_nearest_int (s, "height", q->height);
+			gst_structure_fixate_field_nearest_fraction (s, "framerate", q->fps_n, q->fps_d);
+		}
+
+		dbg = gst_caps_to_string (caps);
+		rb_debug ("setting fixed caps on capsfilter: %s", dbg);
+		g_free (dbg);
+
+		g_object_set (plugin->capsfilter, "caps", caps, NULL);
+	} else {
+		char *dbg = gst_caps_to_string (caps);
+		rb_debug ("vis element caps already fixed: %s", dbg);
+		g_free (dbg);
+	}
+
+	gst_caps_unref (caps);
+}
+
+static void
+mutate_playbin (RBVisualizerPlugin *plugin, GstElement *playbin)
+{
+	GstElement *current_vis_plugin;
+	GstElement *current_video_sink;
+	int playbin_flags;
+
+	if (playbin == plugin->playbin)
+		return;
+
+	rb_debug ("mutating playbin");
+
+	/* check no one has already set the playbin properties we're interested in */
+	g_object_get (playbin,
+		      "vis-plugin", &current_vis_plugin,
+		      "video-sink", &current_video_sink,
+		      "flags", &playbin_flags,
+		      NULL);
+
+	/* ignore fakesinks */
+	if (current_video_sink != NULL) {
+		const char *factoryname;
+		GstElementFactory *factory;
+
+		factory = gst_element_get_factory (current_video_sink);
+		factoryname = gst_plugin_feature_get_name (GST_PLUGIN_FEATURE (factory));
+		if (strcmp (factoryname, "fakesink") == 0) {
+			g_object_unref (current_video_sink);
+			current_video_sink = NULL;
+		}
+	}
+
+	if ((current_vis_plugin != NULL) || (current_video_sink != NULL)) {
+		g_warning ("sink and/or vis plugin already set on playbin");
+		if (current_vis_plugin)
+			g_object_unref (current_vis_plugin);
+		if (current_video_sink)
+			g_object_unref (current_video_sink);
+		return;
+	}
+
+	/* detach from old playbin (this should never really happen) */
+	if (plugin->playbin) {
+		g_object_unref (plugin->playbin);
+	}
+
+	/* attach to new playbin */
+	plugin->playbin = g_object_ref (playbin);
+	g_object_set (plugin->playbin, "video-sink", plugin->sink, NULL);
+
+	/* start visualizer if it's supposed to be running */
+	if (plugin->visualizer != NULL) {
+		playbin_flags |= PLAYBIN2_FLAG_VIS;
+		g_object_set (plugin->playbin,
+			      "flags", playbin_flags,
+			      "vis-plugin", plugin->visualizer,
+			      NULL);
+	}
+}
+
+static void
+playbin_notify_cb (GObject *object, GParamSpec *arg, RBVisualizerPlugin *pi)
+{
+	GstElement *playbin;
+
+	g_object_get (object, "playbin", &playbin, NULL);
+	if (playbin) {
+		mutate_playbin (pi, playbin);
+		g_object_unref (playbin);
+	}
+}
+
+
+static void
+update_visualizer (RBVisualizerPlugin *plugin)
+{
+	if (plugin->visualizer == NULL) {
+		return;
+	}
+
+	/* pad blocking and other such nonsense, i guess */
+}
+
+static void
+start_visualizer_cb (RBVisualizerPage *page, RBVisualizerPlugin *plugin)
+{
+	GstPad *pad;
+	char *plugin_name;
+
+	if (plugin->visualizer) {
+		g_object_unref (plugin->visualizer);
+		plugin->visualizer = NULL;
+		plugin->identity = NULL;
+		plugin->capsfilter = NULL;
+		plugin->vis_plugin = NULL;
+	}
+	plugin->visualizer = gst_bin_new (NULL);
+
+	/* create common bits of visualizer bin: identity ! <effect> ! capsfilter */
+	plugin->identity = gst_element_factory_make ("identity", NULL);
+	plugin->capsfilter = gst_element_factory_make ("capsfilter", NULL);
+
+	plugin_name = g_settings_get_string (plugin->settings, "vis-plugin");
+	if (plugin_name != NULL) {
+		plugin->vis_plugin = gst_element_factory_make (plugin_name, NULL);
+		if (plugin->vis_plugin == NULL) {
+			g_warning ("Configured visualizer plugin %s not available", plugin_name);
+		}
+		g_free (plugin_name);
+	}
+	if (plugin->vis_plugin == NULL) {
+		plugin->vis_plugin = gst_element_factory_make ("goom", NULL);
+		if (plugin->vis_plugin == NULL) {
+			g_warning ("Fallback visualizer plugin (goom) not available");
+			return;
+		}
+	}
+
+	/* set up capsfilter */
+	gst_bin_add_many (GST_BIN (plugin->visualizer), plugin->identity, plugin->vis_plugin, plugin->capsfilter, NULL);
+
+	pad = gst_element_get_static_pad (plugin->identity, "sink");
+	gst_element_add_pad (plugin->visualizer, gst_ghost_pad_new ("sink", pad));
+	gst_object_unref (pad);
+
+	/* XXX check errors etc. */
+	if (gst_element_link_many (plugin->identity, plugin->vis_plugin, plugin->capsfilter, NULL) == FALSE) {
+		g_warning ("couldn't link visualizer bin elements");
+		return;
+	}
+	fixate_vis_caps (plugin);
+
+	g_object_ref (plugin->visualizer);
+
+	if (plugin->playbin_notify_id) {
+		GstPad *pad;
+		int playbin_flags;
+
+		pad = gst_element_get_static_pad (plugin->capsfilter, "src");
+		gst_element_add_pad (plugin->visualizer, gst_ghost_pad_new ("src", pad));
+		gst_object_unref (pad);
+
+		g_object_get (plugin->playbin, "flags", &playbin_flags, NULL);
+		if (plugin->playbin != NULL) {
+			playbin_flags |= PLAYBIN2_FLAG_VIS;
+			rb_debug ("enabling vis; new playbin2 flags %x", playbin_flags);
+			g_object_set (plugin->playbin,
+				      "vis-plugin", plugin->visualizer,
+				      "flags", playbin_flags,
+				      NULL);
+		} else {
+			rb_debug ("playback hasn't started yet");
+		}
+	} else {
+		GstElement *colorspace;
+		GstElement *queue;
+
+		colorspace = gst_element_factory_make ("ffmpegcolorspace", NULL);
+		queue = gst_element_factory_make ("queue", NULL);
+
+		g_object_set (queue, "max-size-buffers", 3, "max-size-bytes", 0, "max-size-time", (gint64) 0, NULL);
+
+		gst_bin_add_many (GST_BIN (plugin->visualizer), queue, colorspace, plugin->sink, NULL);
+		gst_element_link_many (plugin->capsfilter, queue, colorspace, plugin->sink, NULL);
+
+		rb_debug ("adding visualizer bin to the pipeline");
+		rb_player_gst_tee_add_tee (RB_PLAYER_GST_TEE (plugin->player),
+					   plugin->visualizer);
+	}
+}
+
+static void
+stop_visualizer_cb (RBVisualizerPage *page, RBVisualizerPlugin *plugin)
+{
+	if (plugin->visualizer == NULL) {
+		return;
+	}
+
+	if (plugin->playbin_notify_id) {
+		int playbin_flags;
+
+		g_object_get (plugin->playbin, "flags", &playbin_flags, NULL);
+		playbin_flags &= ~PLAYBIN2_FLAG_VIS;
+		rb_debug ("disabling vis; new playbin2 flags %d", playbin_flags);
+		g_object_set (plugin->playbin,
+			      "flags", playbin_flags,
+			      "vis-plugin", NULL,
+			      NULL);
+	} else {
+		rb_debug ("removing visualizer bin from pipeline");
+		rb_player_gst_tee_remove_tee (RB_PLAYER_GST_TEE (plugin->player),
+					      plugin->visualizer);
+	}
+
+	if (plugin->visualizer) {
+		g_object_unref (plugin->visualizer);
+		plugin->visualizer = NULL;
+	}
+}
+
+static void
+settings_changed_cb (GSettings *settings, const char *key, RBVisualizerPlugin *plugin)
+{
+	update_visualizer (plugin);
+}
+
+static void
+playing_song_changed_cb (RBShellPlayer *player, RhythmDBEntry *entry, RBVisualizerPlugin *plugin)
+{
+	g_object_set (plugin->page, "visibility", (entry != NULL), NULL);
+}
+
+static void
+impl_activate (PeasActivatable *activatable)
+{
+	RBVisualizerPlugin *pi = RB_VISUALIZER_PLUGIN (activatable);
+	RBDisplayPageGroup *page_group;
+	RhythmDBEntry *entry;
+	GtkToggleAction *fullscreen;
+	GtkWidget *menu;
+	RBShell *shell;
+
+	g_object_get (pi, "object", &shell, NULL);
+
+	pi->settings = g_settings_new ("org.gnome.rhythmbox.plugins.visualizer");
+	g_signal_connect_object (pi->settings, "changed", G_CALLBACK (settings_changed_cb), pi, 0);
+
+	/* create UI actions and menus and stuff */
+	fullscreen = gtk_toggle_action_new ("VisualizerFullscreen",
+					    _("Fullscreen"),
+					    _("Toggle fullscreen visual effects"),
+					    GTK_STOCK_FULLSCREEN);
+	menu = rb_visualizer_create_popup_menu (fullscreen);
+	g_object_ref_sink (menu);
+
+	/* create visualizer page */
+	pi->page = rb_visualizer_page_new (G_OBJECT (pi), shell, fullscreen, menu);
+	g_signal_connect_object (pi->page, "start", G_CALLBACK (start_visualizer_cb), pi, 0);
+	g_signal_connect_object (pi->page, "stop", G_CALLBACK (stop_visualizer_cb), pi, 0);
+
+	/* don't do anything if we couldn't create a video sink (clutter is broken, etc.) */
+	g_object_get (pi->page, "sink", &pi->sink, NULL);
+	if (pi->sink == NULL) {
+		g_object_unref (shell);
+		return;
+	}
+
+	/* prepare style stuff for fullscreen display */
+	rb_visualizer_fullscreen_load_style (G_OBJECT (pi));
+
+	/* add the visualizer page to the UI */
+	page_group = rb_display_page_group_get_by_id ("display");
+	if (page_group == NULL) {
+		page_group = rb_display_page_group_new (G_OBJECT (shell),
+							"display",
+							_("Display"),
+							RB_DISPLAY_PAGE_GROUP_CATEGORY_TOOLS);
+		rb_shell_append_display_page (shell, RB_DISPLAY_PAGE (page_group), NULL);
+	}
+	g_object_set (pi->page, "visibility", FALSE, NULL);
+
+	rb_shell_append_display_page (shell, RB_DISPLAY_PAGE (pi->page), RB_DISPLAY_PAGE (page_group));
+
+	/* get player objects */
+	g_object_get (shell, "shell-player", &pi->shell_player, NULL);
+	g_object_get (pi->shell_player, "player", &pi->player, NULL);
+
+	/* only show the page in the page tree when playing something */
+	g_signal_connect_object (pi->shell_player, "playing-song-changed", G_CALLBACK (playing_song_changed_cb), pi, 0);
+	entry = rb_shell_player_get_playing_entry (pi->shell_player);
+	playing_song_changed_cb (pi->shell_player, entry, pi);
+	if (entry != NULL) {
+		rhythmdb_entry_unref (entry);
+	}
+
+	/* figure out how to insert the visualizer into the playback pipeline */
+	if (g_object_class_find_property (G_OBJECT_GET_CLASS (pi->player), "playbin")) {
+
+		rb_debug ("using playbin-based visualization");
+		pi->playbin_notify_id = g_signal_connect_object (pi->player,
+								 "notify::playbin",
+								 G_CALLBACK (playbin_notify_cb),
+								 pi,
+								 0);
+		g_object_get (pi->player, "playbin", &pi->playbin, NULL);
+		if (pi->playbin != NULL) {
+			mutate_playbin (pi, pi->playbin);
+		}
+	} else if (RB_IS_PLAYER_GST_TEE (pi->player)) {
+		rb_debug ("using tee-based visualization");
+	} else {
+		g_warning ("unknown player backend type");
+		g_object_unref (pi->player);
+		pi->player = NULL;
+	}
+
+	g_object_unref (shell);
+}
+
+static void
+impl_deactivate	(PeasActivatable *activatable)
+{
+	RBVisualizerPlugin *pi = RB_VISUALIZER_PLUGIN (activatable);
+
+	if (pi->page != NULL) {
+		stop_visualizer_cb (pi->page, pi);
+
+		rb_display_page_delete_thyself (RB_DISPLAY_PAGE (pi->page));
+		pi->page = NULL;
+	}
+
+	if (pi->sink != NULL) {
+		g_object_unref (pi->sink);
+		pi->sink = NULL;
+	}
+
+	if (pi->settings != NULL) {
+		g_object_unref (pi->settings);
+		pi->settings = NULL;
+	}
+}
+
+static void
+rb_visualizer_plugin_init (RBVisualizerPlugin *plugin)
+{
+	rb_debug ("RBVisualizerPlugin initialising");
+
+	/* for uninstalled builds, add plugins/visualizer/icons as an icon search path */
+#ifdef USE_UNINSTALLED_DIRS
+	gtk_icon_theme_append_search_path (gtk_icon_theme_get_default (),
+					   PLUGIN_SRC_DIR G_DIR_SEPARATOR_S "icons");
+#endif
+}
+
+G_MODULE_EXPORT void
+peas_register_types (PeasObjectModule *module)
+{
+	rb_visualizer_plugin_register_type (G_TYPE_MODULE (module));
+	_rb_visualizer_page_register_type (G_TYPE_MODULE (module));
+	peas_object_module_register_extension_type (module,
+						    PEAS_TYPE_ACTIVATABLE,
+						    RB_TYPE_VISUALIZER_PLUGIN);
+}
diff --git a/plugins/visualizer/visualizer-box.png b/plugins/visualizer/visualizer-box.png
new file mode 100644
index 0000000..bda2bcb
Binary files /dev/null and b/plugins/visualizer/visualizer-box.png differ
diff --git a/plugins/visualizer/visualizer-ui.xml b/plugins/visualizer/visualizer-ui.xml
new file mode 100644
index 0000000..da6292a
--- /dev/null
+++ b/plugins/visualizer/visualizer-ui.xml
@@ -0,0 +1,13 @@
+<ui>
+  <popup name="VisualizerPagePopup">
+    <menuitem name="VisualizerToggleFullscreen" action="VisualizerToggleFullscreen"/>
+    <menu name="VisualizerEffects" action="VisualizerEffects">
+      <placeholder/>
+    </menu>
+    <menu name="VisualizerQuality" action="VisualizerQuality">
+      <menuitem name="VisualizerQualityLow" action="VisualizerQualityLow"/>
+      <menuitem name="VisualizerQualityMedium" action="VisualizerQualityMedium"/>
+      <menuitem name="VisualizerQualityHigh" action="VisualizerQualityHigh"/>
+    </menu>
+  </popup>
+</ui>
diff --git a/plugins/visualizer/visualizer.css b/plugins/visualizer/visualizer.css
new file mode 100644
index 0000000..5cb0608
--- /dev/null
+++ b/plugins/visualizer/visualizer.css
@@ -0,0 +1,56 @@
+*
+{
+  font-size: 13;
+  color: #FFFFFF;
+}
+
+MxBoxLayout.TrackInfoBox
+{
+  padding: 7 7 7 7;
+  border-image: url("visualizer-box.png") 7 7 7 7;
+}
+
+MxBoxLayout.ControlsBox
+{
+  padding: 7 7 7 7;
+  border-image: url("visualizer-box.png") 7 7 7 7;
+}
+
+MxFrame.TrackInfoImage
+{
+  padding: 7 7 7 7;
+  border-image: url("visualizer-box.png") 7 7 7 7;
+}
+
+MxButton
+{
+  -mx-border-image-transition-duration: 120;
+  padding: 5 13 5 13;
+  border-image: url("button.png") 11 5 13 5;
+}
+
+MxButton:disabled
+{
+  -mx-border-image-transition-duration: 0;
+  border-image: url("button-disabled.png") 10;
+  color: #adaead;
+}
+
+MxButton:hover
+{
+  -mx-border-image-transition-duration: 0;
+  border-image: url("button-hover.png") 10;
+}
+
+MxButton:focus
+{
+  -mx-border-image-transition-duration: 0;
+  border-image: url("button-focus.png") 10;
+}
+
+MxButton:active, MxButton:checked
+{
+  -mx-border-image-transition-duration: 0;
+  padding: 6 13 4 13;
+  border-image: url("button-active.png") 10;
+}
diff --git a/plugins/visualizer/visualizer.plugin.in b/plugins/visualizer/visualizer.plugin.in
new file mode 100644
index 0000000..c4cef7b
--- /dev/null
+++ b/plugins/visualizer/visualizer.plugin.in
@@ -0,0 +1,8 @@
+[Plugin]
+Module=visualizer
+IAge=2
+_Name=Visualization
+_Description=Displays visualizations
+Authors=Jonathan Matthew
+Copyright=Copyright  2010 Jonathan Matthew
+Website=http://www.rhythmbox.org
diff --git a/po/POTFILES.in b/po/POTFILES.in
index d31a769..6a72329 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -137,6 +137,11 @@ plugins/sample/rb-sample-plugin.c
 [type: gettext/ini]plugins/sample-vala/sample-vala.plugin.in
 plugins/sendto/sendto.py
 [type: gettext/ini]plugins/sendto/sendto.plugin.in
+plugins/visualizer/rb-visualizer-fullscreen.c
+plugins/visualizer/rb-visualizer-menu.c
+plugins/visualizer/rb-visualizer-page.c
+plugins/visualizer/rb-visualizer-plugin.c
+[type: gettext/ini]plugins/visualizer/visualizer.plugin.in
 podcast/rb-feed-podcast-properties-dialog.c
 podcast/rb-podcast-main-source.c
 podcast/rb-podcast-manager.c



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]