[gnome-shell] Bug 578178 – Show large previews for image files



commit 82cf98740a5d24acf40e4437a5012ec384ba2341
Author: Marina Zhurakhinskaya <marinaz redhat com>
Date:   Tue Apr 28 15:35:36 2009 -0400

    Bug 578178 â?? Show large previews for image files
    
    Use the actual image from the file for expanded mode previews for image files. Use the pixbuf loader to set the appropriate image size as soon as the image is prepared, but before it is loaded, to avoid loading large images. Apply the pixbuf orientation setting so that the image is properly rotated. Preserve the original size of the image if its dimensions are smaller than the space available.
    
    Make sure we provide the accurate available width for the details actor. This
    width has to exclude the padding and border width. Also provide the available height for the details actor.
---
 js/ui/docDisplay.js     |   20 +++++-
 js/ui/genericDisplay.js |   74 +++++++++++++------
 js/ui/overlay.js        |   20 +++--
 src/shell-global.c      |  186 +++++++++++++++++++++++++++++++++++++++++++++++
 src/shell-global.h      |    4 +
 5 files changed, 273 insertions(+), 31 deletions(-)

diff --git a/js/ui/docDisplay.js b/js/ui/docDisplay.js
index 68bfa57..ee78264 100644
--- a/js/ui/docDisplay.js
+++ b/js/ui/docDisplay.js
@@ -88,7 +88,7 @@ DocDisplayItem.prototype = {
 
     // Ensures the preview icon is created.
     _ensurePreviewIconCreated : function() {
-        if (!this._showPreview || this._previewIcon)
+        if (this._previewIcon)
             return; 
 
         this._previewIcon = new Clutter.Texture();
@@ -100,6 +100,24 @@ DocDisplayItem.prototype = {
         } else {
             Shell.clutter_texture_set_from_pixbuf(this._previewIcon, this._docInfo.get_icon(GenericDisplay.PREVIEW_ICON_SIZE));
         }
+    },
+
+    // Creates and returns a large preview icon, but only if this._docInfo is an image file
+    // and we were able to generate a pixbuf from it successfully.
+    _createLargePreviewIcon : function(availableWidth, availableHeight) {
+        if (this._docInfo.get_mime_type() == null || this._docInfo.get_mime_type().indexOf("image/") != 0)
+            return null;
+
+        let largePreviewPixbuf = Shell.create_pixbuf_from_image_file(this._docInfo.get_uri(), availableWidth, availableHeight);
+        
+        if (largePreviewPixbuf == null)
+            return null;
+
+        let largePreviewIcon = new Clutter.Texture();
+
+        Shell.clutter_texture_set_from_pixbuf(largePreviewIcon, largePreviewPixbuf); 
+
+        return largePreviewIcon;
     }
 };
 
diff --git a/js/ui/genericDisplay.js b/js/ui/genericDisplay.js
index d34b4d3..d4d2c5a 100644
--- a/js/ui/genericDisplay.js
+++ b/js/ui/genericDisplay.js
@@ -86,7 +86,7 @@ GenericDisplayItem.prototype = {
         this._description = null;
         this._icon = null;
         this._preview = null;
-        this._previewIcon = null;
+        this._previewIcon = null; 
 
         this.dragActor = null;
 
@@ -196,20 +196,19 @@ GenericDisplayItem.prototype = {
      * the preview pop-up has and be item-type specific.
      *
      * availableWidth - width available for displaying details
+     * availableHeight - height available for displaying details
      */ 
-    createDetailsActor: function(availableWidth) {
-        let details = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
+    createDetailsActor: function(availableWidth, availableHeight) {
+
+        let details = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
                                     spacing: PREVIEW_BOX_SPACING,
                                     width: availableWidth });
 
-        this._ensurePreviewIconCreated();
-
-        if (this._previewIcon != null) {
-            let previewIconClone = new Clutter.Clone({ source: this._previewIcon });
-            details.append(previewIconClone, Big.BoxPackFlags.NONE);
-        }
+        let mainDetails = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
+                                        spacing: PREVIEW_BOX_SPACING,
+                                        width: availableWidth });
 
-	// Inner box with name and description
+        // Inner box with name and description
         let textDetails = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
                                         spacing: PREVIEW_BOX_SPACING });
         let detailsName = new Clutter.Text({ color: ITEM_DISPLAY_NAME_COLOR,
@@ -224,7 +223,23 @@ GenericDisplayItem.prototype = {
                                                     text: this._description.text });
         textDetails.append(detailsDescription, Big.BoxPackFlags.NONE);
 
-        details.append(textDetails, Big.BoxPackFlags.EXPAND);
+        mainDetails.append(textDetails, Big.BoxPackFlags.EXPAND);
+
+        this._ensurePreviewIconCreated();
+        let largePreviewIcon = this._createLargePreviewIcon(availableWidth, Math.max(0, availableHeight - mainDetails.height - PREVIEW_BOX_SPACING));
+
+        if (this._previewIcon != null && largePreviewIcon == null) {
+            let previewIconClone = new Clutter.Clone({ source: this._previewIcon });
+            mainDetails.prepend(previewIconClone, Big.BoxPackFlags.NONE);
+        }
+
+        details.append(mainDetails, Big.BoxPackFlags.NONE);
+
+        if (largePreviewIcon != null) {
+            let largePreview = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL });
+            largePreview.append(largePreviewIcon, Big.BoxPackFlags.NONE);
+            details.append(largePreview, Big.BoxPackFlags.NONE);
+        }
    
         // We hide the preview pop-up if the details are shown elsewhere. 
         details.connect("show", 
@@ -315,6 +330,13 @@ GenericDisplayItem.prototype = {
         this.actor.add_actor(this._description);
     },
 
+    //// Virtual protected methods ////
+
+    // Creates and returns a large preview icon, but only if we have a detailed image.
+    _createLargePreviewIcon : function(availableWidth, availableHeight) {
+        return null;
+    },
+
     //// Pure virtual protected methods ////
 
     // Ensures the preview icon is created.
@@ -442,7 +464,6 @@ GenericDisplay.prototype = {
         this._displayedItemsCount = 0;
         this._pageDisplayed = 0;
         this._selectedIndex = -1;
-        this._keepDisplayCurrent = false;
         // These two are public - .actor is the normal "actor subclass" property,
         // but we also expose a .displayControl actor which is separate.
         // See also getSideArea.
@@ -454,17 +475,21 @@ GenericDisplay.prototype = {
                                             orientation: Big.BoxOrientation.HORIZONTAL});
 
         this._availableWidthForItemDetails = this._columnWidth;
+        this._availableHeightForItemDetails = this._height;
         this.selectedItemDetails = new Big.Box({});     
     },
 
     //// Public methods ////
 
-    setAvailableWidthForItemDetails: function(availableWidth) {
+    // Sets dimensions available for the item details display.
+    setAvailableDimensionsForItemDetails: function(availableWidth, availableHeight) {
         this._availableWidthForItemDetails = availableWidth;
+        this._availableHeightForItemDetails = availableHeight;
     },
 
-    getAvailableWidthForItemDetails: function() {
-        return this._availableWidthForItemDetails;
+    // Returns dimensions available for the item details display.
+    getAvailableDimensionsForItemDetails: function() {
+        return [this._availableWidthForItemDetails, this._availableHeightForItemDetails];
     },
 
     // Sets the search string and displays the matching items.
@@ -567,9 +592,8 @@ GenericDisplay.prototype = {
 
     // Updates the displayed items and makes the display actor visible.
     show: function() {
-        this._keepDisplayCurrent = true;
-        this._redisplay(true);
         this._grid.show();
+        this._redisplay(true);
     },
 
     // Hides the display actor.
@@ -577,7 +601,6 @@ GenericDisplay.prototype = {
         this._grid.hide();
         this._filterReset();
         this._removeAllDisplayItems();
-        this._keepDisplayCurrent = false;
     },
 
     // Returns an actor which acts as a sidebar; this is used for
@@ -672,7 +695,7 @@ GenericDisplay.prototype = {
         let displayItem = this._displayedItems[itemId];
         let displayItemIndex = this._getIndexOfDisplayedActor(displayItem.actor);
 
-        if (this.hasSelected() && this._displayedItemsCount == 1) {
+        if (this.hasSelected() && (this._displayedItemsCount == 1 || !this._grid.visible)) { 
             this.unsetSelected();
         } else if (this.hasSelected() && displayItemIndex < this._selectedIndex) {
             this.selectUp();
@@ -736,9 +759,9 @@ GenericDisplay.prototype = {
      *             their own while the user was browsing through the result pages.
      */
     _redisplay: function(resetPage) {
-        if (!this._keepDisplayCurrent)
+        if (!this._grid.visible)
             return;
-
+        
         this._refreshCache();
         if (!this._filterActive())
             this._setDefaultList();
@@ -947,13 +970,20 @@ GenericDisplay.prototype = {
         if (this._selectedIndex != -1) {
             let prev = this._findDisplayedByIndex(this._selectedIndex);
             prev.markSelected(false);
+            // Calling destroy() gets large image previews released as quickly as
+            // possible, if we just removed them, they might hang around for a while
+            // until the actor was garbage collected.
+            let children = this.selectedItemDetails.get_children();
+            for (let i = 0; i < children.length; i++)
+                children[i].destroy();
+    
             this.selectedItemDetails.remove_all();
         }
         this._selectedIndex = index;
         if (index != -1 && index < this._displayedItemsCount) {
             let item = this._findDisplayedByIndex(index);
             item.markSelected(true);
-            this.selectedItemDetails.append(item.createDetailsActor(this._availableWidthForItemDetails), Big.BoxPackFlags.NONE);  
+            this.selectedItemDetails.append(item.createDetailsActor(this._availableWidthForItemDetails, this._availableHeightForItemDetails), Big.BoxPackFlags.NONE);  
             this.emit('selected'); 
         }
     }
diff --git a/js/ui/overlay.js b/js/ui/overlay.js
index 47097dd..4991297 100644
--- a/js/ui/overlay.js
+++ b/js/ui/overlay.js
@@ -152,10 +152,15 @@ Sideshow.prototype = {
         this._additionalWidth = (this._width / SIDESHOW_COLUMNS) *
                                 (this._expandedSideshowColumns - SIDESHOW_COLUMNS);
 
+        let bottomHeight = displayGridRowHeight / 2;
+
+        let global = Shell.Global.get();
+
         let previewWidth = this._expandedWidth - this._width -
                            this._additionalWidth - SIDESHOW_SECTION_SPACING;
 
-        let global = Shell.Global.get();
+        let previewHeight = global.screen_height - Panel.PANEL_HEIGHT - SIDESHOW_PAD - bottomHeight;
+
         this.actor = new Clutter.Group();
         this.actor.height = global.screen_height;
         this._searchEntry = new SearchEntry(this._displayWidth);
@@ -265,9 +270,6 @@ Sideshow.prototype = {
                                             height: LABEL_HEIGHT});
         this._appsSection.append(this._appsText, Big.BoxPackFlags.EXPAND);
 
-
-        let bottomHeight = displayGridRowHeight / 2;
-
         this._itemDisplayHeight = global.screen_height - this._appsSection.y - SIDESHOW_SECTION_MISC_HEIGHT * 2 - bottomHeight;
         
         this._appsContent = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL });
@@ -325,14 +327,16 @@ Sideshow.prototype = {
         this._details = new Big.Box({ x: this._width + this._additionalWidth + SIDESHOW_SECTION_SPACING,
                                       y: Panel.PANEL_HEIGHT + SIDESHOW_PAD,
                                       width: previewWidth,
-                                      height: global.screen_height - Panel.PANEL_HEIGHT - SIDESHOW_PAD - bottomHeight,
+                                      height: previewHeight,
                                       corner_radius: DETAILS_CORNER_RADIUS,
                                       border: DETAILS_BORDER_WIDTH,
                                       border_color: DETAILS_BORDER_COLOR,
                                       padding: DETAILS_PADDING});
-        this._appDisplay.setAvailableWidthForItemDetails(previewWidth);
-        this._docDisplay.setAvailableWidthForItemDetails(previewWidth);
-
+        this._appDisplay.setAvailableDimensionsForItemDetails(previewWidth - DETAILS_PADDING * 2 - DETAILS_BORDER_WIDTH * 2,
+                                                              previewHeight - DETAILS_PADDING * 2 - DETAILS_BORDER_WIDTH * 2);
+        this._docDisplay.setAvailableDimensionsForItemDetails(previewWidth - DETAILS_PADDING * 2 - DETAILS_BORDER_WIDTH * 2,
+                                                              previewHeight - DETAILS_PADDING * 2 - DETAILS_BORDER_WIDTH * 2);
+ 
         /* Proxy the activated signals */
         this._appDisplay.connect('activated', function(appDisplay) {
             me.emit('activated');
diff --git a/src/shell-global.c b/src/shell-global.c
index c01dd51..ab36754 100644
--- a/src/shell-global.c
+++ b/src/shell-global.c
@@ -15,6 +15,7 @@
 #include <unistd.h>
 #include <dbus/dbus-glib.h>
 #include <libgnomeui/gnome-thumbnail.h>
+#include <math.h>
 
 #define SHELL_DBUS_SERVICE "org.gnome.Shell"
 
@@ -368,6 +369,191 @@ shell_get_thumbnail_for_recent_info(GtkRecentInfo  *recent_info)
     return pixbuf;   
 }
 
+// A private structure for keeping width and height.
+typedef struct {
+  int width;
+  int height;
+} Dimensions;
+
+/**
+ * on_image_size_prepared:
+ *
+ * @pixbuf_loader: #GdkPixbufLoader loading the image
+ * @width: the original width of the image
+ * @height: the original height of the image
+ * @data: pointer to the #Dimensions sructure containing available width and height for the image,
+ *        available width or height can be -1 if the dimension is not limited
+ *
+ * Private function.
+ *
+ * Sets the size of the image being loaded to fit the available width and height dimensions,
+ * but never scales up the image beyond its actual size. 
+ * Intended to be used as a callback for #GdkPixbufLoader "size-prepared" signal.
+ */
+static void
+on_image_size_prepared (GdkPixbufLoader *pixbuf_loader,
+                              gint             width,
+                              gint             height,
+                              gpointer         data)
+{
+    Dimensions *available_dimensions = data; 
+    int available_width = available_dimensions->width;
+    int available_height = available_dimensions->height;
+    int scaled_width = -1;
+    int scaled_height = -1; 
+
+    if (width == 0 || height == 0)
+        return;
+
+    if (available_width >= 0 && available_height >= 0) 
+      {
+        // This should keep the aspect ratio of the image intact, because if 
+        // available_width < (available_height * width) / height
+        // than
+        // (available_width * height) / width < available_height
+        // So we are guaranteed to either scale the image to have an available_width 
+        // for width and height scaled accordingly OR have the available_height
+        // for height and width scaled accordingly, whichever scaling results 
+        // in the image that can fit both available dimensions. 
+        scaled_width = MIN(available_width, (available_height * width) / height);
+        scaled_height = MIN(available_height, (available_width * height) / width);
+      } 
+    else if (available_width >= 0) 
+      {
+        scaled_width = available_width;
+        scaled_height = (available_width * height) / width;
+      } 
+    else if (available_height >= 0) 
+      {
+        scaled_width = (available_height * width) / height;
+        scaled_height = available_height;
+      }
+
+    // Scale the image only if that will not increase its original dimensions.
+    if (scaled_width >= 0 && scaled_height >= 0 && scaled_width < width && scaled_height < height) 
+        gdk_pixbuf_loader_set_size (pixbuf_loader, scaled_width, scaled_height);
+}
+
+/**
+ * shell_create_pixbuf_from_image_file:
+ *
+ * @uri: uri of the image file from which to create a pixbuf 
+ * @available_width: available width for the image, can be -1 if not limited
+ * @available_height: available height for the image, can be -1 if not limited
+ *
+ * Return value: (transfer full): #GdkPixbuf with the image file loaded if it was 
+ *               generated succesfully, %NULL otherwise
+ *               The image is scaled down to fit the available width and height 
+ *               dimensions, but the image is never scaled up beyond its actual size.
+ *               The pixbuf is rotated according to the associated orientation setting.
+ */
+GdkPixbuf *
+shell_create_pixbuf_from_image_file(const char *uri,
+                                    int         available_width,
+                                    int         available_height)
+{
+    GdkPixbufLoader *pixbuf_loader = NULL;
+    GdkPixbuf *pixbuf;
+    GdkPixbuf *rotated_pixbuf = NULL;
+    GFile *file = NULL;  
+    char *contents = NULL;
+    gsize size;
+    GError *error = NULL;
+    gboolean success;
+    Dimensions available_dimensions;
+    int width_before_rotation, width_after_rotation;
+
+    file = g_file_new_for_uri (uri);
+    
+    success = g_file_load_contents (file, NULL, &contents, &size, NULL, &error);
+
+    if (!success)
+      {
+        g_warning ("Could not load contents of the file with uri %s: %s", uri, error->message);
+        goto out;
+      }
+
+    pixbuf_loader = gdk_pixbuf_loader_new ();
+      
+    available_dimensions.width = available_width;
+    available_dimensions.height = available_height;
+    g_signal_connect (pixbuf_loader, "size-prepared",
+                      G_CALLBACK (on_image_size_prepared), &available_dimensions);
+
+    success = gdk_pixbuf_loader_write (pixbuf_loader,
+                                       (const guchar *) contents,
+                                       size, 
+                                       &error);
+    if (!success)
+      {
+        g_warning ("Could not write contents of the file with uri %s to the gdk pixbuf loader: %s", uri, error->message);
+        goto out;
+      }
+
+    success = gdk_pixbuf_loader_close (pixbuf_loader, &error);
+    if (!success)
+      {
+        g_warning ("Could not close the pixbuf loader after writing contents of the file with uri %s: %s", uri, error->message);
+        goto out;
+      } 
+
+    pixbuf = gdk_pixbuf_loader_get_pixbuf (pixbuf_loader);
+    width_before_rotation = gdk_pixbuf_get_width (pixbuf);
+
+    rotated_pixbuf = gdk_pixbuf_apply_embedded_orientation (pixbuf);  
+    width_after_rotation = gdk_pixbuf_get_width (rotated_pixbuf);
+
+    // There is currently no way to tell if the pixbuf will need to be rotated before it is loaded, 
+    // so we only check that once it is loaded, and reload it again if it needs to be rotated in order
+    // to use the available width and height correctly. 
+    // http://bugzilla.gnome.org/show_bug.cgi?id=579003
+    if (width_before_rotation != width_after_rotation) 
+      {
+        g_object_unref (pixbuf_loader);
+        g_object_unref (rotated_pixbuf);
+        rotated_pixbuf = NULL;
+       
+        pixbuf_loader = gdk_pixbuf_loader_new ();
+      
+        // We know that the image will later be rotated, so we reverse the available dimensions.
+        available_dimensions.width = available_height;
+        available_dimensions.height = available_width;
+        g_signal_connect (pixbuf_loader, "size-prepared",
+                          G_CALLBACK (on_image_size_prepared), &available_dimensions);
+
+        success = gdk_pixbuf_loader_write (pixbuf_loader,
+                                           (const guchar *) contents,
+                                           size, 
+                                           &error);
+        if (!success)
+          {
+            g_warning ("Could not write contents of the file with uri %s to the gdk pixbuf loader: %s", uri, error->message);
+            goto out;
+          } 
+
+       success = gdk_pixbuf_loader_close (pixbuf_loader, &error);
+       if (!success)
+          {
+            g_warning ("Could not close the pixbuf loader after writing contents of the file with uri %s: %s", uri, error->message); 
+            goto out;
+          } 
+    
+        pixbuf = gdk_pixbuf_loader_get_pixbuf (pixbuf_loader);
+
+        rotated_pixbuf = gdk_pixbuf_apply_embedded_orientation (pixbuf);  
+      }
+
+  out:    
+    g_clear_error (&error);
+    g_free (contents);
+    if (file)
+        g_object_unref (file);
+    if (pixbuf_loader)
+        g_object_unref (pixbuf_loader);
+    
+    return rotated_pixbuf;
+}
+
 /**
  * shell_get_categories_for_desktop_file:
  *
diff --git a/src/shell-global.h b/src/shell-global.h
index 9ce837f..1b638af 100644
--- a/src/shell-global.h
+++ b/src/shell-global.h
@@ -36,6 +36,10 @@ gboolean shell_clutter_texture_set_from_pixbuf (ClutterTexture *texture,
 
 GdkPixbuf *shell_get_thumbnail_for_recent_info(GtkRecentInfo  *recent_info);
 
+GdkPixbuf *shell_create_pixbuf_from_image_file(const char *uri, 
+                                               int available_width, 
+                                               int available_height);
+
 GSList *shell_get_categories_for_desktop_file(const char *desktop_file_name);
 
 guint16 shell_get_event_key_symbol(ClutterEvent *event);



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