diff --git a/src/compositor/meta-background-actor-private.h b/src/compositor/meta-background-actor-private.h index 7b1f8b8a7..8473347cd 100644 --- a/src/compositor/meta-background-actor-private.h +++ b/src/compositor/meta-background-actor-private.h @@ -9,15 +9,39 @@ void meta_background_actor_set_visible_region (MetaBackgroundActor *self, cairo_region_t *visible_region); -GTask *meta_background_draw_async (MetaScreen *screen, - const char *picture_uri, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data); -CoglHandle meta_background_draw_finish (MetaScreen *screen, - GAsyncResult *result, - char **picture_uri, - GError **error); +/** + * MetaBackgroundSlideshow: + * + * A class for handling animated backgrounds. + */ + +#define META_TYPE_BACKGROUND_SLIDESHOW (meta_background_slideshow_get_type ()) +#define META_BACKGROUND_SLIDESHOW(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), META_TYPE_BACKGROUND_SLIDESHOW, MetaBackgroundSlideshow)) +#define META_BACKGROUND_SLIDESHOW_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), META_TYPE_BACKGROUND_SLIDESHOW, MetaBackgroundSlideshowClass)) +#define META_IS_BACKGROUND_SLIDESHOW(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), META_TYPE_BACKGROUND_SLIDESHOW)) +#define META_IS_BACKGROUND_SLIDESHOW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), META_TYPE_BACKGROUND_SLIDESHOW)) +#define META_BACKGROUND_SLIDESHOW_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), META_TYPE_BACKGROUND_SLIDESHOW, MetaBackgroundSlideshowClass)) + +typedef struct _MetaBackgroundSlideshow MetaBackgroundSlideshow; +typedef struct _MetaBackgroundSlideshowClass MetaBackgroundSlideshowClass; + +GType meta_background_slideshow_get_type (void) G_GNUC_CONST; + +MetaBackgroundSlideshow *meta_background_slideshow_new (MetaScreen *screen, + const char *picture_uri); + +const char *meta_background_slideshow_get_uri (MetaBackgroundSlideshow *slideshow); + +GTask *meta_background_slideshow_draw_async (MetaBackgroundSlideshow *slideshow, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +CoglHandle meta_background_slideshow_draw_finish (MetaBackgroundSlideshow *slideshow, + GAsyncResult *result, + GError **error); + +int meta_background_slideshow_get_next_timeout (MetaBackgroundSlideshow *slideshow); + #endif /* META_BACKGROUND_ACTOR_PRIVATE_H */ diff --git a/src/compositor/meta-background-actor.c b/src/compositor/meta-background-actor.c index 195ddf09c..1e0981da3 100644 --- a/src/compositor/meta-background-actor.c +++ b/src/compositor/meta-background-actor.c @@ -25,6 +25,8 @@ #include +#include + #include #include "cogl-utils.h" @@ -43,6 +45,7 @@ typedef struct _MetaBackground MetaBackground; typedef struct { + MetaBackgroundSlideshow *slideshow; CoglTexture *texture; float texture_width; float texture_height; @@ -52,7 +55,6 @@ typedef struct { ClutterColor colors[2]; GDesktopBackgroundStyle style; GDesktopBackgroundShading shading; - char *picture_uri; } MetaBackgroundState; struct _MetaBackground @@ -65,6 +67,8 @@ struct _MetaBackground MetaBackgroundState old_state; MetaBackgroundState state; GSettings *settings; + MetaBackgroundSlideshow *pending_slideshow; + GSource *timeout; GTask *rendering_task; }; @@ -101,16 +105,17 @@ static GParamSpec *obj_props[PROP_LAST]; G_DEFINE_TYPE (MetaBackgroundActor, meta_background_actor, CLUTTER_TYPE_ACTOR); -static void meta_background_update (MetaBackground *background, +static void meta_background_uri_changed (MetaBackground *background, const char *picture_uri); +static gboolean meta_background_update (MetaBackground *background); static void meta_background_actor_screen_size_changed (MetaScreen *screen, MetaBackgroundActor *actor); static void meta_background_actor_constructed (GObject *object); static void clear_state (MetaBackgroundState *state); -static void set_texture (MetaBackground *background, - CoglHandle texture, - const char *picture_uri); +static void set_texture (MetaBackground *background, + CoglHandle texture, + MetaBackgroundSlideshow *slideshow); static void meta_background_free (MetaBackground *background) @@ -131,11 +136,16 @@ on_settings_changed (GSettings *settings, const char *key, MetaBackground *background) { - char *picture_uri; + if (strcmp (key, "picture-uri") == 0) + { + char *picture_uri; - picture_uri = g_settings_get_string (settings, "picture-uri"); - meta_background_update (background, picture_uri); - g_free (picture_uri); + picture_uri = g_settings_get_string (settings, "picture-uri"); + meta_background_uri_changed (background, picture_uri); + g_free (picture_uri); + } + else + meta_background_update (background); } static GSettings * @@ -166,7 +176,7 @@ meta_background_get (MetaScreen *screen, g_signal_connect (settings, "changed", G_CALLBACK (on_settings_changed), background); - on_settings_changed (settings, NULL, background); + on_settings_changed (settings, "picture-uri", background); } return background; @@ -186,8 +196,7 @@ static inline void clear_state (MetaBackgroundState *state) { g_clear_pointer (&state->texture, cogl_handle_unref); - g_free (state->picture_uri); - state->picture_uri = NULL; + g_clear_object (&state->slideshow); } static void @@ -218,13 +227,14 @@ get_color_from_settings (GSettings *settings, } static void -set_texture (MetaBackground *background, - CoglHandle texture, - const char *picture_uri) +set_texture (MetaBackground *background, + CoglHandle texture, + MetaBackgroundSlideshow *slideshow) { GSList *l; gboolean crossfade; int width, height; + int timeout; g_assert (texture != COGL_INVALID_HANDLE); @@ -242,7 +252,23 @@ set_texture (MetaBackground *background, background->state.shading = g_settings_get_enum (background->settings, "color-shading-type"); background->state.texture_width = cogl_texture_get_width (background->state.texture); background->state.texture_height = cogl_texture_get_height (background->state.texture); - background->state.picture_uri = g_strdup (picture_uri); + background->state.slideshow = g_object_ref (slideshow); + + timeout = meta_background_slideshow_get_next_timeout (slideshow); + + if (background->timeout != NULL) + g_source_destroy (background->timeout); + + if (timeout > 0) + { + background->timeout = g_timeout_source_new (timeout * 1000); + g_source_set_callback (background->timeout, + (GSourceFunc) (meta_background_update), + background, NULL); + g_source_attach (background->timeout, NULL); + } + else + background->timeout = NULL; crossfade = state_is_valid (&background->old_state); @@ -987,13 +1013,14 @@ on_background_drawn (GObject *object, GAsyncResult *result, gpointer user_data) { + MetaBackgroundSlideshow *slideshow; MetaBackground *background; CoglHandle texture; GError *error; - char *picture_uri; error = NULL; - texture = meta_background_draw_finish (META_SCREEN (object), result, &picture_uri, &error); + slideshow = META_BACKGROUND_SLIDESHOW (object); + texture = meta_background_slideshow_draw_finish (slideshow, result, &error); /* Don't even access user_data if cancelled, it might be already freed */ @@ -1010,9 +1037,8 @@ on_background_drawn (GObject *object, if (texture != COGL_INVALID_HANDLE) { - set_texture (background, texture, picture_uri); + set_texture (background, texture, slideshow); cogl_handle_unref (texture); - g_free (picture_uri); return; } else @@ -1024,7 +1050,7 @@ on_background_drawn (GObject *object, } /* - * meta_background_update: + * meta_background_uri_changed: * @background: a #MetaBackground * @picture_uri: the new image URI * @@ -1032,11 +1058,15 @@ on_background_drawn (GObject *object, * a thread, and the actual on screen change is therefore delayed until * the redraw is finished. */ -void -meta_background_update (MetaBackground *background, - const char *picture_uri) +static void +meta_background_uri_changed (MetaBackground *background, + const char *picture_uri) { - if (g_strcmp0 (background->state.picture_uri, picture_uri) == 0) + MetaBackgroundSlideshow *current_slideshow; + + current_slideshow = background->pending_slideshow; + if (current_slideshow && + strcmp (meta_background_slideshow_get_uri (current_slideshow), picture_uri) == 0) return; if (background->cancellable) @@ -1046,13 +1076,38 @@ meta_background_update (MetaBackground *background, } g_clear_object (&background->rendering_task); + g_clear_object (&background->pending_slideshow); background->cancellable = g_cancellable_new (); + background->pending_slideshow = meta_background_slideshow_new (background->screen, + picture_uri); - background->rendering_task = meta_background_draw_async (background->screen, - picture_uri, - background->cancellable, - on_background_drawn, background); + background->rendering_task = meta_background_slideshow_draw_async (background->pending_slideshow, + background->cancellable, + on_background_drawn, background); +} + +static gboolean +meta_background_update (MetaBackground *background) +{ + if (background->timeout) + { + g_source_destroy (background->timeout); + background->timeout = NULL; + } + + if (background->cancellable) + { + g_cancellable_cancel (background->cancellable); + g_object_unref (background->cancellable); + } + + g_clear_object (&background->rendering_task); + background->rendering_task = meta_background_slideshow_draw_async (background->state.slideshow, + background->cancellable, + on_background_drawn, background); + + return FALSE; } /** diff --git a/src/compositor/meta-background.c b/src/compositor/meta-background.c index d427d1560..cc7de0714 100644 --- a/src/compositor/meta-background.c +++ b/src/compositor/meta-background.c @@ -23,76 +23,890 @@ #include "config.h" +#include +#include +#include + +#include +#include #include #include #include "meta-background-actor-private.h" #include +/* From and To */ +#define CACHE_SIZE 2 + +typedef struct _SizedUri { + char *picture_uri; + int width, height; + + struct _SizedUri *next; +} SizedUri; + +typedef struct { + SizedUri *from; + SizedUri *to; + + time_t starttime; + time_t endtime; +} Slide; + +typedef struct { + char *uri; + GdkPixbuf *pixbuf; + + int lru_age; +} CacheEntry; + +struct _MetaBackgroundSlideshow { + GObject parent_instance; + + /* Immutable once created */ + MetaScreen *screen; + char *picture_uri; + + GMutex cache_mutex; + CacheEntry cache[CACHE_SIZE]; + + GMutex slides_lock; + GQueue slides; + time_t total_duration; +}; + +struct _MetaBackgroundSlideshowClass { + GObjectClass parent_class; +}; + +G_DEFINE_TYPE (MetaBackgroundSlideshow, meta_background_slideshow, G_TYPE_OBJECT); + static void -meta_background_draw_thread (GTask *task, - gpointer source_object, - gpointer task_data, - GCancellable *cancellable) +slide_free (Slide *slide) +{ + SizedUri *cur, *next; + + for (cur = slide->from; cur; cur = next) + { + next = cur->next; + + g_free (cur->picture_uri); + g_slice_free (SizedUri, cur); + } + for (cur = slide->to; cur; cur = next) + { + next = cur->next; + + g_free (cur->picture_uri); + g_slice_free (SizedUri, cur); + } + + g_slice_free (Slide, slide); +} + +static void +clear_cache_entry (CacheEntry *entry) +{ + g_free (entry->uri); + entry->uri = NULL; + g_clear_object (&entry->pixbuf); +} + +static void +insert_cache (MetaBackgroundSlideshow *slideshow, + const char *pixbuf_uri, + GdkPixbuf *pixbuf) +{ + int i; + int min_age, max_age; + CacheEntry *candidate; + + g_mutex_lock (&slideshow->cache_mutex); + + candidate = NULL; + min_age = INT_MAX; + max_age = INT_MIN; + for (i = 0; i < CACHE_SIZE; i++) + { + CacheEntry *entry = &slideshow->cache[i]; + + if (entry->uri == NULL) + { + candidate = entry; + min_age = INT_MIN; + } + else if (entry->lru_age < min_age) + { + candidate = entry; + min_age = entry->lru_age; + } + + if (entry->lru_age > max_age) + max_age = entry->lru_age; + } + + g_assert (candidate != NULL); + + clear_cache_entry (candidate); + candidate->uri = g_strdup (pixbuf_uri); + candidate->pixbuf = g_object_ref (pixbuf); + candidate->lru_age = max_age + 1; + + g_mutex_unlock (&slideshow->cache_mutex); +} + +static GdkPixbuf * +hit_cache (MetaBackgroundSlideshow *slideshow, + const char *pixbuf_uri) +{ + GdkPixbuf *hit; + int i; + + g_mutex_lock (&slideshow->cache_mutex); + + hit = NULL; + + for (i = 0; i < CACHE_SIZE; i++) + { + CacheEntry *entry = &slideshow->cache[i]; + + if (entry->uri != NULL && + strcmp (entry->uri, pixbuf_uri) == 0) + { + entry->lru_age ++; + hit = g_object_ref (entry->pixbuf); + } + } + + g_mutex_unlock (&slideshow->cache_mutex); + + return hit; +} + +typedef enum { + STATE_INITIAL, + STATE_BACKGROUND, + STATE_STARTTIME, + STATE_STATIC_SLIDE, + STATE_TRANSITION_SLIDE, + STATE_FILE, + STATE_FILE_SIZE, +} ParserState; + +/* initial -> background -> transition/static -> to/from/file -> size */ +#define STATE_STACK_SIZE 5 + +typedef struct { + GQueue *slides_queue; + Slide *current_slide; + SizedUri **current_size_list; + SizedUri *current_size; + + time_t starttime; + struct tm starttime_tm; + + ParserState state_stack[STATE_STACK_SIZE]; + int state_stack_len; +} SlideshowParseContext; + +static inline ParserState +parse_context_get_state (SlideshowParseContext *parser) +{ + return parser->state_stack[parser->state_stack_len-1]; +} + +static inline void +parse_context_push_state (SlideshowParseContext *parser, + ParserState state) +{ + g_assert (parser->state_stack_len < STATE_STACK_SIZE); + + parser->state_stack[parser->state_stack_len] = state; + parser->state_stack_len++; +} + +static inline void +parse_context_pop_state (SlideshowParseContext *parser) +{ + parser->state_stack_len--; +} + +static void +slideshow_start_element (GMarkupParseContext *context, + const char *element_name, + const char **attribute_names, + const char **attribute_values, + gpointer user_data, + GError **error) +{ + SlideshowParseContext *parser = user_data; + + switch (parse_context_get_state (parser)) { + case STATE_INITIAL: + if (strcmp (element_name, "background")) + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT, + "Invalid root element %s", element_name); + else + parse_context_push_state (parser, STATE_BACKGROUND); + break; + + case STATE_BACKGROUND: + if (strcmp (element_name, "starttime") == 0) + parse_context_push_state (parser, STATE_STARTTIME); + else if (strcmp (element_name, "static") == 0 || + strcmp (element_name, "transition") == 0) + { + if (strcmp (element_name, "static") == 0) + parse_context_push_state (parser, STATE_STATIC_SLIDE); + else + parse_context_push_state (parser, STATE_TRANSITION_SLIDE); + + parser->current_slide = g_slice_new0 (Slide); + parser->current_slide->starttime = parser->starttime; + + g_queue_push_tail (parser->slides_queue, parser->current_slide); + } + else + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT, + "Invalid element %s in state ", element_name); + break; + + case STATE_STARTTIME: + if (strcmp (element_name, "year") && + strcmp (element_name, "month") && + strcmp (element_name, "day") && + strcmp (element_name, "hour") && + strcmp (element_name, "minute") && + strcmp (element_name, "second")) + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT, + "Invalid element %s in state ", element_name); + break; + + case STATE_STATIC_SLIDE: + if (strcmp (element_name, "file") == 0) + { + parse_context_push_state (parser, STATE_FILE); + + parser->current_size_list = &parser->current_slide->from; + } + else if (strcmp (element_name, "duration")) + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT, + "Invalid element %s in state ", element_name); + break; + + case STATE_TRANSITION_SLIDE: + if (strcmp (element_name, "from") == 0 || + strcmp (element_name, "to") == 0) + { + parse_context_push_state (parser, STATE_FILE); + + if (strcmp (element_name, "from") == 0) + parser->current_size_list = &parser->current_slide->from; + else + parser->current_size_list = &parser->current_slide->to; + } + else if (strcmp (element_name, "duration")) + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT, + "Invalid element %s in state ", element_name); + break; + + case STATE_FILE: + if (strcmp (element_name, "size") == 0) + { + const char *width, *height; + + if (g_markup_collect_attributes (element_name, + attribute_names, + attribute_values, + error, + G_MARKUP_COLLECT_STRING, + "width", &width, + G_MARKUP_COLLECT_STRING, + "height", &height, + G_MARKUP_COLLECT_INVALID)) + { + SizedUri *new_size = g_slice_new (SizedUri); + + new_size->picture_uri = NULL; + new_size->width = atoi(width); + new_size->height = atoi(height); + new_size->next = NULL; + + if (parser->current_size == NULL) + { + *parser->current_size_list = new_size; + parser->current_size = new_size; + } + else + parser->current_size->next = new_size; + + parse_context_push_state (parser, STATE_FILE_SIZE); + } + } + else + { + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT, + "Invalid element %s in state ", element_name); + } + break; + + case STATE_FILE_SIZE: + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT, + "Invalid element %s in state ", element_name); + break; + } +}; + +static void +slideshow_end_element (GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + SlideshowParseContext *parser = user_data; + + switch (parse_context_get_state (parser)) { + case STATE_STARTTIME: + if (strcmp (element_name, "starttime") == 0) + { + parser->starttime = mktime (&parser->starttime_tm); + parse_context_pop_state (parser); + } + break; + + case STATE_STATIC_SLIDE: + case STATE_TRANSITION_SLIDE: + if (strcmp (element_name, "duration")) + { + parse_context_pop_state (parser); + parser->current_slide = NULL; + parser->current_size_list = NULL; + } + break; + + case STATE_FILE: + parse_context_pop_state (parser); + parser->current_size = NULL; + break; + + case STATE_FILE_SIZE: + case STATE_BACKGROUND: + parse_context_pop_state (parser); + break; + + case STATE_INITIAL: + g_assert_not_reached (); + } +} + +static int +strntoi(const char *text, size_t text_len) +{ + char *v; + int i; + + v = g_strndup (text, text_len); + i = atoi(v); + + g_free (v); + return i; +} + +static gboolean +is_all_white (const char *text, size_t text_len) +{ + size_t i; + + for (i = 0; i < text_len; i++) + if (!g_ascii_isspace(text[i])) + return FALSE; + + return TRUE; +} + +static void +slideshow_text (GMarkupParseContext *context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + SlideshowParseContext *parser = user_data; + const char *current_element = g_markup_parse_context_get_element (context); + + switch (parse_context_get_state (parser)) { + case STATE_STARTTIME: + if (strcmp (current_element, "year") == 0) + parser->starttime_tm.tm_year = strntoi (text, text_len) - 1900; + else if (strcmp (current_element, "month") == 0) + parser->starttime_tm.tm_mon = strntoi (text, text_len) - 1; + else if (strcmp (current_element, "day") == 0) + parser->starttime_tm.tm_mday = strntoi (text, text_len); + else if (strcmp (current_element, "hour") == 0) + parser->starttime_tm.tm_hour = strntoi (text, text_len); + else if (strcmp (current_element, "minute") == 0) + parser->starttime_tm.tm_min = strntoi (text, text_len); + else if (strcmp (current_element, "second") == 0) + parser->starttime_tm.tm_sec = strntoi (text, text_len); + else if (!is_all_white (text, text_len)) + g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, + "Unexpected content in element <%s>", current_element); + break; + + case STATE_STATIC_SLIDE: + case STATE_TRANSITION_SLIDE: + if (strcmp (current_element, "duration") == 0) + { + /* Values are floating point in the XML, but we only + handle integers. + Also, some XML files represent infinity as a very high value, + so we don't want to overflow reading. + */ + char *value = g_strndup (text, text_len); + long long duration = g_ascii_strtod (value, NULL); + + /* We use milliseconds for timeouts, so anything greater than that + is impossible (and means infinite) + */ + if ((duration * 1000) > (long long)INT_MAX) + { + parser->current_slide->endtime = parser->starttime = -1; + } + else + { + parser->current_slide->endtime = parser->current_slide->starttime + (int)duration; + parser->starttime = parser->current_slide->endtime; + } + + g_free (value); + } + else if (!is_all_white (text, text_len)) + { + g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, + "Unexpected content in element <%s>", current_element); + } + break; + + case STATE_FILE: + { + if (parser->current_size != NULL && + !is_all_white (text, text_len)) + { + g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, + "Unexpected content in element <%s>", current_element); + break; + } + else + { + SizedUri *new_size = g_slice_new (SizedUri); + + new_size->picture_uri = NULL; + new_size->width = -1; + new_size->height = -1; + new_size->next = NULL; + + *parser->current_size_list = new_size; + parser->current_size = new_size; + } + } + /* Fall through */ + + case STATE_FILE_SIZE: + parser->current_size->picture_uri = g_strdup_printf("file://%.*s", (int)text_len, text); + break; + + case STATE_INITIAL: + case STATE_BACKGROUND: + if (!is_all_white (text, text_len)) + g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, + "Unexpected content in element <%s>", current_element); + } +} + +static GMarkupParser slideshow_parser = { + slideshow_start_element, + slideshow_end_element, + slideshow_text, + NULL, + NULL, +}; + +/* Based on the default size for GBufferedInputStream */ +#define BUFFER_SIZE 4096 + +static gboolean +parse_slideshow (MetaBackgroundSlideshow *slideshow, + GInputStream *stream, + GCancellable *cancellable, + GError **error) +{ + SlideshowParseContext parser = { + NULL, NULL, NULL, NULL, 0, { 0 }, { STATE_INITIAL }, 1 + }; + GMarkupParseContext *context; + char buffer[BUFFER_SIZE]; + GError *local_error; + gsize read; + GList *iter; + time_t total_duration; + + parser.slides_queue = &slideshow->slides; + parser.starttime_tm.tm_isdst = -1; + + if (!g_seekable_seek (G_SEEKABLE (stream), 0, G_SEEK_SET, cancellable, error)) + return FALSE; + + context = g_markup_parse_context_new (&slideshow_parser, + G_MARKUP_TREAT_CDATA_AS_TEXT | G_MARKUP_PREFIX_ERROR_POSITION, + &parser, NULL); + + local_error = NULL; + + do + { + g_input_stream_read_all (stream, buffer, sizeof(buffer), + &read, cancellable, error); + } + while (read > 0 && + g_markup_parse_context_parse (context, buffer, read, &local_error)); + + g_markup_parse_context_free (context); + + if (read > 0) + { + g_message ("Failed to parse slideshow file: %s", local_error->message); + g_set_error (error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_UNKNOWN_TYPE, + _("File format not recognized")); + + g_clear_error (&local_error); + g_queue_foreach (&slideshow->slides, (GFunc) slide_free, NULL); + g_queue_clear (&slideshow->slides); + return FALSE; + } + + total_duration = 0; + + for (iter = slideshow->slides.head; iter; iter = iter->next) + { + Slide *slide = iter->data; + + if (slide->endtime < 0) + { + slideshow->total_duration = -1; + return TRUE; + } + + total_duration += slide->endtime - slide->starttime; + } + + slideshow->total_duration = total_duration; + return TRUE; +} + +static Slide * +make_single_pixbuf_slide (const char *picture_uri, + GdkPixbuf *pixbuf) +{ + Slide *slide; + SizedUri *uri; + + slide = g_slice_new (Slide); + uri = g_slice_new (SizedUri); + + uri->picture_uri = g_strdup (picture_uri); + uri->width = gdk_pixbuf_get_width (pixbuf); + uri->height = gdk_pixbuf_get_height (pixbuf); + uri->next = NULL; + slide->from = uri; + slide->to = NULL; + slide->starttime = -1; + slide->endtime = -1; + + return slide; +} + +static gboolean +ensure_slideshow (MetaBackgroundSlideshow *slideshow, + GCancellable *cancellable, + GError **error) { GFile *file; GInputStream *stream; GdkPixbuf *pixbuf; - GError *error; + GError *local_error; + gboolean ok; - /* TODO: handle slideshows and other complications */ + if (!g_queue_is_empty (&slideshow->slides)) + return TRUE; - file = g_file_new_for_uri (task_data); + pixbuf = hit_cache (slideshow, slideshow->picture_uri); + if (pixbuf != NULL) + { + g_queue_push_tail (&slideshow->slides, + make_single_pixbuf_slide (slideshow->picture_uri, pixbuf)); + slideshow->total_duration = -1; + return TRUE; + } - error = NULL; - stream = G_INPUT_STREAM (g_file_read (file, cancellable, &error)); + file = g_file_new_for_uri (slideshow->picture_uri); + + stream = G_INPUT_STREAM (g_file_read (file, cancellable, error)); if (stream == NULL) { g_object_unref (file); - g_task_return_error (task, error); - return; + return FALSE; } - pixbuf = gdk_pixbuf_new_from_stream (stream, cancellable, &error); - if (pixbuf == NULL) + local_error = NULL; + pixbuf = gdk_pixbuf_new_from_stream (stream, cancellable, &local_error); + + if (pixbuf != NULL) { - g_object_unref (file); - g_object_unref (stream); - g_task_return_error (task, error); + g_queue_push_tail (&slideshow->slides, + make_single_pixbuf_slide (slideshow->picture_uri, pixbuf)); + insert_cache (slideshow, slideshow->picture_uri, pixbuf); + slideshow->total_duration = -1; + + ok = TRUE; + } + else if (g_error_matches (local_error, GDK_PIXBUF_ERROR, + GDK_PIXBUF_ERROR_UNKNOWN_TYPE)) + { + g_clear_error (&local_error); + ok = parse_slideshow (slideshow, stream, cancellable, error); + } + else + { + g_propagate_error (error, local_error); + + ok = FALSE; } - g_task_return_pointer (task, pixbuf, g_object_unref); + if (pixbuf) + g_object_unref (pixbuf); g_object_unref (file); g_object_unref (stream); + + return ok; +} + +static Slide * +find_current_slide (MetaBackgroundSlideshow *slideshow, + time_t current_time) +{ + Slide *first_slide; + GList *iter; + + g_assert (slideshow->slides.head != NULL); + + first_slide = slideshow->slides.head->data; + + if (slideshow->total_duration < 0) + return first_slide; + + g_assert (first_slide->starttime >= 0); + + /* Account for loops, looking for the difference from the + starttime indicated in the XML */ + current_time = first_slide->starttime + + (current_time - first_slide->starttime) % slideshow->total_duration; + + for (iter = slideshow->slides.head; iter; iter = iter->next) + { + Slide *slide = iter->data; + + g_assert (slide->endtime >= 0); + if (current_time >= slide->starttime && + current_time < slide->endtime) + return slide; + } + + g_assert_not_reached (); +} + +/* + * Find the FileSize that best matches the given size. + * Do two passes; the first pass only considers FileSizes + * that are larger than the given size. + * We are looking for the image that best matches the aspect ratio. + * When two images have the same aspect ratio, prefer the one whose + * width is closer to the given width. + * + * Shamelessly taken from gnome-bg.c + */ +static SizedUri * +find_best_size (SizedUri *sizes, gint width, gint height) +{ + SizedUri *s, *best; + gdouble a, d, distance; + gint pass; + + a = width/(gdouble)height; + distance = 10000.0; + best = NULL; + + for (pass = 0; pass < 2; pass++) + { + for (s = sizes; s; s = s->next) + { + if (pass == 0 && (s->width < width || s->height < height)) + continue; + + d = fabs (a - s->width/(gdouble)s->height); + if (d < distance) + { + distance = d; + best = s; + } + else if (d == distance) + { + if (abs (s->width - width) < abs (best->width - width)) + { + best = s; + } + } + } + + if (best) + break; + } + + return best; +} + +static GdkPixbuf * +load_best_pixbuf (MetaBackgroundSlideshow *slideshow, + SizedUri *size, + GCancellable *cancellable, + GError **error) +{ + GFile *file; + GInputStream *stream; + GdkPixbuf *pixbuf; + int width, height; + + meta_screen_get_size (slideshow->screen, &width, &height); + size = find_best_size (size, width, height); + pixbuf = hit_cache (slideshow, size->picture_uri); + + if (pixbuf) + return pixbuf; + + file = g_file_new_for_uri (size->picture_uri); + + stream = G_INPUT_STREAM (g_file_read (file, cancellable, error)); + if (stream == NULL) + { + g_object_unref (file); + return NULL; + } + + pixbuf = gdk_pixbuf_new_from_stream (stream, cancellable, error); + + if (pixbuf) + insert_cache (slideshow, size->picture_uri, pixbuf); + + g_object_unref (file); + g_object_unref (stream); + return pixbuf; +} + +static void +meta_background_slideshow_draw_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + MetaBackgroundSlideshow *self = source_object; + GError *error; + Slide *slide; + time_t current_time; + + g_mutex_lock (&self->slides_lock); + + error = NULL; + if (!ensure_slideshow (self, cancellable, &error)) + goto error; + + current_time = time (NULL); + slide = find_current_slide (self, current_time); + + if (slide->to != NULL) + { + GdkPixbuf *from, *to, *blended; + int transition_steps, current_step; + Slide *first_slide = self->slides.head->data; + + from = load_best_pixbuf (self, slide->from, cancellable, &error); + if (!from) + goto error; + + to = load_best_pixbuf (self, slide->to, cancellable, &error); + if (!to) + goto error; + + g_mutex_unlock (&self->slides_lock); + + /* Round to five minute granularity */ + current_time = first_slide->starttime + + (current_time - first_slide->starttime) % self->total_duration; + transition_steps = (slide->endtime - slide->starttime) / 300 * 300; + current_step = (current_time - slide->starttime) / 300 * 300; + + blended = gdk_pixbuf_copy (from); + gdk_pixbuf_composite (to, blended, + 0, 0, + gdk_pixbuf_get_width (blended), + gdk_pixbuf_get_height (blended), + 0.0, 0.0, 1.0, 1.0, + GDK_INTERP_BILINEAR, + (255 * ((double)current_step/transition_steps)) + 0.5); + + g_object_unref (from); + g_object_unref (to); + g_task_return_pointer (task, blended, g_object_unref); + } + else + { + GdkPixbuf *static_pixbuf; + + static_pixbuf = load_best_pixbuf (self, slide->from, cancellable, &error); + if (!static_pixbuf) + goto error; + + g_mutex_unlock (&self->slides_lock); + g_task_return_pointer (task, static_pixbuf, g_object_unref); + } + + return; + + error: + g_task_return_error (task, error); + g_mutex_unlock (&self->slides_lock); } GTask * -meta_background_draw_async (MetaScreen *screen, - const char *picture_uri, - GCancellable *cancellable, - GAsyncReadyCallback callback, - gpointer user_data) +meta_background_slideshow_draw_async (MetaBackgroundSlideshow *slideshow, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) { GTask *task; - g_return_val_if_fail (META_IS_SCREEN (screen), NULL); - - task = g_task_new (screen, cancellable, callback, user_data); - g_task_set_source_tag (task, meta_background_draw_async); + task = g_task_new (slideshow, cancellable, callback, user_data); + g_task_set_source_tag (task, meta_background_slideshow_draw_async); g_task_set_return_on_cancel (task, TRUE); g_task_set_check_cancellable (task, TRUE); - g_task_set_task_data (task, g_strdup (picture_uri), g_free); - g_task_run_in_thread (task, meta_background_draw_thread); + g_task_run_in_thread (task, meta_background_slideshow_draw_thread); return task; } CoglHandle -meta_background_draw_finish (MetaScreen *screen, - GAsyncResult *result, - char **picture_uri, - GError **error) +meta_background_slideshow_draw_finish (MetaBackgroundSlideshow *slideshow, + GAsyncResult *result, + GError **error) { GdkPixbuf *pixbuf; CoglHandle handle; @@ -101,9 +915,6 @@ meta_background_draw_finish (MetaScreen *screen, if (pixbuf == NULL) return COGL_INVALID_HANDLE; - if (picture_uri != NULL) - *picture_uri = g_strdup (g_task_get_task_data (G_TASK (result))); - handle = cogl_texture_new_from_data (gdk_pixbuf_get_width (pixbuf), gdk_pixbuf_get_height (pixbuf), COGL_TEXTURE_NO_ATLAS | COGL_TEXTURE_NO_SLICING, @@ -115,3 +926,80 @@ meta_background_draw_finish (MetaScreen *screen, g_object_unref (pixbuf); return handle; } + +static void +meta_background_slideshow_init (MetaBackgroundSlideshow *self) +{ + g_mutex_init (&self->cache_mutex); + g_mutex_init (&self->slides_lock); +} + +static void +meta_background_slideshow_finalize (GObject *object) +{ + MetaBackgroundSlideshow *self = META_BACKGROUND_SLIDESHOW (object); + int i = 0; + + for (i = 0; i < CACHE_SIZE; i++) + clear_cache_entry (&self->cache[i]); + + g_queue_foreach (&self->slides, (GFunc) slide_free, NULL); + g_queue_clear (&self->slides); + + g_free (self->picture_uri); +} + +static void +meta_background_slideshow_class_init (MetaBackgroundSlideshowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = meta_background_slideshow_finalize; +} + +MetaBackgroundSlideshow * +meta_background_slideshow_new (MetaScreen *screen, + const char *picture_uri) +{ + MetaBackgroundSlideshow *self; + + self = g_object_new (META_TYPE_BACKGROUND_SLIDESHOW, NULL); + + self->screen = screen; + self->picture_uri = g_strdup (picture_uri); + + return self; +} + +const char * +meta_background_slideshow_get_uri (MetaBackgroundSlideshow *self) +{ + return self->picture_uri; +} + +int +meta_background_slideshow_get_next_timeout (MetaBackgroundSlideshow *slideshow) +{ + time_t current_time; + Slide *current_slide; + int retval; + + g_mutex_lock (&slideshow->slides_lock); + + g_assert (!g_queue_is_empty (&slideshow->slides)); + + current_time = time(NULL); + current_slide = find_current_slide (slideshow, current_time); + + if (current_slide->endtime < 0) + retval = -1; + else if (current_slide->to != NULL) + /* Translition slides have five minute granularity */ + retval = 300; + else + retval = current_slide->endtime - current_time; + + g_mutex_unlock (&slideshow->slides_lock); + + return retval; +}