diff --git a/js/misc/docInfo.js b/js/misc/docInfo.js index c590dccea..3f97a1924 100644 --- a/js/misc/docInfo.js +++ b/js/misc/docInfo.js @@ -5,6 +5,8 @@ const Gio = imports.gi.Gio; const Gtk = imports.gi.Gtk; const Shell = imports.gi.Shell; +const Lang = imports.lang; +const Signals = imports.signals; const Main = imports.ui.main; const THUMBNAIL_ICON_MARGIN = 2; @@ -22,34 +24,19 @@ DocInfo.prototype = { }, createIcon : function(size) { - let icon = new Clutter.Texture(); - let iconPixbuf; + let icon = null; + let defaultPixbuf = this._recentInfo.get_icon(size); - if (this.uri.match("^file://")) - iconPixbuf = Shell.get_thumbnail(this.uri, this.mimeType); - - if (iconPixbuf) { - // We calculate the width and height of the texture so as - // to preserve the aspect ratio of the thumbnail. Because - // the images generated based on thumbnails don't have an - // internal padding like system icons do, we create a - // slightly smaller texture and then create a group around - // it for padding purposes - - let scalingFactor = (size - THUMBNAIL_ICON_MARGIN * 2) / Math.max(iconPixbuf.get_width(), iconPixbuf.get_height()); - icon.set_width(Math.ceil(iconPixbuf.get_width() * scalingFactor)); - icon.set_height(Math.ceil(iconPixbuf.get_height() * scalingFactor)); - Shell.clutter_texture_set_from_pixbuf(icon, iconPixbuf); - - let group = new Clutter.Group({ width: size, - height: size }); - group.add_actor(icon); - icon.set_position(THUMBNAIL_ICON_MARGIN, THUMBNAIL_ICON_MARGIN); - return group; - } else { - Shell.clutter_texture_set_from_pixbuf(icon, this._recentInfo.get_icon(size)); - return icon; + if (this.uri.match("^file://")) { + icon = Shell.TextureCache.get_default().load_thumbnail(size, this.uri, this.mimeType, + defaultPixbuf); } + + if (!icon) { + icon = new Clutter.Texture({}); + Shell.clutter_texture_set_from_pixbuf(icon, defaultPixbuf); + } + return icon; }, launch : function() { @@ -112,3 +99,58 @@ DocInfo.prototype = { return this._recentInfo.get_modified(); } }; + +var docManagerInstance = null; + +function getDocManager(size) { + if (docManagerInstance == null) + docManagerInstance = new DocManager(size); + return docManagerInstance; +} + +function DocManager(size) { + this._init(size); +} + +DocManager.prototype = { + _init: function(iconSize) { + this._iconSize = iconSize; + this._recentManager = Gtk.RecentManager.get_default(); + this._items = {}; + this._recentManager.connect('changed', Lang.bind(this, function(recentManager) { + this._reload(); + this.emit('changed'); + })); + this._reload(); + }, + + _reload: function() { + let docs = this._recentManager.get_items(); + let newItems = {}; + for (let i = 0; i < docs.length; i++) { + let recentInfo = docs[i]; + let docInfo = new DocInfo(recentInfo); + + // we use GtkRecentInfo URI as an item Id + newItems[docInfo.uri] = docInfo; + } + let deleted = {}; + for (var uri in this._items) { + if (!(uri in newItems)) + deleted[uri] = 1; + } + /* If we'd cached any thumbnail references that no longer exist, + dump them here */ + let texCache = Shell.TextureCache.get_default(); + for (var uri in deleted) { + texCache.unref_thumbnail(this._iconSize, uri); + } + this._items = newItems; + }, + + getItems: function() { + return this._items; + } +} + +Signals.addSignalMethods(DocManager.prototype); diff --git a/js/ui/docDisplay.js b/js/ui/docDisplay.js index 5dd10fa4c..a05c036a3 100644 --- a/js/ui/docDisplay.js +++ b/js/ui/docDisplay.js @@ -77,9 +77,10 @@ DocDisplay.prototype = { _init : function(width) { GenericDisplay.GenericDisplay.prototype._init.call(this, width); let me = this; - this._recentManager = Gtk.RecentManager.get_default(); + + this._docManager = DocInfo.getDocManager(GenericDisplay.ITEM_DISPLAY_ICON_SIZE); this._docsStale = true; - this._recentManager.connect('changed', function(recentManager, userData) { + this._docManager.connect('changed', function(mgr, userData) { me._docsStale = true; // Changes in local recent files should not happen when we are in the overlay mode, // but redisplaying right away is cool when we use Zephyr. @@ -96,15 +97,7 @@ DocDisplay.prototype = { let me = this; if (!this._docsStale) return; - this._allItems = {}; - let docs = this._recentManager.get_items(); - for (let i = 0; i < docs.length; i++) { - let recentInfo = docs[i]; - let docInfo = new DocInfo.DocInfo(recentInfo); - - // we use GtkRecentInfo URI as an item Id - this._allItems[docInfo.uri] = docInfo; - } + this._allItems = this._docManager.getItems(); this._docsStale = false; }, diff --git a/src/shell-global.c b/src/shell-global.c index 2c8a06808..6a803b5d8 100644 --- a/src/shell-global.c +++ b/src/shell-global.c @@ -14,7 +14,6 @@ #include #include #include -#include #include #include #include @@ -339,73 +338,6 @@ shell_clutter_texture_set_from_pixbuf (ClutterTexture *texture, 0, NULL); } -static GnomeThumbnailFactory *thumbnail_factory; - -/** - * shell_get_thumbnail: - * - * @uri: URI of the file to thumbnail - * - * @mime_type: Mime-Type of the file to thumbnail - * - * Return value: #GdkPixbuf containing a thumbnail for file @uri - * if the thumbnail exists or can be generated, %NULL otherwise - */ -GdkPixbuf * -shell_get_thumbnail(const gchar *uri, - const gchar *mime_type) -{ - char *existing_thumbnail; - GdkPixbuf *pixbuf = NULL; - GError *error = NULL; - GFile *file = NULL; - GFileInfo *file_info = NULL; - GTimeVal mtime_g; - time_t mtime = 0; - - file = g_file_new_for_uri (uri); - file_info = g_file_query_info (file, G_FILE_ATTRIBUTE_TIME_MODIFIED, G_FILE_QUERY_INFO_NONE, NULL, NULL); - g_object_unref (file); - if (file_info) { - g_file_info_get_modification_time (file_info, &mtime_g); - g_object_unref (file_info); - mtime = (time_t) mtime_g.tv_sec; - } - - if (thumbnail_factory == NULL) - thumbnail_factory = gnome_thumbnail_factory_new (GNOME_THUMBNAIL_SIZE_NORMAL); - - existing_thumbnail = gnome_thumbnail_factory_lookup (thumbnail_factory, uri, mtime); - - if (existing_thumbnail != NULL) - { - pixbuf = gdk_pixbuf_new_from_file(existing_thumbnail, &error); - if (error != NULL) - { - g_warning("Could not generate a pixbuf from file %s: %s", existing_thumbnail, error->message); - g_clear_error (&error); - } - } - else if (gnome_thumbnail_factory_has_valid_failed_thumbnail (thumbnail_factory, uri, mtime)) - return NULL; - else if (gnome_thumbnail_factory_can_thumbnail (thumbnail_factory, uri, mime_type, mtime)) - { - pixbuf = gnome_thumbnail_factory_generate_thumbnail (thumbnail_factory, uri, mime_type); - if (pixbuf) - { - // we need to save the thumbnail so that we don't need to generate it again in the future - gnome_thumbnail_factory_save_thumbnail (thumbnail_factory, pixbuf, uri, mtime); - } - else - { - g_warning ("Could not generate thumbnail for %s", uri); - gnome_thumbnail_factory_create_failed_thumbnail (thumbnail_factory, uri, mtime); - } - } - - return pixbuf; -} - /** * shell_get_event_key_symbol: * diff --git a/src/shell-global.h b/src/shell-global.h index 26179c1c6..e7dce9559 100644 --- a/src/shell-global.h +++ b/src/shell-global.h @@ -36,8 +36,6 @@ GType shell_global_get_type (void) G_GNUC_CONST; gboolean shell_clutter_texture_set_from_pixbuf (ClutterTexture *texture, GdkPixbuf *pixbuf); -GdkPixbuf *shell_get_thumbnail(const gchar *uri, const gchar *mime_type); - guint16 shell_get_event_key_symbol(ClutterEvent *event); guint16 shell_get_button_event_click_count(ClutterEvent *event); diff --git a/src/shell-texture-cache.c b/src/shell-texture-cache.c index eaf9b36da..4a4c9ab5d 100644 --- a/src/shell-texture-cache.c +++ b/src/shell-texture-cache.c @@ -1,16 +1,20 @@ #include "shell-texture-cache.h" #include "shell-global.h" #include +#include +#include typedef struct { GIcon *icon; + gchar *thumbnail_uri; guint size; } CacheKey; struct _ShellTextureCachePrivate { GHashTable *gicon_cache; /* CacheKey -> CoglTexture* */ + GnomeThumbnailFactory *thumbnails; }; static void shell_texture_cache_dispose (GObject *object); @@ -25,6 +29,8 @@ cache_key_hash (gconstpointer a) if (akey->icon) return g_icon_hash (akey->icon) + 31*akey->size; + else if (akey->thumbnail_uri) + return g_str_hash (akey->thumbnail_uri) + 31*akey->size; g_assert_not_reached (); } @@ -39,7 +45,10 @@ cache_key_equal (gconstpointer a, return FALSE; if (akey->icon && bkey->icon) return g_icon_equal (akey->icon, bkey->icon); - g_assert_not_reached (); + else if (akey->thumbnail_uri && bkey->thumbnail_uri) + return strcmp (akey->thumbnail_uri, bkey->thumbnail_uri) == 0; + + return FALSE; } static void @@ -48,6 +57,8 @@ cache_key_destroy (gpointer a) CacheKey *akey = (CacheKey*)a; if (akey->icon) g_object_unref (akey->icon); + if (akey->thumbnail_uri) + g_free (akey->thumbnail_uri); g_free (akey); } @@ -66,6 +77,7 @@ shell_texture_cache_init (ShellTextureCache *self) self->priv = g_new0 (ShellTextureCachePrivate, 1); self->priv->gicon_cache = g_hash_table_new_full (cache_key_hash, cache_key_equal, cache_key_destroy, cogl_handle_unref); + self->priv->thumbnails = gnome_thumbnail_factory_new (GNOME_THUMBNAIL_SIZE_NORMAL); } static void @@ -77,6 +89,10 @@ shell_texture_cache_dispose (GObject *object) g_hash_table_destroy (self->priv->gicon_cache); self->priv->gicon_cache = NULL; + if (self->priv->thumbnails) + g_object_unref (self->priv->thumbnails); + self->priv->thumbnails = NULL; + G_OBJECT_CLASS (shell_texture_cache_parent_class)->dispose (object); } @@ -94,7 +110,11 @@ shell_texture_cache_new () } typedef struct { + ShellTextureCache *cache; char *uri; + char *mimetype; + GdkPixbuf *fallback_pixbuf; + gboolean thumbnail; GIcon *icon; GtkIconInfo *icon_info; gint width; @@ -102,7 +122,6 @@ typedef struct { gpointer user_data; } AsyncIconLookupData; - static gboolean compute_pixbuf_scale (gint width, gint height, @@ -200,6 +219,10 @@ icon_lookup_data_destroy (gpointer p) } else if (data->uri) g_free (data->uri); + if (data->mimetype) + g_free (data->mimetype); + if (data->fallback_pixbuf) + g_object_unref (data->fallback_pixbuf); g_free (data); } @@ -328,6 +351,56 @@ out: return rotated_pixbuf; } +static GdkPixbuf * +impl_load_thumbnail (ShellTextureCache *cache, + const char *uri, + const char *mime_type, + guint size, + GError **error) +{ + GnomeThumbnailFactory *thumbnail_factory; + GdkPixbuf *pixbuf = NULL; + GFile *file; + GFileInfo *file_info; + GTimeVal mtime_g; + time_t mtime = 0; + char *existing_thumbnail; + + file = g_file_new_for_uri (uri); + file_info = g_file_query_info (file, G_FILE_ATTRIBUTE_TIME_MODIFIED, G_FILE_QUERY_INFO_NONE, NULL, NULL); + g_object_unref (file); + if (file_info) + { + g_file_info_get_modification_time (file_info, &mtime_g); + g_object_unref (file_info); + mtime = (time_t) mtime_g.tv_sec; + } + + thumbnail_factory = cache->priv->thumbnails; + + existing_thumbnail = gnome_thumbnail_factory_lookup (thumbnail_factory, uri, mtime); + + if (existing_thumbnail != NULL) + pixbuf = gdk_pixbuf_new_from_file_at_size (existing_thumbnail, size, size, error); + else if (gnome_thumbnail_factory_has_valid_failed_thumbnail (thumbnail_factory, uri, mtime)) + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Has failed thumbnail"); + else if (gnome_thumbnail_factory_can_thumbnail (thumbnail_factory, uri, mime_type, mtime)) + { + pixbuf = gnome_thumbnail_factory_generate_thumbnail (thumbnail_factory, uri, mime_type); + if (pixbuf) + { + // we need to save the thumbnail so that we don't need to generate it again in the future + gnome_thumbnail_factory_save_thumbnail (thumbnail_factory, pixbuf, uri, mtime); + } + else + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Failed to generate thumbnail"); + gnome_thumbnail_factory_create_failed_thumbnail (thumbnail_factory, uri, mtime); + } + } + return pixbuf; +} + static void load_pixbuf_thread (GSimpleAsyncResult *result, GObject *object, @@ -337,9 +410,19 @@ load_pixbuf_thread (GSimpleAsyncResult *result, AsyncIconLookupData *data; GError *error = NULL; - data = g_object_get_data (G_OBJECT (result), "load_icon_pixbuf_async"); + data = g_object_get_data (G_OBJECT (result), "load_pixbuf_async"); + g_assert (data != NULL); - if (data->uri) + if (data->thumbnail) + { + pixbuf = impl_load_thumbnail (data->cache, data->uri, data->mimetype, data->width, &error); + if (!pixbuf && data->fallback_pixbuf) + { + g_clear_error (&error); + pixbuf = g_object_ref (data->fallback_pixbuf); + } + } + else if (data->uri) pixbuf = impl_load_pixbuf_file (data->uri, data->width, data->height, &error); else if (data->icon) pixbuf = impl_load_pixbuf_gicon (data->icon, data->icon_info, data->width, &error); @@ -375,6 +458,7 @@ load_icon_pixbuf_async (ShellTextureCache *cache, AsyncIconLookupData *data; data = g_new0 (AsyncIconLookupData, 1); + data->cache = cache; data->icon = g_object_ref (icon); data->icon_info = gtk_icon_info_copy (icon_info); data->width = data->height = size; @@ -382,7 +466,7 @@ load_icon_pixbuf_async (ShellTextureCache *cache, result = g_simple_async_result_new (G_OBJECT (cache), callback, user_data, load_icon_pixbuf_async); - g_object_set_data_full (G_OBJECT (result), "load_icon_pixbuf_async", data, icon_lookup_data_destroy); + g_object_set_data_full (G_OBJECT (result), "load_pixbuf_async", data, icon_lookup_data_destroy); g_simple_async_result_run_in_thread (result, load_pixbuf_thread, G_PRIORITY_DEFAULT, cancellable); g_object_unref (result); @@ -401,6 +485,7 @@ load_uri_pixbuf_async (ShellTextureCache *cache, AsyncIconLookupData *data; data = g_new0 (AsyncIconLookupData, 1); + data->cache = cache; data->uri = g_strdup (uri); data->width = width; data->height = height; @@ -408,7 +493,38 @@ load_uri_pixbuf_async (ShellTextureCache *cache, result = g_simple_async_result_new (G_OBJECT (cache), callback, user_data, load_uri_pixbuf_async); - g_object_set_data_full (G_OBJECT (result), "load_uri_pixbuf_async", data, icon_lookup_data_destroy); + g_object_set_data_full (G_OBJECT (result), "load_pixbuf_async", data, icon_lookup_data_destroy); + g_simple_async_result_run_in_thread (result, load_pixbuf_thread, G_PRIORITY_DEFAULT, cancellable); + + g_object_unref (result); +} + +static void +load_thumbnail_async (ShellTextureCache *cache, + const char *uri, + const char *mimetype, + guint size, + GdkPixbuf *fallback, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GSimpleAsyncResult *result; + AsyncIconLookupData *data; + + data = g_new0 (AsyncIconLookupData, 1); + data->cache = cache; + data->uri = g_strdup (uri); + data->mimetype = g_strdup (mimetype); + data->fallback_pixbuf = g_object_ref (fallback); + data->thumbnail = TRUE; + data->width = size; + data->height = size; + data->user_data = user_data; + + result = g_simple_async_result_new (G_OBJECT (cache), callback, user_data, load_thumbnail_async); + + g_object_set_data_full (G_OBJECT (result), "load_pixbuf_async", data, icon_lookup_data_destroy); g_simple_async_result_run_in_thread (result, load_pixbuf_thread, G_PRIORITY_DEFAULT, cancellable); g_object_unref (result); @@ -425,6 +541,8 @@ load_pixbuf_async_finish (ShellTextureCache *cache, GAsyncResult *result, GError typedef struct { char *uri; + gboolean thumbnail; + char *mimetype; GIcon *icon; GtkIconInfo *icon_info; guint width; @@ -469,12 +587,15 @@ on_pixbuf_loaded (GObject *source, g_object_unref (pixbuf); - if (data->icon) + if (data->icon || data->thumbnail) { gpointer orig_key, value; key = g_new0 (CacheKey, 1); - key->icon = g_object_ref (data->icon); + if (data->icon) + key->icon = g_object_ref (data->icon); + else + key->thumbnail_uri = g_strdup (data->uri); key->size = data->width; if (!g_hash_table_lookup_extended (cache->priv->gicon_cache, key, @@ -495,10 +616,15 @@ out: } else if (data->uri) g_free (data->uri); + + if (data->mimetype) + g_free (data->mimetype); + /* Alternatively we could weakref and just do nothing if the texture is destroyed */ g_object_unref (data->texture); + g_clear_error (&error); g_free (data); } @@ -522,6 +648,7 @@ shell_texture_cache_load_gicon (ShellTextureCache *cache, texture = CLUTTER_TEXTURE (clutter_texture_new ()); clutter_actor_set_size (CLUTTER_ACTOR (texture), size, size); + memset (&key, 0, sizeof(key)); key.icon = icon; key.size = size; texdata = g_hash_table_lookup (cache->priv->gicon_cache, &key); @@ -630,6 +757,88 @@ shell_texture_cache_load_uri_sync (ShellTextureCache *cache, return CLUTTER_ACTOR (texture); } +/** + * shell_texture_cache_load_thumbnail: + * @cache: + * @size: Size in pixels to use for thumbnail + * @uri: Source URI + * @mimetype: Source mime type + * @fallback: (allow-none): If no thumbnail is cached, use this pixbuf + * + * Asynchronously load a thumbnail image of a URI into a texture. The + * returned texture object will be a new instance; however, its texture data + * may be shared with other objects. This implies the texture data is cached. + * + * The current caching policy is permanent; to uncache, you must explicitly + * call shell_texture_cache_unref_thumbnail(). + * + * A fallback pixbuf may be provided in the case where thumbnailing is + * not applicable, or fails. + * + * Returns: (transfer none): A new #ClutterActor + */ +ClutterActor * +shell_texture_cache_load_thumbnail (ShellTextureCache *cache, + int size, + const char *uri, + const char *mimetype, + GdkPixbuf *fallback) +{ + ClutterTexture *texture; + AsyncTextureLoadData *data; + CacheKey key; + CoglHandle texdata; + + texture = CLUTTER_TEXTURE (clutter_texture_new ()); + clutter_actor_set_size (CLUTTER_ACTOR (texture), size, size); + + memset (&key, 0, sizeof(key)); + key.size = size; + key.thumbnail_uri = (char*)uri; + + texdata = g_hash_table_lookup (cache->priv->gicon_cache, &key); + if (!texdata) + { + data = g_new0 (AsyncTextureLoadData, 1); + data->uri = g_strdup (uri); + data->mimetype = g_strdup (mimetype); + data->thumbnail = TRUE; + data->width = size; + data->height = size; + data->texture = g_object_ref (texture); + load_thumbnail_async (cache, uri, mimetype, size, fallback, NULL, on_pixbuf_loaded, data); + } + else + { + clutter_texture_set_cogl_texture (texture, texdata); + } + + return CLUTTER_ACTOR (texture); +} + +/** + * shell_texture_cache_unref_thumbnail: + * @cache: + * @size: Size in pixels + * @uri: Source URI + * + * Removes the reference the shell_texture_cache_load_thumbnail() function + * created for a thumbnail. + */ +void +shell_texture_cache_unref_thumbnail (ShellTextureCache *cache, + int size, + const char *uri) +{ + CacheKey key; + + memset (&key, 0, sizeof(key)); + key.size = size; + key.thumbnail_uri = (char*)uri; + + g_hash_table_remove (cache->priv->gicon_cache, &key); +} + static ShellTextureCache *instance = NULL; /** diff --git a/src/shell-texture-cache.h b/src/shell-texture-cache.h index 68dd7d6b8..ec8096300 100644 --- a/src/shell-texture-cache.h +++ b/src/shell-texture-cache.h @@ -2,6 +2,7 @@ #define __SHELL_TEXTURE_CACHE_H__ #include +#include #include #define SHELL_TYPE_TEXTURE_CACHE (shell_texture_cache_get_type ()) @@ -37,6 +38,16 @@ ClutterActor *shell_texture_cache_load_gicon (ShellTextureCache *cache, GIcon *icon, gint size); +ClutterActor *shell_texture_cache_load_thumbnail (ShellTextureCache *cache, + int size, + const char *uri, + const char *mimetype, + GdkPixbuf *fallback); + +void shell_texture_cache_unref_thumbnail (ShellTextureCache *cache, + int size, + const char *uri); + ClutterActor *shell_texture_cache_load_uri_async (ShellTextureCache *cache, const gchar *filename, int available_width,