[rhythmbox] New widget for displaying album art images



commit 739e2b31662f936ac90f4a7f247ee1acb8dfb282
Author: Jonathan Matthew <jonathan d14n org>
Date:   Wed Mar 28 22:18:28 2012 +1000

    New widget for displaying album art images
    
    This is similar to the widget in the artdisplay plugin,
    except it uses cairo for compositing rather than gdk-pixbuf,
    it displays a larger image in a tooltip (up to a limit),
    and it fades between images at a much higher frame rate.

 configure.ac                                       |    1 +
 data/icons/hicolor/48x48/Makefile.am               |    2 +-
 data/icons/hicolor/48x48/status/Makefile.am        |   11 +
 .../48x48/status/rhythmbox-missing-artwork.png     |  Bin 0 -> 722 bytes
 lib/rb-stock-icons.c                               |    1 +
 lib/rb-stock-icons.h                               |    1 +
 po/POTFILES.in                                     |    1 +
 widgets/Makefile.am                                |    6 +-
 widgets/rb-fading-image.c                          |  681 ++++++++++++++++++++
 widgets/rb-fading-image.h                          |   68 ++
 10 files changed, 769 insertions(+), 3 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index a469f1a..230230e 100644
--- a/configure.ac
+++ b/configure.ac
@@ -851,6 +851,7 @@ data/icons/hicolor/32x32/apps/Makefile
 data/icons/hicolor/32x32/places/Makefile
 data/icons/hicolor/48x48/Makefile
 data/icons/hicolor/48x48/apps/Makefile
+data/icons/hicolor/48x48/status/Makefile
 data/icons/hicolor/256x256/Makefile
 data/icons/hicolor/256x256/apps/Makefile
 data/icons/hicolor/scalable/Makefile
diff --git a/data/icons/hicolor/48x48/Makefile.am b/data/icons/hicolor/48x48/Makefile.am
index ebbd145..1eae4a6 100644
--- a/data/icons/hicolor/48x48/Makefile.am
+++ b/data/icons/hicolor/48x48/Makefile.am
@@ -1 +1 @@
-SUBDIRS = apps
+SUBDIRS = apps status
diff --git a/data/icons/hicolor/48x48/status/Makefile.am b/data/icons/hicolor/48x48/status/Makefile.am
new file mode 100644
index 0000000..70f97fb
--- /dev/null
+++ b/data/icons/hicolor/48x48/status/Makefile.am
@@ -0,0 +1,11 @@
+themedir = $(datadir)/icons/hicolor
+size = 48x48
+context = status
+
+iconsdir = $(themedir)/$(size)/$(context)
+
+icons_DATA = \
+	rhythmbox-missing-artwork.png
+
+EXTRA_DIST = \
+	$(icons_DATA)
diff --git a/data/icons/hicolor/48x48/status/rhythmbox-missing-artwork.png b/data/icons/hicolor/48x48/status/rhythmbox-missing-artwork.png
new file mode 100644
index 0000000..e198da6
Binary files /dev/null and b/data/icons/hicolor/48x48/status/rhythmbox-missing-artwork.png differ
diff --git a/lib/rb-stock-icons.c b/lib/rb-stock-icons.c
index 8efa041..9c257ea 100644
--- a/lib/rb-stock-icons.c
+++ b/lib/rb-stock-icons.c
@@ -59,6 +59,7 @@ const char RB_STOCK_PLAYLIST[] = "playlist";
 const char RB_STOCK_PLAYLIST_NEW[] = "playlist-new";
 const char RB_STOCK_AUTO_PLAYLIST[] = "playlist-automatic";
 const char RB_STOCK_AUTO_PLAYLIST_NEW[] = "playlist-automatic-new";
+const char RB_STOCK_MISSING_ARTWORK[] = "rhythmbox-missing-artwork";
 const char GNOME_MEDIA_SHUFFLE[] = "media-playlist-shuffle";
 const char GNOME_MEDIA_REPEAT[] = "media-playlist-repeat";
 const char GNOME_MEDIA_EJECT[] = "media-eject";
diff --git a/lib/rb-stock-icons.h b/lib/rb-stock-icons.h
index 662fa5f..fee81fa 100644
--- a/lib/rb-stock-icons.h
+++ b/lib/rb-stock-icons.h
@@ -43,6 +43,7 @@ extern const char RB_STOCK_PLAYLIST[];
 extern const char RB_STOCK_PLAYLIST_NEW[];
 extern const char RB_STOCK_AUTO_PLAYLIST[];
 extern const char RB_STOCK_AUTO_PLAYLIST_NEW[];
+extern const char RB_STOCK_MISSING_ARTWORK[];
 extern const char GNOME_MEDIA_SHUFFLE[];
 extern const char GNOME_MEDIA_REPEAT[];
 extern const char GNOME_MEDIA_EJECT[];
diff --git a/po/POTFILES.in b/po/POTFILES.in
index cad1047..27cd6c7 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -181,6 +181,7 @@ widgets/rb-alert-dialog.c
 widgets/rb-cell-renderer-pixbuf.c
 widgets/rb-dialog.c
 widgets/rb-entry-view.c
+widgets/rb-fading-image.c
 widgets/rb-header.c
 widgets/rb-library-browser.c
 widgets/rb-property-view.c
diff --git a/widgets/Makefile.am b/widgets/Makefile.am
index a2aa52a..9f8510d 100644
--- a/widgets/Makefile.am
+++ b/widgets/Makefile.am
@@ -13,7 +13,8 @@ widgetinclude_HEADERS =					\
 	rb-segmented-bar.h				\
 	rb-song-info.h					\
 	rb-source-toolbar.h				\
-	rb-uri-dialog.h
+	rb-uri-dialog.h					\
+	rb-fading-image.h
 
 librbwidgets_la_SOURCES =				\
 	$(widgetinclude_HEADERS)			\
@@ -44,7 +45,8 @@ librbwidgets_la_SOURCES =				\
 	eggwrapbox.h					\
 	eggwrapbox-enums.c				\
 	eggwrapbox-enums.h				\
-	rb-source-toolbar.c
+	rb-source-toolbar.c				\
+	rb-fading-image.c
 
 INCLUDES =						\
 	-DGNOMELOCALEDIR=\""$(datadir)/locale"\"        \
diff --git a/widgets/rb-fading-image.c b/widgets/rb-fading-image.c
new file mode 100644
index 0000000..2173ec3
--- /dev/null
+++ b/widgets/rb-fading-image.c
@@ -0,0 +1,681 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ *  Copyright (C) 2012 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 <widgets/rb-fading-image.h>
+#include <lib/rb-debug.h>
+#include <lib/rb-util.h>
+
+#define RENDER_FRAME_TIME	(1000 / 25)	/* fps? */
+#define BORDER_WIDTH		1.0
+
+#define MAX_TOOLTIP_SIZE	256
+
+static void rb_fading_image_class_init (RBFadingImageClass *klass);
+static void rb_fading_image_init (RBFadingImage *image);
+
+struct _RBFadingImagePrivate
+{
+	char *fallback_icon;
+	cairo_pattern_t *current_pat;
+	cairo_pattern_t *next_pat;
+	cairo_pattern_t *fallback_pat;
+	gdouble alpha;
+
+	GdkPixbuf *current;
+	GdkPixbuf *current_full;
+	GdkPixbuf *next;
+	GdkPixbuf *next_full;
+	GdkPixbuf *fallback;
+	GdkPixbufLoader *loader;
+
+	guint64 start;
+	guint64 end;
+	gulong render_timer_id;
+};
+
+G_DEFINE_TYPE (RBFadingImage, rb_fading_image, GTK_TYPE_WIDGET)
+
+/**
+ * SECTION:rb-fading-image
+ * @short_description: image display widget that fades between two images
+ *
+ * This widget displays images, performing a simple fade transition between
+ * them.  It also emits signals when URIs or pixbufs are dropped onto it.
+ */
+
+enum
+{
+	PROP_0,
+	PROP_FALLBACK
+};
+
+enum
+{
+	URI_DROPPED,
+	PIXBUF_DROPPED,
+	LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = {0};
+
+static gboolean
+prepare_image (cairo_t *cr, cairo_pattern_t **save, GdkPixbuf *pixbuf)
+{
+	if (*save != NULL) {
+		cairo_set_source (cr, *save);
+		return TRUE;
+	}
+
+	if (pixbuf != NULL) {
+		gdk_cairo_set_source_pixbuf (cr, pixbuf, 0.0, 0.0);
+		*save = cairo_get_source (cr);
+		cairo_pattern_reference (*save);
+		return TRUE;
+	} else {
+		return FALSE;
+	}
+}
+
+static void
+impl_realize (GtkWidget *widget)
+{
+	GtkAllocation allocation;
+	GdkWindowAttr attributes;
+	GdkWindow *window;
+	int attributes_mask;
+
+	gtk_widget_set_realized (widget, TRUE);
+
+	gtk_widget_get_allocation (widget, &allocation);
+
+	attributes.x = allocation.x;
+	attributes.y = allocation.y;
+	attributes.width = allocation.width;
+	attributes.height = allocation.height;
+	attributes.wclass = GDK_INPUT_OUTPUT;
+	attributes.window_type = GDK_WINDOW_CHILD;
+	attributes.event_mask = gtk_widget_get_events (widget) | GDK_EXPOSURE_MASK | GDK_BUTTON_PRESS_MASK | GDK_KEY_RELEASE_MASK | GDK_FOCUS_CHANGE_MASK;
+	attributes.visual = gtk_widget_get_visual (widget);
+
+	attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL;
+
+	window = gdk_window_new (gtk_widget_get_parent_window (widget), &attributes, attributes_mask);
+	gtk_widget_set_window (widget, window);
+	gdk_window_set_user_data (window, widget);
+
+	gtk_widget_set_can_focus (widget, TRUE);
+}
+
+static void
+draw_image (cairo_t *cr, GdkPixbuf *image, int width, int height, cairo_extend_t extend, double alpha)
+{
+	cairo_matrix_t matrix;
+	int image_width;
+	int image_height;
+
+	image_width = gdk_pixbuf_get_width (image);
+	image_height = gdk_pixbuf_get_height (image);
+
+	cairo_save (cr);
+
+	cairo_matrix_init_translate (&matrix,
+				     - (BORDER_WIDTH + (width/2 - image_width/2)),
+				     - (BORDER_WIDTH + (height/2 - image_height/2)));
+	cairo_pattern_set_matrix (cairo_get_source (cr), &matrix);
+	cairo_pattern_set_filter (cairo_get_source (cr), CAIRO_FILTER_BEST);
+	cairo_pattern_set_extend (cairo_get_source (cr), extend);
+	cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
+
+	cairo_rectangle (cr, BORDER_WIDTH, BORDER_WIDTH, width, height);
+	cairo_clip (cr);
+	cairo_paint_with_alpha (cr, alpha);
+
+	cairo_restore (cr);
+}
+
+static gboolean
+impl_draw (GtkWidget *widget, cairo_t *cr)
+{
+	RBFadingImage *image;
+	int width;
+	int height;
+
+	image = RB_FADING_IMAGE (widget);
+	width = gtk_widget_get_allocated_width (widget);
+	height = gtk_widget_get_allocated_height (widget);
+
+	cairo_save (cr);
+	cairo_set_line_width (cr, BORDER_WIDTH);
+	cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 1.0);
+	cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
+	cairo_rectangle (cr, 0, 0, width, height);
+	cairo_stroke (cr);
+	cairo_restore (cr);
+
+	width -= 2 * BORDER_WIDTH;
+	height -= 2 * BORDER_WIDTH;
+
+	/* draw current image */
+	if (prepare_image (cr, &image->priv->current_pat, image->priv->current)) {
+		draw_image (cr,
+			    image->priv->current,
+			    width,
+			    height,
+			    CAIRO_EXTEND_NONE,
+			    1.0 - image->priv->alpha);
+	} else if (prepare_image (cr, &image->priv->fallback_pat, image->priv->fallback)) {
+		draw_image (cr,
+			    image->priv->fallback,
+			    width,
+			    height,
+			    CAIRO_EXTEND_PAD,
+			    1.0 - image->priv->alpha);
+	} else {
+		cairo_save (cr);
+		cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
+		cairo_rectangle (cr, BORDER_WIDTH, BORDER_WIDTH, width, height);
+		cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
+		cairo_clip (cr);
+		cairo_paint (cr);
+		cairo_restore (cr);
+	}
+
+	/* if we're fading to a new image, draw that too */
+	if (image->priv->alpha < 0.001) {
+		/* do nothing */
+	} else if (prepare_image (cr, &image->priv->next_pat, image->priv->next)) {
+		draw_image (cr,
+			    image->priv->next,
+			    width,
+			    height,
+			    CAIRO_EXTEND_NONE,
+			    image->priv->alpha);
+	} else if (prepare_image (cr, &image->priv->fallback_pat, image->priv->fallback)) {
+		draw_image (cr,
+			    image->priv->fallback,
+			    width,
+			    height,
+			    CAIRO_EXTEND_PAD,
+			    image->priv->alpha);
+	} else {
+		/* also do nothing */
+	}
+
+	return TRUE;
+}
+
+static gboolean
+impl_query_tooltip (GtkWidget *widget, int x, int y, gboolean keyboard_mode, GtkTooltip *tooltip)
+{
+	RBFadingImage *image = RB_FADING_IMAGE (widget);
+	GdkPixbuf *scaled;
+	GdkPixbuf *full;
+
+	if (image->priv->render_timer_id != 0) {
+		full = image->priv->next_full;
+		scaled = image->priv->next;
+	} else {
+		full = image->priv->current_full;
+		scaled = image->priv->current;
+	}
+
+	if (full == NULL) {
+		gtk_tooltip_set_icon (tooltip, NULL);
+		gtk_tooltip_set_text (tooltip, _("Drop artwork here"));
+		return TRUE;
+	} else if (full == scaled) {
+		return FALSE;
+	} else {
+		gtk_tooltip_set_icon (tooltip, full);
+		return TRUE;
+	}
+}
+
+static void
+impl_drag_data_received (GtkWidget *widget,
+			 GdkDragContext *context,
+			 int x,
+			 int y,
+			 GtkSelectionData *selection,
+			 guint info,
+			 guint time_)
+{
+	GdkPixbuf *pixbuf;
+	char **uris;
+
+	pixbuf = gtk_selection_data_get_pixbuf (selection);
+	if (pixbuf != NULL) {
+		g_signal_emit (widget, signals[PIXBUF_DROPPED], 0, pixbuf);
+		g_object_unref (pixbuf);
+		return;
+	}
+
+	uris = gtk_selection_data_get_uris (selection);
+	if (uris != NULL) {
+		if (uris[0] != NULL) {
+			g_signal_emit (widget, signals[URI_DROPPED], 0, uris[0]);
+		}
+
+		g_strfreev (uris);
+		return;
+	}
+
+	rb_debug ("weird drag data received");
+}
+
+static void
+impl_drag_data_get (GtkWidget *widget, GdkDragContext *context, GtkSelectionData *selection, guint info, guint time_)
+{
+	RBFadingImage *image = RB_FADING_IMAGE (widget);
+
+	if (image->priv->current_full) {
+		gtk_selection_data_set_pixbuf (selection, image->priv->current_full);
+	}
+
+	/* might be nice if we could provide a uri here? */
+}
+
+
+static void
+impl_finalize (GObject *object)
+{
+	RBFadingImage *image = RB_FADING_IMAGE (object);
+
+	g_free (image->priv->fallback_icon);
+
+	if (image->priv->current_pat != NULL) {
+		cairo_pattern_destroy (image->priv->current_pat);
+	}
+	if (image->priv->next_pat != NULL) {
+		cairo_pattern_destroy (image->priv->next_pat);
+	}
+	if (image->priv->fallback_pat != NULL) {
+		cairo_pattern_destroy (image->priv->fallback_pat);
+	}
+
+	G_OBJECT_CLASS (rb_fading_image_parent_class)->finalize (object);
+}
+
+static void
+impl_dispose (GObject *object)
+{
+	RBFadingImage *image = RB_FADING_IMAGE (object);
+
+	if (image->priv->render_timer_id != 0) {
+		g_source_remove (image->priv->render_timer_id);
+		image->priv->render_timer_id = 0;
+	}
+	if (image->priv->current != NULL) {
+		g_object_unref (image->priv->current);
+		image->priv->current = NULL;
+	}
+	if (image->priv->next != NULL) {
+		g_object_unref (image->priv->next);
+		image->priv->next = NULL;
+	}
+	if (image->priv->fallback != NULL) {
+		g_object_unref (image->priv->fallback);
+		image->priv->fallback = NULL;
+	}
+
+	G_OBJECT_CLASS (rb_fading_image_parent_class)->dispose (object);
+}
+
+static void
+impl_constructed (GObject *object)
+{
+	RBFadingImage *image;
+
+	RB_CHAIN_GOBJECT_METHOD (rb_fading_image_parent_class, constructed, object);
+
+	image = RB_FADING_IMAGE (object);
+
+	gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (image)),
+				     GTK_STYLE_CLASS_SPINNER);
+
+	if (image->priv->fallback_icon != NULL) {
+		GError *error = NULL;
+		image->priv->fallback =
+			gtk_icon_theme_load_icon (gtk_icon_theme_get_default (),
+						  image->priv->fallback_icon,
+						  48,
+						  GTK_ICON_LOOKUP_FORCE_SIZE,
+						  &error);
+		if (error != NULL) {
+			g_warning ("couldn't load fallback icon %s: %s", image->priv->fallback_icon, error->message);
+			g_clear_error (&error);
+		}
+	}
+
+	gtk_widget_set_has_tooltip (GTK_WIDGET (image), TRUE);
+
+	/* drag and drop target */
+	gtk_drag_dest_set (GTK_WIDGET (image), GTK_DEST_DEFAULT_ALL, NULL, 0, GDK_ACTION_COPY);
+	gtk_drag_dest_add_image_targets (GTK_WIDGET (image));
+	gtk_drag_dest_add_uri_targets (GTK_WIDGET (image));
+
+	/* drag and drop source */
+	gtk_drag_source_set (GTK_WIDGET (image), GDK_BUTTON1_MASK, NULL, 0, GDK_ACTION_COPY);
+	gtk_drag_source_add_image_targets (GTK_WIDGET (image));
+}
+
+
+static void
+impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+	RBFadingImage *image = RB_FADING_IMAGE (object);
+
+	switch (prop_id) {
+	case PROP_FALLBACK:
+		g_value_set_string (value, image->priv->fallback_icon);
+		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)
+{
+	RBFadingImage *image = RB_FADING_IMAGE (object);
+
+	switch (prop_id) {
+	case PROP_FALLBACK:
+		image->priv->fallback_icon = g_value_dup_string (value);
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+		break;
+	}
+}
+
+static void
+rb_fading_image_init (RBFadingImage *image)
+{
+	image->priv = G_TYPE_INSTANCE_GET_PRIVATE (image, RB_TYPE_FADING_IMAGE, RBFadingImagePrivate);
+}
+
+static void
+rb_fading_image_class_init (RBFadingImageClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+	object_class->constructed = impl_constructed;
+	object_class->dispose = impl_dispose;
+	object_class->finalize = impl_finalize;
+	object_class->set_property = impl_set_property;
+	object_class->get_property = impl_get_property;
+
+	widget_class->realize = impl_realize;
+	widget_class->draw = impl_draw;
+	widget_class->query_tooltip = impl_query_tooltip;
+	widget_class->drag_data_get = impl_drag_data_get;
+	widget_class->drag_data_received = impl_drag_data_received;
+
+	/**
+	 * RBFadingImage:fallback:
+	 *
+	 * Name of an icon to display when no image is available.
+	 */
+	g_object_class_install_property (object_class,
+					 PROP_FALLBACK,
+					 g_param_spec_string ("fallback",
+							      "fallback",
+							      "fallback icon name",
+							      NULL,
+							      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+	/**
+	 * RBFadingImage::uri-dropped
+	 * @image: the #RBFadingImage
+	 * @uri: the URI that was dropped
+	 *
+	 * Emitted when a URI is dragged and dropped on the image
+	 */
+	signals[URI_DROPPED] =
+		g_signal_new ("uri-dropped",
+			      G_OBJECT_CLASS_TYPE (object_class),
+			      G_SIGNAL_RUN_LAST,
+			      0,
+			      NULL, NULL,
+			      g_cclosure_marshal_VOID__STRING,
+			      G_TYPE_NONE,
+			      1, G_TYPE_STRING);
+
+	/**
+	 * RBFadingImage::pixbuf-dropped
+	 * @image: the #RBFadingImage
+	 * @pixbuf: the pixbuf that was dropped
+	 *
+	 * Emitted when an image is dragged and dropped on the image
+	 */
+	signals[PIXBUF_DROPPED] =
+		g_signal_new ("pixbuf-dropped",
+			      G_OBJECT_CLASS_TYPE (object_class),
+			      G_SIGNAL_RUN_LAST,
+			      0,
+			      NULL, NULL,
+			      g_cclosure_marshal_VOID__OBJECT,
+			      G_TYPE_NONE,
+			      1, GDK_TYPE_PIXBUF);
+
+	g_type_class_add_private (klass, sizeof (RBFadingImagePrivate));
+}
+
+
+static GdkPixbuf *
+scale_thumbnail_if_necessary (RBFadingImage *image, GdkPixbuf *pixbuf)
+{
+	int w, h;
+	int pw, ph;
+	int sw, sh;
+	double factor;
+
+	w = gtk_widget_get_allocated_width (GTK_WIDGET (image)) - 2 * BORDER_WIDTH;
+	h = gtk_widget_get_allocated_height (GTK_WIDGET (image)) - 2 * BORDER_WIDTH;
+	pw = gdk_pixbuf_get_width (pixbuf);
+	ph = gdk_pixbuf_get_height (pixbuf);
+
+	if (pw <= w && ph <= h) {
+		return g_object_ref (pixbuf);
+	}
+
+	if (pw > ph) {
+		sw = w;
+		factor = (double) w / pw;
+		sh = (int)((double)ph * factor);
+	} else {
+		sh = h;
+		factor = (double) h / ph;
+		sw = (int)((double)pw * factor);
+	}
+
+	return gdk_pixbuf_scale_simple (pixbuf, sw, sh, GDK_INTERP_HYPER);
+}
+
+static GdkPixbuf *
+scale_full_if_necessary (RBFadingImage *image, GdkPixbuf *pixbuf)
+{
+	int pw, ph;
+	int sw, sh;
+	double factor;
+
+	pw = gdk_pixbuf_get_width (pixbuf);
+	ph = gdk_pixbuf_get_height (pixbuf);
+
+	if (pw <= MAX_TOOLTIP_SIZE && ph <= MAX_TOOLTIP_SIZE) {
+		return g_object_ref (pixbuf);
+	}
+	if (pw > ph) {
+		sw = MAX_TOOLTIP_SIZE;
+		factor = (double) MAX_TOOLTIP_SIZE / pw;
+		sh = (int)((double)ph * factor);
+	} else {
+		sh = MAX_TOOLTIP_SIZE;
+		factor = (double) MAX_TOOLTIP_SIZE / ph;
+		sw = (int)((double)pw * factor);
+	}
+
+	return gdk_pixbuf_scale_simple (pixbuf, sw, sh, GDK_INTERP_HYPER);
+}
+
+
+static void
+clear_next (RBFadingImage *image)
+{
+	if (image->priv->next_pat != NULL) {
+		cairo_pattern_destroy (image->priv->next_pat);
+		image->priv->next_pat = NULL;
+	}
+	if (image->priv->next != NULL) {
+		g_object_unref (image->priv->next);
+		image->priv->next = NULL;
+	}
+	if (image->priv->next_full != NULL) {
+		g_object_unref (image->priv->next_full);
+		image->priv->next_full = NULL;
+	}
+}
+
+static void
+replace_current (RBFadingImage *image, GdkPixbuf *next, GdkPixbuf *next_full)
+{
+	if (image->priv->current_pat != NULL) {
+		cairo_pattern_destroy (image->priv->current_pat);
+		image->priv->current_pat = NULL;
+	}
+	if (image->priv->current != NULL) {
+		g_object_unref (image->priv->current);
+		image->priv->current = NULL;
+	}
+	if (image->priv->current_full != NULL) {
+		g_object_unref (image->priv->current_full);
+		image->priv->current_full = NULL;
+	}
+	if (next != NULL) {
+		image->priv->current = g_object_ref (next);
+	}
+	if (next_full != NULL) {
+		image->priv->current_full = g_object_ref (next_full);
+	}
+
+}
+
+/**
+ * rb_fading_image_set_pixbuf:
+ * @image: a #RBFadingImage
+ * @pixbuf: (transfer none): the next pixbuf to display
+ *
+ * Sets the next image to be displayed.
+ */
+void
+rb_fading_image_set_pixbuf (RBFadingImage *image, GdkPixbuf *pixbuf)
+{
+	GdkPixbuf *scaled = NULL;
+	GdkPixbuf *full = NULL;
+
+	clear_next (image);
+	if (pixbuf != NULL) {
+		scaled = scale_thumbnail_if_necessary (image, pixbuf);
+		full = scale_full_if_necessary (image, pixbuf);
+	}
+
+	if (image->priv->render_timer_id != 0) {
+		image->priv->next_full = full;
+		image->priv->next = scaled;
+	} else {
+		replace_current (image, scaled, full);
+		gtk_widget_queue_draw (GTK_WIDGET (image));
+		gtk_widget_trigger_tooltip_query (GTK_WIDGET (image));
+	}
+}
+
+static gboolean
+render_timer (RBFadingImage *image)
+{
+	gint64 now;
+
+	now = g_get_monotonic_time ();
+
+	/* calculate alpha, whether this is the last frame, etc. */
+	if (image->priv->next != NULL || image->priv->current != NULL) {
+		image->priv->alpha = (((double)now - image->priv->start) / (image->priv->end - image->priv->start));
+		if (image->priv->alpha > 1.0)
+			image->priv->alpha = 1.0;
+
+		gtk_widget_queue_draw (GTK_WIDGET (image));
+	}
+
+	if (now >= image->priv->end) {
+		replace_current (image, image->priv->next, image->priv->next_full);
+		clear_next (image);
+		gtk_widget_trigger_tooltip_query (GTK_WIDGET (image));
+		image->priv->alpha = 0.0;
+		image->priv->render_timer_id = 0;
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+static void
+update_render_timer (RBFadingImage *image)
+{
+	/* add timer if not already present */
+	if (image->priv->render_timer_id == 0) {
+		image->priv->render_timer_id = g_timeout_add (RENDER_FRAME_TIME,
+							      (GSourceFunc) render_timer,
+							      image);
+	}
+}
+
+/**
+ * rb_fading_image_start:
+ * @image: a #RBFadingImage
+ * @duration: length of fade in milliseconds
+ *
+ * Starts fading to the next image.  If no next image has been supplied,
+ * the fallback image will be used instead.  If the next image has been
+ * supplied, but has not finished loading yet, the fade will be delayed
+ * until it finishes.  If the previous fade has not yet finished,
+ * something tricky happens.
+ */
+void
+rb_fading_image_start (RBFadingImage *image, guint64 duration)
+{
+	image->priv->start = g_get_monotonic_time ();
+	image->priv->end = image->priv->start + (duration * 1000);
+
+	update_render_timer (image);
+}
diff --git a/widgets/rb-fading-image.h b/widgets/rb-fading-image.h
new file mode 100644
index 0000000..ec710cb
--- /dev/null
+++ b/widgets/rb-fading-image.h
@@ -0,0 +1,68 @@
+/*
+ *  Copyright (C) 2012 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_FADING_IMAGE_H
+#define RB_FADING_IMAGE_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define RB_TYPE_FADING_IMAGE         (rb_fading_image_get_type ())
+#define RB_FADING_IMAGE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_FADING_IMAGE, RBFadingImage))
+#define RB_FADING_IMAGE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_FADING_IMAGE, RBFadingImageClass))
+#define RB_IS_FADING_IMAGE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_FADING_IMAGE))
+#define RB_IS_FADING_IMAGE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_FADING_IMAGE))
+#define RB_FADING_IMAGE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_FADING_IMAGE, RBFadingImageClass))
+
+typedef struct _RBFadingImage RBFadingImage;
+typedef struct _RBFadingImageClass RBFadingImageClass;
+typedef struct _RBFadingImagePrivate RBFadingImagePrivate;
+
+struct _RBFadingImage
+{
+	GtkWidget parent;
+
+	RBFadingImagePrivate *priv;
+};
+
+struct _RBFadingImageClass
+{
+	GtkWidgetClass parent;
+};
+
+/* create instances using g_object_new, since you'll need to set widget properties too */
+
+GType		rb_fading_image_get_type	(void);
+
+void		rb_fading_image_set_pixbuf	(RBFadingImage *image, GdkPixbuf *pixbuf);
+
+void		rb_fading_image_start		(RBFadingImage *image, guint64 duration);
+
+G_END_DECLS
+
+#endif /* RB_FADING_IMAGE_H */



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