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).
This commit is contained in:
Marina Zhurakhinskaya 2009-03-20 12:06:34 -04:00
parent c018b7652f
commit 288fb7a837
4 changed files with 285 additions and 35 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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');
},

View File

@ -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() {