Bug 577338 – Show item details on click in the expanded view

Change the overlay behavior to display more details about an item on single
click and launch it on double click.

When the item is clicked on in the expanded view, the details are shown in the
area on the right that is allocated for showing details. The details pop-up is
not shown for the item that was clicked on, but it is shown for other items on
hover and for the item that was clicked if the mouse pointer is moved back to
it.

Both hovering and single clicking results in the details pop-up being shown in
the regular view. (Single clicking actually doesn't do anything in the regular
view, but the details pop-up is shown due to hovering within the time it takes
to perform a single click.)

The overlay now uses 3 columns on the wide screen for displaying items in the
expanded view. This allows keeping the size of the details area the same for
expanded and regular views.

Add shell_get_button_event_click_count() to shell-global.[hc] to retrieve
the click count for button press and release events.

Add selectedItemDetails public variable actor to the generic display to
contain the details of the selected item and be shown in the overlay when
it is in the expanded view mode.

Fix the bug when the sideshow section would loose selection in the expanded
view if it did not have any items, and would not regain it if it was repopulated
with some items (e.g. when the search string changes).

The sideshow no longer takes overlay parent and width as constructor arguments.
It is added to the overlay inside the overlay code and manages its own width
instead (which is ok, since it is pretty much a private class within overlay).

Clean up the way selection is moved when an item is launched in order to have
selection on click and activation on double click be implemented in a similar
fashion. An unneeded _activatedItem variable in generic display was removed,
and the selected item is activated instead when necessary. The flow of processing
signals changed so that generic display no longer waits for the selection from
a different sideshow section to be removed before selecting an item that was
clicked on. This removed the need for doActivate() function.
This commit is contained in:
Marina Zhurakhinskaya 2009-03-31 14:12:33 -04:00
parent 2b9b600710
commit 5fbc0d4a56
4 changed files with 204 additions and 76 deletions

View File

@ -63,10 +63,14 @@ GenericDisplayItem.prototype = {
width: availableWidth,
height: ITEM_DISPLAY_HEIGHT });
this.actor._delegate = this;
this.actor.connect('button-release-event',
this.actor.connect('button-press-event',
Lang.bind(this,
function(draggable, e) {
this.activate();
function(actor, e) {
let clickCount = Shell.get_button_event_click_count(e);
if (clickCount == 1)
this.select();
else if (clickCount == 2)
this.activate();
}));
let draggable = DND.makeDraggable(this.actor);
@ -176,12 +180,69 @@ GenericDisplayItem.prototype = {
this._bg.background_color = color;
},
// Activates the item, as though it was clicked
// Activates the item, as though it was launched
activate: function() {
this.hidePreview();
this.emit('activate');
},
// Selects the item, as though it was clicked
select: function() {
this.emit('select');
},
/*
* Returns an actor containing item details. In the future details can have more information than what
* the preview pop-up has and be item-type specific.
*
* availableWidth - width available for displaying details
*/
createDetailsActor: function(availableWidth) {
let details = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
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);
}
// 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,
font_name: "Sans bold 14px",
line_wrap: true,
text: this._name.text});
textDetails.append(detailsName, Big.BoxPackFlags.NONE);
let detailsDescription = new Clutter.Text({ color: ITEM_DISPLAY_NAME_COLOR,
font_name: "Sans 14px",
line_wrap: true,
text: this._description.text });
textDetails.append(detailsDescription, Big.BoxPackFlags.NONE);
details.append(textDetails, Big.BoxPackFlags.EXPAND);
// We hide the preview pop-up if the details are shown elsewhere.
details.connect("show",
Lang.bind(this,
function() {
// Right now "show" signal is emitted when an actor is added to a parent that
// has not been added to anything and "visible" property is also set to true
// at this point, so checking if the parent that the actor has been added to
// has a parent of its own is a temporary workaround. That other actor is
// presumed to be displayed, which is a limitation of this workaround, but is
// the case with our usage of the details actor now.
// http://bugzilla.openedhand.com/show_bug.cgi?id=1138
if (details.get_parent() != null && details.get_parent().get_parent() != null)
this.hidePreview();
}));
return details;
},
// Destoys the item, as well as a preview for the item if it exists.
destroy: function() {
this.actor.destroy();
@ -274,33 +335,33 @@ GenericDisplayItem.prototype = {
padding: PREVIEW_BOX_PADDING,
spacing: PREVIEW_BOX_SPACING });
let previewDetailsWidth = this._availableWidth - PREVIEW_BOX_PADDING * 2;
let textDetailsWidth = 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;
textDetailsWidth = 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,
let textDetails = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
spacing: PREVIEW_BOX_SPACING });
let detailsName = 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);
textDetails.width = Math.max(PREVIEW_DETAILS_MIN_WIDTH, textDetailsWidth, detailsName.width);
previewDetails.append(previewName, Big.BoxPackFlags.NONE);
textDetails.append(detailsName, Big.BoxPackFlags.NONE);
let previewDescription = new Clutter.Text({ color: ITEM_DISPLAY_NAME_COLOR,
let detailsDescription = 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);
textDetails.append(detailsDescription, Big.BoxPackFlags.NONE);
this._preview.append(previewDetails, Big.BoxPackFlags.EXPAND);
this._preview.append(textDetails, Big.BoxPackFlags.EXPAND);
// Add the preview to global stage to allow for top-level layering
let global = Shell.Global.get();
@ -377,8 +438,6 @@ GenericDisplay.prototype = {
this._displayedItems = {};
this._displayedItemsCount = 0;
this._pageDisplayed = 0;
// GenericDisplayItem
this._activatedItem = null;
this._selectedIndex = -1;
this._keepDisplayCurrent = false;
this.actor = this._grid;
@ -387,27 +446,32 @@ GenericDisplay.prototype = {
height: 24,
spacing: 12,
orientation: Big.BoxOrientation.HORIZONTAL});
this._availableWidthForItemDetails = this._columnWidth;
this.selectedItemDetails = new Big.Box({});
},
//// Public methods ////
setAvailableWidthForItemDetails: function(availableWidth) {
this._availableWidthForItemDetails = availableWidth;
},
getAvailableWidthForItemDetails: function() {
return this._availableWidthForItemDetails;
},
// Sets the search string and displays the matching items.
setSearch: function(text) {
this._search = text.toLowerCase();
this._redisplay(true);
},
// Sets this._activatedItem to the item that is selected and emits 'activated' signal.
// The reason we don't call launch() on the activated item right away is because we want
// the class that contains the display to do all other necessary actions and then call
// doActivate(). Currently, when a selected item is activated we only clear the search
// entry, but when an item that was not selected is clicked, we want to move the selection
// to the clicked item first. This needs to happen in the class that contains the display
// because the selection might be moved from some other display that class contains.
// Launches the item that is currently selected and emits 'activated' signal.
activateSelected: function() {
if (this._selectedIndex != -1) {
let selected = this._findDisplayedByIndex(this._selectedIndex);
this._activatedItem = selected;
selected.launch()
this.emit('activated');
}
},
@ -475,16 +539,6 @@ GenericDisplay.prototype = {
return this._displayedItemsCount > 0;
},
// Highlights the activated item and launches it.
doActivate: function() {
if (this._activatedItem != null) {
// We update the selection, so that in case an item was selected by clicking on it and
// it is different from an item that was previously selected, we can highlight the new selection.
this._selectIndex(this._getIndexOfDisplayedActor(this._activatedItem.actor));
this._activatedItem.launch();
}
},
// Readjusts display layout and the items displayed based on the new dimensions.
updateDimensions: function(width, height, numberOfColumns) {
this._numberOfColumns = numberOfColumns;
@ -567,16 +621,24 @@ GenericDisplay.prototype = {
return;
}
let me = this;
let itemInfo = this._allItems[itemId];
let displayItem = this._createDisplayItem(itemInfo);
displayItem.setShowPreview(true);
displayItem.connect('activate', function() {
me._activatedItem = displayItem;
me.emit('activated');
});
displayItem.connect('activate',
Lang.bind(this,
function() {
// update the selection
this._selectIndex(this._getIndexOfDisplayedActor(displayItem.actor));
this.activateSelected();
}));
displayItem.connect('select',
Lang.bind(this,
function() {
// update the selection
this._selectIndex(this._getIndexOfDisplayedActor(displayItem.actor));
}));
this._grid.add_actor(displayItem.actor);
this._displayedItems[itemId] = displayItem;
this._displayedItemsCount++;
@ -819,16 +881,20 @@ GenericDisplay.prototype = {
return -1;
},
// Selects (e.g. highlights) a display item at the provided index.
// Selects (e.g. highlights) a display item at the provided index,
// updates this.selectedItemDetails actor, and emits 'selected' signal.
_selectIndex: function(index) {
if (this._selectedIndex != -1) {
let prev = this._findDisplayedByIndex(this._selectedIndex);
prev.markSelected(false);
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.emit('selected');
}
}
};

View File

@ -28,13 +28,17 @@ const SIDESHOW_MIN_WIDTH = 250;
const SIDESHOW_SECTION_PADDING_TOP = 6;
const SIDESHOW_SECTION_SPACING = 6;
const SIDESHOW_COLUMNS = 1;
const EXPANDED_SIDESHOW_COLUMNS = 2;
const DETAILS_CORNER_RADIUS = 5;
const DETAILS_BORDER_WIDTH = 1;
const DETAILS_PADDING = 6;
// This is the height of section components other than the item display.
const SIDESHOW_SECTION_MISC_HEIGHT = (LABEL_HEIGHT + SIDESHOW_SECTION_SPACING) * 2 + SIDESHOW_SECTION_PADDING_TOP;
const SIDESHOW_SEARCH_BG_COLOR = new Clutter.Color();
SIDESHOW_SEARCH_BG_COLOR.from_pixel(0xffffffff);
const SIDESHOW_TEXT_COLOR = new Clutter.Color();
SIDESHOW_TEXT_COLOR.from_pixel(0xffffffff);
const DETAILS_BORDER_COLOR = new Clutter.Color();
DETAILS_BORDER_COLOR.from_pixel(0xffffffff);
// Time for initial animation going into overlay mode
const ANIMATION_TIME = 0.5;
@ -65,35 +69,43 @@ const WORKSPACE_GRID_PADDING = 12;
const COLUMNS_FOR_WORKSPACES_REGULAR_SCREEN = 3;
const ROWS_FOR_WORKSPACES_REGULAR_SCREEN = 6;
const WORKSPACES_X_FACTOR_ASIDE_MODE_REGULAR_SCREEN = 4 - 0.25;
const EXPANDED_SIDESHOW_COLUMNS_REGULAR_SCREEN = 2;
const COLUMNS_FOR_WORKSPACES_WIDE_SCREEN = 4;
const ROWS_FOR_WORKSPACES_WIDE_SCREEN = 8;
const WORKSPACES_X_FACTOR_ASIDE_MODE_WIDE_SCREEN = 5 - 0.25;
const EXPANDED_SIDESHOW_COLUMNS_WIDE_SCREEN = 3;
let wideScreen = false;
let displayGridColumnWidth = null;
let displayGridRowHeight = null;
function Sideshow(parent, width) {
this._init(parent, width);
function Sideshow() {
this._init();
}
Sideshow.prototype = {
_init : function(parent, width) {
_init : function() {
let me = this;
this._moreAppsMode = false;
this._moreDocsMode = false;
this._width = width - SIDESHOW_PAD;
let asideXFactor = wideScreen ? WORKSPACES_X_FACTOR_ASIDE_MODE_WIDE_SCREEN : WORKSPACES_X_FACTOR_ASIDE_MODE_REGULAR_SCREEN;
this._expandedSideshowColumns = wideScreen ? EXPANDED_SIDESHOW_COLUMNS_WIDE_SCREEN : EXPANDED_SIDESHOW_COLUMNS_REGULAR_SCREEN;
// this figures out the additional width we can give to the display in the 'More' mode
this._width = displayGridColumnWidth - SIDESHOW_PAD;
// this figures out the additional width we can give to the display in the 'More' mode,
// assuming that we want to keep the columns the same width in both modes
this._additionalWidth = ((this._width + SIDESHOW_PAD) / SIDESHOW_COLUMNS) *
(EXPANDED_SIDESHOW_COLUMNS - SIDESHOW_COLUMNS);
(this._expandedSideshowColumns - SIDESHOW_COLUMNS);
let previewWidth = displayGridColumnWidth * asideXFactor - this._width -
this._additionalWidth - SIDESHOW_SECTION_SPACING * 2;
let global = Shell.Global.get();
this.actor = new Clutter.Group();
parent.add_actor(this.actor);
let icontheme = Gtk.IconTheme.get_default();
this._searchBox = new Big.Box({ background_color: SIDESHOW_SEARCH_BG_COLOR,
corner_radius: 4,
@ -255,37 +267,43 @@ Sideshow.prototype = {
this._docsDisplayControlBox = new Big.Box({x_align: Big.BoxAlignment.CENTER});
this._docsDisplayControlBox.append(this._docDisplay.displayControl, Big.BoxPackFlags.NONE);
this._details = new Big.Box({ x: SIDESHOW_PAD + 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,
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);
/* Proxy the activated signals */
this._appDisplay.connect('activated', function(appDisplay) {
// we allow clicking on an item to launch it, and this unsets the selection
// 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');
});
this._docDisplay.connect('activated', function(docDisplay) {
// we allow clicking on an item to launch it, and this unsets the selection
// 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');
});
this._appDisplay.connect('selected', function(appDisplay) {
// We allow clicking on any item to select it, so if an
// item in the app display is selected, we need to make sure that
// no item in the doc display has the selection.
me._docDisplay.unsetSelected();
me._docDisplay.hidePreview();
});
this._docDisplay.connect('selected', function(docDisplay) {
// We allow clicking on any item to select it, so if an
// item in the doc display is selected, we need to make sure that
// no item in the app display has the selection.
me._appDisplay.unsetSelected();
me._appDisplay.hidePreview();
});
this._appDisplay.connect('redisplayed', function(appDisplay) {
// This can be applicable if app display previously had the selection,
// but it got updated and now has no items, so we can try to move
// the selection to the doc display.
if (!me._appDisplay.hasSelected() && !me._docDisplay.hasSelected())
me._docDisplay.selectFirstItem();
me._ensureItemSelected();
});
this._docDisplay.connect('redisplayed', function(docDisplay) {
if (!me._docDisplay.hasSelected() && !me._appDisplay.hasSelected())
me._appDisplay.selectFirstItem();
me._ensureItemSelected();
});
this._moreAppsLink.connect('clicked',
@ -328,6 +346,22 @@ Sideshow.prototype = {
this._unsetMoreDocsMode();
},
// Ensures that one of the displays has the selection if neither owns it after the
// latest redisplay. This can be applicable if the display that earlier had the
// selection no longer has any items, or if their is a single section being shown
// in the expanded view and it went from having no matching items to having some.
// We first try to place the selection in the applications section, because it is
// displayed above the documents section.
_ensureItemSelected: function() {
if (!this._appDisplay.hasSelected() && !this._docDisplay.hasSelected()) {
if (this._appDisplay.hasItems()) {
this._appDisplay.selectFirstItem();
} else if (this._docDisplay.hasItems()) {
this._docDisplay.selectFirstItem();
}
}
},
// Sets the 'More' mode for browsing applications. Updates the applications section to have more items.
// Slides down the documents section to reveal the additional applications.
_setMoreAppsMode: function() {
@ -428,13 +462,17 @@ Sideshow.prototype = {
if (this._moreAppsMode) {
this._appDisplay.updateDimensions(this._width + this._additionalWidth,
this._itemDisplayHeight + SIDESHOW_SECTION_MISC_HEIGHT,
EXPANDED_SIDESHOW_COLUMNS);
this._expandedSideshowColumns);
this._moreAppsLink.setText("Less...");
this._appsSection.insert_after(this._appsDisplayControlBox, this._appDisplay.actor, Big.BoxPackFlags.NONE);
this.actor.add_actor(this._details);
this._details.append(this._appDisplay.selectedItemDetails, Big.BoxPackFlags.NONE);
} else {
this._appDisplay.updateDimensions(this._width, this._appsSectionDefaultHeight - SIDESHOW_SECTION_MISC_HEIGHT, SIDESHOW_COLUMNS);
this._moreAppsLink.setText("More...");
this._appsSection.remove_actor(this._appsDisplayControlBox);
this.actor.remove_actor(this._details);
this._details.remove_all();
}
this._moreAppsLink.actor.show();
},
@ -540,13 +578,17 @@ Sideshow.prototype = {
if (this._moreDocsMode) {
this._docDisplay.updateDimensions(this._width + this._additionalWidth,
this._itemDisplayHeight + SIDESHOW_SECTION_MISC_HEIGHT,
EXPANDED_SIDESHOW_COLUMNS);
this._expandedSideshowColumns);
this._moreDocsLink.setText("Less...");
this._docsSection.insert_after(this._docsDisplayControlBox, this._docDisplay.actor, Big.BoxPackFlags.NONE);
this.actor.add_actor(this._details);
this._details.append(this._docDisplay.selectedItemDetails, Big.BoxPackFlags.NONE);
} else {
this._docDisplay.updateDimensions(this._width, this._docsSectionDefaultHeight - SIDESHOW_SECTION_MISC_HEIGHT, SIDESHOW_COLUMNS);
this._moreDocsLink.setText("More...");
this._docsSection.remove_actor(this._docsDisplayControlBox);
this.actor.remove_actor(this._details);
this._details.remove_all();
}
this._moreDocsLink.actor.show();
}
@ -594,9 +636,8 @@ Overlay.prototype = {
global.overlay_group.add_actor(this._group);
// TODO - recalculate everything when desktop size changes
let sideshowWidth = displayGridColumnWidth;
this._sideshow = new Sideshow(this._group, sideshowWidth);
this._sideshow = new Sideshow();
this._group.add_actor(this._sideshow.actor);
this._workspaces = null;
this._workspacesBackground = null;
this._sideshow.connect('activated', function(sideshow) {
@ -698,7 +739,7 @@ Overlay.prototype = {
x: displayGridColumnWidth,
y: Panel.PANEL_HEIGHT,
width: displayGridColumnWidth * columnsUsed,
height: global.screen_width - Panel.PANEL_HEIGHT });
height: global.screen_height - Panel.PANEL_HEIGHT });
this._group.add_actor(this._workspacesBackground);
this._workspaces = new Workspaces.Workspaces(workspacesWidth, workspacesHeight, workspacesX, workspacesY,

View File

@ -421,6 +421,12 @@ shell_get_categories_for_desktop_file(const char *desktop_file_name)
return categories_list;
}
/**
* shell_get_event_key_symbol:
*
* Return value: Clutter key value for the key press and release events,
* as specified in clutter-keysyms.h
*/
guint16
shell_get_event_key_symbol(ClutterEvent *event)
{
@ -430,6 +436,19 @@ shell_get_event_key_symbol(ClutterEvent *event)
return event->key.keyval;
}
/**
* shell_get_button_event_click_count:
*
* Return value: click count for button press and release events
*/
guint16
shell_get_button_event_click_count(ClutterEvent *event)
{
g_return_val_if_fail(event->type == CLUTTER_BUTTON_PRESS ||
event->type == CLUTTER_BUTTON_RELEASE, 0);
return event->button.click_count;
}
/**
* shell_global_get:
*

View File

@ -40,6 +40,8 @@ GSList *shell_get_categories_for_desktop_file(const char *desktop_file_name);
guint16 shell_get_event_key_symbol(ClutterEvent *event);
guint16 shell_get_button_event_click_count(ClutterEvent *event);
ShellGlobal *shell_global_get (void);
void shell_global_grab_dbus_service (ShellGlobal *global);