[rhythmbox] visualizer: new clutter-based visualizer plugin
- From: Jonathan Matthew <jmatthew src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [rhythmbox] visualizer: new clutter-based visualizer plugin
- Date: Sun, 24 Jul 2011 14:19:42 +0000 (UTC)
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 (¬hing);
+ 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", ¤t_vis_plugin,
+ "video-sink", ¤t_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]