/* gnome-bg-crossfade.h - fade window background between two surfaces
*
* Copyright (C) 2008 Red Hat, Inc.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public License
* as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
* Author: Ray Strode <rstrode@redhat.com>
*/
#include <string.h>
#include <math.h>
#include <stdarg.h>
#include <gio/gio.h>
#include <gdk/gdk.h>
#include <gdk/gdkx.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <gtk/gtk.h>
#include <cairo.h>
#include <cairo-xlib.h>
#define GNOME_DESKTOP_USE_UNSTABLE_API
#include "gnome-bg.h"
#include "gnome-bg-crossfade.h"
struct _GnomeBGCrossfadePrivate
{
GdkWindow *window;
int width;
int height;
cairo_surface_t *fading_surface;
cairo_surface_t *end_surface;
gdouble start_time;
gdouble total_duration;
guint timeout_id;
guint is_first_frame : 1;
};
enum {
PROP_0,
PROP_WIDTH,
PROP_HEIGHT,
};
enum {
FINISHED,
NUMBER_OF_SIGNALS
};
static guint signals[NUMBER_OF_SIGNALS] = { 0 };
G_DEFINE_TYPE (GnomeBGCrossfade, gnome_bg_crossfade, G_TYPE_OBJECT)
#define GNOME_BG_CROSSFADE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o),\
GNOME_TYPE_BG_CROSSFADE,\
GnomeBGCrossfadePrivate))
static void
gnome_bg_crossfade_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
GnomeBGCrossfade *fade;
g_assert (GNOME_IS_BG_CROSSFADE (object));
fade = GNOME_BG_CROSSFADE (object);
switch (property_id)
{
case PROP_WIDTH:
fade->priv->width = g_value_get_int (value);
break;
case PROP_HEIGHT:
fade->priv->height = g_value_get_int (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
gnome_bg_crossfade_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
GnomeBGCrossfade *fade;
g_assert (GNOME_IS_BG_CROSSFADE (object));
fade = GNOME_BG_CROSSFADE (object);
switch (property_id)
{
case PROP_WIDTH:
g_value_set_int (value, fade->priv->width);
break;
case PROP_HEIGHT:
g_value_set_int (value, fade->priv->height);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
gnome_bg_crossfade_finalize (GObject *object)
{
GnomeBGCrossfade *fade;
fade = GNOME_BG_CROSSFADE (object);
gnome_bg_crossfade_stop (fade);
if (fade->priv->fading_surface != NULL) {
cairo_surface_destroy (fade->priv->fading_surface);
fade->priv->fading_surface = NULL;
}
if (fade->priv->end_surface != NULL) {
g_object_unref (fade->priv->end_surface);
fade->priv->end_surface = NULL;
}
}
static void
gnome_bg_crossfade_class_init (GnomeBGCrossfadeClass *fade_class)
{
GObjectClass *gobject_class;
gobject_class = G_OBJECT_CLASS (fade_class);
gobject_class->get_property = gnome_bg_crossfade_get_property;
gobject_class->set_property = gnome_bg_crossfade_set_property;
gobject_class->finalize = gnome_bg_crossfade_finalize;
/**
* GnomeBGCrossfade:width:
*
* When a crossfade is running, this is width of the fading
* surface.
*/
g_object_class_install_property (gobject_class,
PROP_WIDTH,
g_param_spec_int ("width",
"Window Width",
"Width of window to fade",
0, G_MAXINT, 0,
G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE));
/**
* GnomeBGCrossfade:height:
*
* When a crossfade is running, this is height of the fading
* surface.
*/
g_object_class_install_property (gobject_class,
PROP_HEIGHT,
g_param_spec_int ("height", "Window Height",
"Height of window to fade on",
0, G_MAXINT, 0,
G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE));
/**
* GnomeBGCrossfade::finished:
* @fade: the #GnomeBGCrossfade that received the signal
* @window: the #GdkWindow the crossfade happend on.
*
* When a crossfade finishes, @window will have a copy
* of the end surface as its background, and this signal will
* get emitted.
*/
signals[FINISHED] = g_signal_new ("finished",
G_OBJECT_CLASS_TYPE (gobject_class),
G_SIGNAL_RUN_LAST, 0, NULL, NULL,
g_cclosure_marshal_VOID__OBJECT,
G_TYPE_NONE, 1, G_TYPE_OBJECT);
g_type_class_add_private (gobject_class, sizeof (GnomeBGCrossfadePrivate));
}
static void
gnome_bg_crossfade_init (GnomeBGCrossfade *fade)
{
fade->priv = GNOME_BG_CROSSFADE_GET_PRIVATE (fade);
fade->priv->fading_surface = NULL;
fade->priv->end_surface = NULL;
fade->priv->timeout_id = 0;
}
/**
* gnome_bg_crossfade_new:
* @width: The width of the crossfading window
* @height: The height of the crossfading window
*
* Creates a new object to manage crossfading a
* window background between two #cairo_surface_ts.
*
* Return value: the new #GnomeBGCrossfade
**/
GnomeBGCrossfade *
gnome_bg_crossfade_new (int width,
int height)
{
GObject *object;
object = g_object_new (GNOME_TYPE_BG_CROSSFADE,
"width", width,
"height", height, NULL);
return (GnomeBGCrossfade *) object;
}
static cairo_surface_t *
tile_surface (cairo_surface_t *surface,
int width,
int height)
{
cairo_surface_t *copy;
cairo_t *cr;
if (surface == NULL) {
copy = gdk_window_create_similar_surface (gdk_get_default_root_window (),
CAIRO_CONTENT_COLOR,
width, height);
} else {
copy = cairo_surface_create_similar (surface,
cairo_surface_get_content (surface),
width, height);
}
cr = cairo_create (copy);
if (surface != NULL) {
cairo_pattern_t *pattern;
cairo_set_source_surface (cr, surface, 0.0, 0.0);
pattern = cairo_get_source (cr);
cairo_pattern_set_extend (pattern, CAIRO_EXTEND_REPEAT);
} else {
GtkStyle *style;
style = gtk_widget_get_default_style ();
gdk_cairo_set_source_color (cr, &style->bg[GTK_STATE_NORMAL]);
}
cairo_paint (cr);
if (cairo_status (cr) != CAIRO_STATUS_SUCCESS) {
cairo_surface_destroy (copy);
copy = NULL;
}
cairo_destroy (cr);
return copy;
}
/**
* gnome_bg_crossfade_set_start_surface:
* @fade: a #GnomeBGCrossfade
* @surface: The cairo surface to fade from
*
* Before initiating a crossfade with gnome_bg_crossfade_start()
* a start and end surface have to be set. This function sets
* the surface shown at the beginning of the crossfade effect.
*
* Return value: %TRUE if successful, or %FALSE if the surface
* could not be copied.
**/
gboolean
gnome_bg_crossfade_set_start_surface (GnomeBGCrossfade *fade,
cairo_surface_t *surface)
{
g_return_val_if_fail (GNOME_IS_BG_CROSSFADE (fade), FALSE);
if (fade->priv->fading_surface != NULL) {
cairo_surface_destroy (fade->priv->fading_surface);
fade->priv->fading_surface = NULL;
}
fade->priv->fading_surface = tile_surface (surface,
fade->priv->width,
fade->priv->height);
return fade->priv->fading_surface != NULL;
}
static gdouble
get_current_time (void)
{
const double microseconds_per_second = (double) G_USEC_PER_SEC;
double timestamp;
GTimeVal now;
g_get_current_time (&now);
timestamp = ((microseconds_per_second * now.tv_sec) + now.tv_usec) /
microseconds_per_second;
return timestamp;
}
/**
* gnome_bg_crossfade_set_end_surface:
* @fade: a #GnomeBGCrossfade
* @surface: The cairo surface to fade to
*
* Before initiating a crossfade with gnome_bg_crossfade_start()
* a start and end surface have to be set. This function sets
* the surface shown at the end of the crossfade effect.
*
* Return value: %TRUE if successful, or %FALSE if the surface
* could not be copied.
**/
gboolean
gnome_bg_crossfade_set_end_surface (GnomeBGCrossfade *fade,
cairo_surface_t *surface)
{
g_return_val_if_fail (GNOME_IS_BG_CROSSFADE (fade), FALSE);
if (fade->priv->end_surface != NULL) {
cairo_surface_destroy (fade->priv->end_surface);
fade->priv->end_surface = NULL;
}
fade->priv->end_surface = tile_surface (surface,
fade->priv->width,
fade->priv->height);
/* Reset timer in case we're called while animating
*/
fade->priv->start_time = get_current_time ();
return fade->priv->end_surface != NULL;
}
static gboolean
animations_are_disabled (GnomeBGCrossfade *fade)
{
GtkSettings *settings;
GdkScreen *screen;
gboolean are_enabled;
g_assert (fade->priv->window != NULL);
screen = gdk_window_get_screen (fade->priv->window);
settings = gtk_settings_get_for_screen (screen);
g_object_get (settings, "gtk-enable-animations", &are_enabled, NULL);
return !are_enabled;
}
static void
send_root_property_change_notification (GnomeBGCrossfade *fade)
{
long zero_length_pixmap;
/* We do a zero length append to force a change notification,
* without changing the value */
XChangeProperty (GDK_WINDOW_XDISPLAY (fade->priv->window),
GDK_WINDOW_XID (fade->priv->window),
gdk_x11_get_xatom_by_name ("_XROOTPMAP_ID"),
XA_PIXMAP, 32, PropModeAppend,
(guchar *) &zero_length_pixmap, 0);
}
static void
draw_background (GnomeBGCrossfade *fade)
{
if (gdk_window_get_window_type (fade->priv->window) == GDK_WINDOW_ROOT) {
XClearArea (GDK_WINDOW_XDISPLAY (fade->priv->window),
GDK_WINDOW_XID (fade->priv->window),
0, 0,
gdk_window_get_width (fade->priv->window),
gdk_window_get_height (fade->priv->window),
False);
send_root_property_change_notification (fade);
gdk_flush ();
} else {
gdk_window_invalidate_rect (fade->priv->window, NULL, FALSE);
gdk_window_process_updates (fade->priv->window, FALSE);
}
}
static gboolean
on_tick (GnomeBGCrossfade *fade)
{
gdouble now, percent_done;
cairo_t *cr;
cairo_status_t status;
g_return_val_if_fail (GNOME_IS_BG_CROSSFADE (fade), FALSE);
now = get_current_time ();
percent_done = (now - fade->priv->start_time) / fade->priv->total_duration;
percent_done = CLAMP (percent_done, 0.0, 1.0);
/* If it's taking a long time to get to the first frame,
* then lengthen the duration, so the user will get to see
* the effect.
*/
if (fade->priv->is_first_frame && percent_done > .33) {
fade->priv->is_first_frame = FALSE;
fade->priv->total_duration *= 1.5;
return on_tick (fade);
}
if (fade->priv->fading_surface == NULL) {
return FALSE;
}
if (animations_are_disabled (fade)) {
return FALSE;
}
/* We accumulate the results in place for performance reasons.
*
* This means 1) The fade is exponential, not linear (looks good!)
* 2) The rate of fade is not independent of frame rate. Slower machines
* will get a slower fade (but never longer than .75 seconds), and
* even the fastest machines will get *some* fade because the framerate
* is capped.
*/
cr = cairo_create (fade->priv->fading_surface);
cairo_set_source_surface (cr, fade->priv->end_surface,
0.0, 0.0);
cairo_paint_with_alpha (cr, percent_done);
status = cairo_status (cr);
cairo_destroy (cr);
if (status == CAIRO_STATUS_SUCCESS) {
draw_background (fade);
}
return percent_done <= .99;
}
static void
on_finished (GnomeBGCrossfade *fade)
{
cairo_pattern_t *pattern;
if (fade->priv->timeout_id == 0)
return;
g_assert (fade->priv->end_surface != NULL);
pattern = cairo_pattern_create_for_surface (fade->priv->end_surface);
gdk_window_set_background_pattern (fade->priv->window, pattern);
cairo_pattern_destroy (pattern);
draw_background (fade);
cairo_surface_destroy (fade->priv->end_surface);
fade->priv->end_surface = NULL;
g_assert (fade->priv->fading_surface != NULL);
cairo_surface_destroy (fade->priv->fading_surface);
fade->priv->fading_surface = NULL;
fade->priv->timeout_id = 0;
g_signal_emit (fade, signals[FINISHED], 0, fade->priv->window);
}
/**
* gnome_bg_crossfade_start:
* @fade: a #GnomeBGCrossfade
* @window: The #GdkWindow to draw crossfade on
*
* This function initiates a quick crossfade between two surfaces on
* the background of @window. Before initiating the crossfade both
* gnome_bg_crossfade_start() and gnome_bg_crossfade_end() need to
* be called. If animations are disabled, the crossfade is skipped,
* and the window background is set immediately to the end surface.
**/
void
gnome_bg_crossfade_start (GnomeBGCrossfade *fade,
GdkWindow *window)
{
GSource *source;
GMainContext *context;
cairo_pattern_t *pattern;
g_return_if_fail (GNOME_IS_BG_CROSSFADE (fade));
g_return_if_fail (window != NULL);
g_return_if_fail (fade->priv->fading_surface != NULL);
g_return_if_fail (fade->priv->end_surface != NULL);
g_return_if_fail (!gnome_bg_crossfade_is_started (fade));
g_return_if_fail (gdk_window_get_window_type (window) != GDK_WINDOW_FOREIGN);
source = g_timeout_source_new (1000 / 60.0);
g_source_set_callback (source,
(GSourceFunc) on_tick,
fade,
(GDestroyNotify) on_finished);
context = g_main_context_default ();
fade->priv->timeout_id = g_source_attach (source, context);
g_source_unref (source);
fade->priv->window = window;
pattern = cairo_pattern_create_for_surface (fade->priv->fading_surface);
gdk_window_set_background_pattern (fade->priv->window, pattern);
cairo_pattern_destroy (pattern);
draw_background (fade);
fade->priv->is_first_frame = TRUE;
fade->priv->total_duration = .75;
fade->priv->start_time = get_current_time ();
}
/**
* gnome_bg_crossfade_is_started:
* @fade: a #GnomeBGCrossfade
*
* This function reveals whether or not @fade is currently
* running on a window. See gnome_bg_crossfade_start() for
* information on how to initiate a crossfade.
*
* Return value: %TRUE if fading, or %FALSE if not fading
**/
gboolean
gnome_bg_crossfade_is_started (GnomeBGCrossfade *fade)
{
g_return_val_if_fail (GNOME_IS_BG_CROSSFADE (fade), FALSE);
return fade->priv->timeout_id != 0;
}
/**
* gnome_bg_crossfade_stop:
* @fade: a #GnomeBGCrossfade
*
* This function stops any in progress crossfades that may be
* happening. It's harmless to call this function if @fade is
* already stopped.
**/
void
gnome_bg_crossfade_stop (GnomeBGCrossfade *fade)
{
g_return_if_fail (GNOME_IS_BG_CROSSFADE (fade));
if (!gnome_bg_crossfade_is_started (fade))
return;
g_assert (fade->priv->timeout_id != 0);
g_source_remove (fade->priv->timeout_id);
fade->priv->timeout_id = 0;
}