From 288fb7a8377662c037f50cd495ccd5128a76680c Mon Sep 17 00:00:00 2001 From: Marina Zhurakhinskaya Date: Fri, 20 Mar 2009 12:06:34 -0400 Subject: [PATCH] Bug 571426 - Show pop-up previews of sideshow items The pop-up previews have larger images than the item displays, which is particularly nice when we are displaying thumbnails for documents. The previews are also at least as wide as is required to fit the item title on one line and the item description inside them is wrapped. Therefore they act as tooltips showing the full title and description text. The preview updates when the item under the mouse pointer changes. Changes in overlay.js ensure that we keep the sideshow on top when the workspaces are not being animated so that we can find the item over which the pointer is located. The preview is removed when the item it is shown for starts being dragged. _hideInProgress variable was added to represent the state of the overlay when the code for hiding it was already triggered. This fixes the error which was happening when the code for hiding the overlay was triggered multiple times (for example by the user clicking the Activities button twice when exiting the overlay). --- js/ui/appDisplay.js | 49 ++++++++--- js/ui/docDisplay.js | 37 ++++++--- js/ui/genericDisplay.js | 180 ++++++++++++++++++++++++++++++++++++++-- js/ui/overlay.js | 54 ++++++++++-- 4 files changed, 285 insertions(+), 35 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 971b44857..79ba93985 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -55,20 +55,20 @@ AppDisplayItem.prototype = { let description = appInfo.get_description(); - let iconTheme = Gtk.IconTheme.get_default(); - let icon = new Clutter.Texture({ width: GenericDisplay.ITEM_DISPLAY_ICON_SIZE, height: GenericDisplay.ITEM_DISPLAY_ICON_SIZE}); - let gicon = appInfo.get_icon(); - let path = null; - if (gicon != null) { - let iconinfo = iconTheme.lookup_by_gicon(gicon, GenericDisplay.ITEM_DISPLAY_ICON_SIZE, Gtk.IconLookupFlags.NO_SVG); - if (iconinfo) - path = iconinfo.get_filename(); + + this._gicon = appInfo.get_icon(); + let iconPath = null; + if (this._gicon != null) { + let iconTheme = Gtk.IconTheme.get_default(); + let iconInfo = iconTheme.lookup_by_gicon(this._gicon, GenericDisplay.ITEM_DISPLAY_ICON_SIZE, Gtk.IconLookupFlags.NO_SVG); + if (iconInfo) + iconPath = iconInfo.get_filename(); } - if (path) { + if (iconPath) { try { - icon.set_from_file(path); + icon.set_from_file(iconPath); icon.x = GenericDisplay.ITEM_DISPLAY_PADDING; icon.y = GenericDisplay.ITEM_DISPLAY_PADDING; } catch (e) { @@ -76,6 +76,7 @@ AppDisplayItem.prototype = { log('Error loading AppDisplayItem icon ' + e); } } + this._setItemInfo(name, description, icon); }, @@ -99,8 +100,34 @@ AppDisplayItem.prototype = { context.set_icon(icon); context.set_timestamp(timestamp); this._appInfo.launch([], context); - } + }, + //// Protected method overrides //// + + // Ensures the preview icon is created. + _ensurePreviewIconCreated : function() { + if (!this._hasPreview || this._previewIcon) + return; + + let previewIconPath = null; + + if (this._gicon != null) { + let iconTheme = Gtk.IconTheme.get_default(); + let previewIconInfo = iconTheme.lookup_by_gicon(this._gicon, GenericDisplay.PREVIEW_ICON_SIZE, Gtk.IconLookupFlags.NO_SVG); + if (previewIconInfo) + previewIconPath = previewIconInfo.get_filename(); + } + + if (previewIconPath) { + try { + this._previewIcon = new Clutter.Texture({ width: GenericDisplay.PREVIEW_ICON_SIZE, height: GenericDisplay.PREVIEW_ICON_SIZE}); + this._previewIcon.set_from_file(previewIconPath); + } catch (e) { + // we can get an error here if the file path doesn't exist on the system + log('Error loading AppDisplayItem preview icon ' + e); + } + } + } }; /* This class represents a display containing a collection of application items. diff --git a/js/ui/docDisplay.js b/js/ui/docDisplay.js index e60482f9f..1fd470029 100644 --- a/js/ui/docDisplay.js +++ b/js/ui/docDisplay.js @@ -33,24 +33,24 @@ DocDisplayItem.prototype = { let description = ""; let icon = new Clutter.Texture(); - let pixbuf = Shell.get_thumbnail_for_recent_info(docInfo); - if (pixbuf) { + this._iconPixbuf = Shell.get_thumbnail_for_recent_info(docInfo); + if (this._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 use extra margin when positioning it. - let scalingFactor = (GenericDisplay.ITEM_DISPLAY_ICON_SIZE - ITEM_DISPLAY_ICON_MARGIN * 2) / Math.max(pixbuf.get_width(), pixbuf.get_height()); - icon.set_width(Math.ceil(pixbuf.get_width() * scalingFactor)); - icon.set_height(Math.ceil(pixbuf.get_height() * scalingFactor)); - Shell.clutter_texture_set_from_pixbuf(icon, pixbuf); + let scalingFactor = (GenericDisplay.ITEM_DISPLAY_ICON_SIZE - ITEM_DISPLAY_ICON_MARGIN * 2) / Math.max(this._iconPixbuf.get_width(), this._iconPixbuf.get_height()); + icon.set_width(Math.ceil(this._iconPixbuf.get_width() * scalingFactor)); + icon.set_height(Math.ceil(this._iconPixbuf.get_height() * scalingFactor)); + Shell.clutter_texture_set_from_pixbuf(icon, this._iconPixbuf); icon.x = GenericDisplay.ITEM_DISPLAY_PADDING + ITEM_DISPLAY_ICON_MARGIN; - icon.y = GenericDisplay.ITEM_DISPLAY_PADDING + ITEM_DISPLAY_ICON_MARGIN; + icon.y = GenericDisplay.ITEM_DISPLAY_PADDING + ITEM_DISPLAY_ICON_MARGIN; } else { Shell.clutter_texture_set_from_pixbuf(icon, docInfo.get_icon(GenericDisplay.ITEM_DISPLAY_ICON_SIZE)); icon.x = GenericDisplay.ITEM_DISPLAY_PADDING; icon.y = GenericDisplay.ITEM_DISPLAY_PADDING; } - - this._setItemInfo(name, description, icon); + + this._setItemInfo(name, description, icon); }, //// Public methods //// @@ -82,8 +82,25 @@ DocDisplayItem.prototype = { } else { log("Failed to get application info for " + this._docInfo.get_uri()); } - } + }, + //// Protected method overrides //// + + // Ensures the preview icon is created. + _ensurePreviewIconCreated : function() { + if (!this._hasPreview || this._previewIcon) + return; + + this._previewIcon = new Clutter.Texture(); + if (this._iconPixbuf) { + let scalingFactor = (GenericDisplay.PREVIEW_ICON_SIZE / Math.max(this._iconPixbuf.get_width(), this._iconPixbuf.get_height())); + this._previewIcon.set_width(Math.ceil(this._iconPixbuf.get_width() * scalingFactor)); + this._previewIcon.set_height(Math.ceil(this._iconPixbuf.get_height() * scalingFactor)); + Shell.clutter_texture_set_from_pixbuf(this._previewIcon, this._iconPixbuf); + } else { + Shell.clutter_texture_set_from_pixbuf(this._previewIcon, this._docInfo.get_icon(GenericDisplay.PREVIEW_ICON_SIZE)); + } + } }; /* This class represents a display containing a collection of document items. diff --git a/js/ui/genericDisplay.js b/js/ui/genericDisplay.js index cf254af3c..c328403c3 100644 --- a/js/ui/genericDisplay.js +++ b/js/ui/genericDisplay.js @@ -3,6 +3,7 @@ const Big = imports.gi.Big; const Clutter = imports.gi.Clutter; const Gio = imports.gi.Gio; +const Gdk = imports.gi.Gdk; const Gtk = imports.gi.Gtk; const Lang = imports.lang; const Pango = imports.gi.Pango; @@ -23,6 +24,8 @@ const ITEM_DISPLAY_SELECTED_BACKGROUND_COLOR = new Clutter.Color(); ITEM_DISPLAY_SELECTED_BACKGROUND_COLOR.from_pixel(0x00ff0055); const DISPLAY_CONTROL_SELECTED_COLOR = new Clutter.Color(); DISPLAY_CONTROL_SELECTED_COLOR.from_pixel(0x112288ff); +const PREVIEW_BOX_BACKGROUND_COLOR = new Clutter.Color(); +PREVIEW_BOX_BACKGROUND_COLOR.from_pixel(0xADADADf0); const ITEM_DISPLAY_HEIGHT = 50; const ITEM_DISPLAY_ICON_SIZE = 48; @@ -30,6 +33,14 @@ const ITEM_DISPLAY_PADDING = 1; const DEFAULT_COLUMN_GAP = 6; const LABEL_HEIGHT = 16; +const PREVIEW_ICON_SIZE = 96; +const PREVIEW_BOX_PADDING = 6; +const PREVIEW_BOX_SPACING = 4; +const PREVIEW_BOX_CORNER_RADIUS = 10; +// how far relative to the full item width the preview box should be placed +const PREVIEW_PLACING = 3/4; +const PREVIEW_DETAILS_MIN_WIDTH = PREVIEW_ICON_SIZE * 2; + /* This is a virtual class that represents a single display item containing * a name, a description, and an icon. It allows selecting an item and represents * it by highlighting it with a different background color than the default. @@ -37,12 +48,13 @@ const LABEL_HEIGHT = 16; * availableWidth - total width available for the item */ function GenericDisplayItem(availableWidth) { - this._init(name, description, icon, availableWidth); + this._init(availableWidth); } GenericDisplayItem.prototype = { _init: function(availableWidth) { this._availableWidth = availableWidth; + this._hasPreview = false; this.actor = new Clutter.Group({ reactive: true, width: availableWidth, @@ -54,7 +66,9 @@ GenericDisplayItem.prototype = { this.activate(); })); - DND.makeDraggable(this.actor); + let draggable = DND.makeDraggable(this.actor); + draggable.connect('drag-begin', Lang.bind(this, this._onDragBegin)); + this._bg = new Big.Box({ background_color: ITEM_DISPLAY_BACKGROUND_COLOR, corner_radius: 4, x: 0, y: 0, @@ -64,8 +78,13 @@ GenericDisplayItem.prototype = { this._name = null; this._description = null; this._icon = null; + this._preview = null; + this._previewIcon = null; this.dragActor = null; + + this.actor.connect('enter-event', Lang.bind(this, this._onEnter)); + this.actor.connect('leave-event', Lang.bind(this, this._onLeave)); }, //// Draggable object interface //// @@ -97,6 +116,40 @@ GenericDisplayItem.prototype = { //// Public methods //// + // Sets a boolean value that indicates whether the item should display a pop-up preview on mouse over. + setHasPreview: function(hasPreview) { + this._hasPreview = hasPreview; + }, + + // Returns a boolean value that indicates whether the item displays a pop-up preview on mouse over. + getHasPreview: function() { + return this._hasPreview; + }, + + // Displays the preview for the item. + showPreview: function() { + if(!this._hasPreview) + return; + + this._ensurePreviewCreated(); + + let [x, y] = this.actor.get_transformed_position(); + let global = Shell.Global.get(); + let previewX = Math.min(x + this._availableWidth * PREVIEW_PLACING, global.screen_width - this._preview.width); + let previewY = Math.min(y, global.screen_height - this._preview.height); + this._preview.set_position(previewX, previewY); + + this._preview.show(); + }, + + // Hides the preview for the item. + hidePreview: function() { + if(!this._hasPreview) + return; + + this._preview.hide(); + }, + // Highlights the item by setting a different background color than the default // if isSelected is true, removes the highlighting otherwise. markSelected: function(isSelected) { @@ -110,8 +163,16 @@ GenericDisplayItem.prototype = { // Activates the item, as though it was clicked activate: function() { + this.hidePreview(); this.emit('activate'); }, + + // Destoys the item, as well as a preview for the item if it exists. + destroy: function() { + this.actor.destroy(); + if (this._preview != null) + this._preview.destroy(); + }, //// Pure virtual public methods //// @@ -127,7 +188,7 @@ GenericDisplayItem.prototype = { * * nameText - name of the item * descriptionText - short description of the item - * iconActor - ClutterTexture containing the icon image which should be 48x48 pixels + * iconActor - ClutterTexture containing the icon image which should be ITEM_DISPLAY_ICON_SIZE size */ _setItemInfo: function(nameText, descriptionText, iconActor) { if (this._name != null) { @@ -146,14 +207,23 @@ GenericDisplayItem.prototype = { this._icon.destroy(); this._icon = null; } + // This ensures we'll create a new preview and previewIcon next time we need a preview + if (this._preview != null) { + this._preview.destroy(); + this._preview = null; + } + if (this._previewIcon != null) { + this._previewIcon.destroy(); + this._previewIcon = null; + } this._icon = iconActor; this.actor.add_actor(this._icon); - let text_width = this._availableWidth - (ITEM_DISPLAY_ICON_SIZE + 4); + let textWidth = this._availableWidth - (ITEM_DISPLAY_ICON_SIZE + 4); this._name = new Clutter.Text({ color: ITEM_DISPLAY_NAME_COLOR, font_name: "Sans 14px", - width: text_width, + width: textWidth, ellipsize: Pango.EllipsizeMode.END, text: nameText, x: ITEM_DISPLAY_ICON_SIZE + 4, @@ -161,12 +231,84 @@ GenericDisplayItem.prototype = { this.actor.add_actor(this._name); this._description = new Clutter.Text({ color: ITEM_DISPLAY_DESCRIPTION_COLOR, font_name: "Sans 12px", - width: text_width, + width: textWidth, ellipsize: Pango.EllipsizeMode.END, text: descriptionText ? descriptionText : "", x: this._name.x, y: this._name.height + 4 }); this.actor.add_actor(this._description); + }, + + //// Pure virtual protected methods //// + + // Ensures the preview icon is created. + _ensurePreviewIconCreated: function() { + throw new Error("Not implemented"); + }, + + //// Private methods //// + + // Ensures the preview actor is created. + _ensurePreviewCreated: function() { + if (!this._hasPreview || this._preview) + return; + + this._preview = new Big.Box({ background_color: PREVIEW_BOX_BACKGROUND_COLOR, + orientation: Big.BoxOrientation.HORIZONTAL, + corner_radius: PREVIEW_BOX_CORNER_RADIUS, + padding: PREVIEW_BOX_PADDING, + spacing: PREVIEW_BOX_SPACING }); + + let previewDetailsWidth = this._availableWidth - PREVIEW_BOX_PADDING * 2; + + this._ensurePreviewIconCreated(); + + if (this._previewIcon != null) { + this._preview.append(this._previewIcon, Big.BoxPackFlags.EXPAND); + previewDetailsWidth = this._availableWidth - this._previewIcon.width - PREVIEW_BOX_PADDING * 2 - PREVIEW_BOX_SPACING; + } + + // Inner box with name and description + let previewDetails = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL, + spacing: PREVIEW_BOX_SPACING }); + let previewName = new Clutter.Text({ color: ITEM_DISPLAY_NAME_COLOR, + font_name: "Sans bold 14px", + text: this._name.text}); + + previewDetails.width = Math.max(PREVIEW_DETAILS_MIN_WIDTH, previewDetailsWidth, previewName.width); + + previewDetails.append(previewName, Big.BoxPackFlags.NONE); + + let previewDescription = new Clutter.Text({ color: ITEM_DISPLAY_NAME_COLOR, + font_name: "Sans 14px", + line_wrap: true, + text: this._description.text }); + previewDetails.append(previewDescription, Big.BoxPackFlags.NONE); + + this._preview.append(previewDetails, Big.BoxPackFlags.EXPAND); + + // Add the preview to global stage to allow for top-level layering + let global = Shell.Global.get(); + global.stage.add_actor(this._preview); + this._preview.hide(); + }, + + // Performs actions on mouse enter event for the item. Currently, shows the preview for the item. + _onEnter: function(actor, event) { + this.showPreview(); + }, + + // Performs actions on mouse leave event for the item. Currently, hides the preview for the item. + _onLeave: function(actor, event) { + this.hidePreview(); + }, + + // Hides the preview once the item starts being dragged. + _onDragBegin : function (draggable, time) { + // For some reason, we are not getting leave-event signal when we are dragging an item, + // so the preview box stays behind if we didn't have the call here. It makes sense to hide + // the preview as soon as the item starts being dragged anyway. + this.hidePreview(); } }; @@ -293,6 +435,14 @@ GenericDisplay.prototype = { this._selectIndex(-1); }, + // Hides the preview if any item has one being displayed. + hidePreview: function() { + for (itemId in this._displayedItems) { + let item = this._displayedItems[itemId]; + item.hidePreview(); + } + }, + // Returns true if the display has any displayed items. hasItems: function() { return this._displayedItemsCount > 0; @@ -376,6 +526,7 @@ GenericDisplay.prototype = { let itemInfo = this._allItems[itemId]; let displayItem = this._createDisplayItem(itemInfo); + displayItem.setHasPreview(true); displayItem.connect('activate', function() { me._activatedItem = displayItem; @@ -412,16 +563,16 @@ GenericDisplay.prototype = { // in case the drop was not accepted by any actor.) displayItem.actor.hide_all(); this._grid.remove_actor(displayItem.actor); - // We should not destroy the actor up-front, because that would also + // We should not destroy the item up-front, because that would also // destroy the icon that was used to clone the image for the drag actor. // We destroy it once the dragActor is destroyed instead. displayItem.dragActor.connect('destroy', function(item) { - displayItem.actor.destroy(); + displayItem.destroy(); }); } else { - displayItem.actor.destroy(); + displayItem.destroy(); } delete this._displayedItems[itemId]; this._displayedItemsCount--; @@ -457,6 +608,17 @@ GenericDisplay.prototype = { this._displayMatchedItems(true); + // Check if the pointer is over one of the items and display the preview pop-up if it is. + let [child, x, y, mask] = Gdk.Screen.get_default().get_root_window().get_pointer(); + let global = Shell.Global.get(); + let actor = global.stage.get_actor_at_pos(x, y); + if (actor != null) { + let item = this._findDisplayedByActor(actor.get_parent()); + if (item != null) { + item.showPreview(); + } + } + this.emit('redisplayed'); }, diff --git a/js/ui/overlay.js b/js/ui/overlay.js index cfb1b9458..5ad47edd9 100644 --- a/js/ui/overlay.js +++ b/js/ui/overlay.js @@ -256,6 +256,8 @@ Sideshow.prototype = { // so that we can move it to the item that was clicked on me._appDisplay.unsetSelected(); me._docDisplay.unsetSelected(); + me._appDisplay.hidePreview(); + me._docDisplay.hidePreview(); me._appDisplay.doActivate(); me.emit('activated'); }); @@ -264,6 +266,8 @@ Sideshow.prototype = { // so that we can move it to the item that was clicked on me._appDisplay.unsetSelected(); me._docDisplay.unsetSelected(); + me._appDisplay.hidePreview(); + me._docDisplay.hidePreview(); me._docDisplay.doActivate(); me.emit('activated'); }); @@ -571,6 +575,7 @@ Overlay.prototype = { this._group._delegate = this; this.visible = false; + this._hideInProgress = false; let background = new Clutter.Rectangle({ color: OVERLAY_BACKGROUND_COLOR, reactive: true, @@ -602,10 +607,16 @@ Overlay.prototype = { let workspacesX = displayGridColumnWidth * asideXFactor + WORKSPACE_GRID_PADDING; me._workspaces.addButton.hide(); me._workspaces.updatePosition(workspacesX, null); + // lower the sideshow below the workspaces background, so that the workspaces + // background covers the parts of the sideshow that are gradually being + // revealed from underneath it + me._sideshow.actor.lower(me._workspacesBackground); Tweener.addTween(me._workspacesBackground, { x: displayGridColumnWidth * asideXFactor, time: ANIMATION_TIME, - transition: "easeOutQuad" + transition: "easeOutQuad", + onComplete: me._animationDone, + onCompleteScope: me }); } }); @@ -614,10 +625,15 @@ Overlay.prototype = { let workspacesX = displayGridColumnWidth + WORKSPACE_GRID_PADDING; me._workspaces.addButton.show(); me._workspaces.updatePosition(workspacesX, null); + // lower the sideshow below the workspaces background, so that the workspaces + // background covers the parts of the sideshow as it slides in over them + me._sideshow.actor.lower(me._workspacesBackground); Tweener.addTween(me._workspacesBackground, { x: displayGridColumnWidth, time: ANIMATION_TIME, - transition: "easeOutQuad" + transition: "easeOutQuad", + onComplete: me._animationDone, + onCompleteScope: me }); } }); @@ -683,7 +699,6 @@ Overlay.prototype = { this._workspaces = new Workspaces.Workspaces(workspacesWidth, workspacesHeight, workspacesX, workspacesY, addButtonSize, addButtonX, addButtonY); this._group.add_actor(this._workspaces.actor); - this._workspaces.actor.raise_top(); // All the the actors in the window group are completely obscured, // hiding the group holding them while the overlay is displayed greatly @@ -694,12 +709,22 @@ Overlay.prototype = { // clones of them, this would obviously no longer be necessary. global.window_group.hide(); this._group.show(); + + // Dummy tween, just waiting for the workspace animation + Tweener.addTween(this, + { time: ANIMATION_TIME, + onComplete: this._animationDone, + onCompleteScope: this + }); }, hide : function() { - if (!this.visible) + if (!this.visible || this._hideInProgress) return; + this._hideInProgress = true; + // lower the sideshow, so that workspaces display is on top and covers the sideshow while it is sliding out + this._sideshow.actor.lower(this._workspacesBackground); this._workspaces.hide(); // Dummy tween, just waiting for the workspace animation @@ -712,10 +737,26 @@ Overlay.prototype = { //// Private methods //// + // Raises the sideshow to the top, so that we can tell if the pointer is above one of its items. + // We need to do this every time animation of the workspaces is done bacause the workspaces actor + // currently covers the whole screen, regardless of where the workspaces are actually displayed. + // On the other hand, we need the workspaces to be on top when they are sliding in, out, + // and to the side because we want them to cover the sideshow as they do that. + // + // Once we rework the workspaces actor to only cover the area it actually needs, we can + // remove this workaround. Also http://bugzilla.openedhand.com/show_bug.cgi?id=1513 requests being + // able to pick only a reactive actor at a certain position, rather than any actor. Being able + // to do that would allow us to not have to raise the sideshow. + _animationDone: function() { + if (this._hideInProgress) + return; + + this._sideshow.actor.raise_top(); + }, + _hideDone: function() { let global = Shell.Global.get(); - this.visible = false; global.window_group.show(); this._workspaces.destroy(); @@ -726,6 +767,9 @@ Overlay.prototype = { this._sideshow.hide(); this._group.hide(); + + this.visible = false; + this._hideInProgress = false; }, _deactivate : function() {