From 48cda5b179e3cd2192209da0fc7ebba751b3f6c4 Mon Sep 17 00:00:00 2001 From: Marina Zhurakhinskaya Date: Sat, 20 Dec 2008 04:27:57 +0000 Subject: [PATCH] Create two base classes: GenericDisplayItem and GenericDisplay, as well as corresponding classes for applications and documents that inherit from them. Use half the height of the sideshow in the overlay mode for the AppDisplay, and the other half for the DocDisplay. Enable moving the selection between the two displays by using up and down arrow keys. Enable activating any item by clicking on it, in addition to activating the currently selected item by pressing Enter. Apply search entry content to both sets of items. svn path=/trunk/; revision=132 --- js/ui/appDisplay.js | 320 ++++++++----------------------- js/ui/docDisplay.js | 184 ++++++++++++++++++ js/ui/genericDisplay.js | 415 ++++++++++++++++++++++++++++++++++++++++ js/ui/overlay.js | 102 ++++++++-- 4 files changed, 770 insertions(+), 251 deletions(-) create mode 100644 js/ui/docDisplay.js create mode 100644 js/ui/genericDisplay.js diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index a9ff0200c..e3345da76 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -3,14 +3,11 @@ const Signals = imports.signals; const Clutter = imports.gi.Clutter; const Gio = imports.gi.Gio; -const Pango = imports.gi.Pango; const Gtk = imports.gi.Gtk; - -const Tidy = imports.gi.Tidy; -const Big = imports.gi.Big; -const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; +const GenericDisplay = imports.ui.genericDisplay; + // TODO - move this into GConf once we're not a plugin anymore // but have taken over metacity // This list is taken from GNOME Online popular applications @@ -37,45 +34,29 @@ const DEFAULT_APPLICATIONS = [ 'vncviewer.desktop' ]; -const APPDISPLAY_NAME_COLOR = new Clutter.Color(); -APPDISPLAY_NAME_COLOR.from_pixel(0xffffffff); -const APPDISPLAY_COMMENT_COLOR = new Clutter.Color(); -APPDISPLAY_COMMENT_COLOR.from_pixel(0xffffffbb); -const APPDISPLAY_BACKGROUND_COLOR = new Clutter.Color(); -APPDISPLAY_BACKGROUND_COLOR.from_pixel(0x000000ff); -const APPDISPLAY_SELECTED_BACKGROUND_COLOR = new Clutter.Color(); -APPDISPLAY_SELECTED_BACKGROUND_COLOR.from_pixel(0x00ff0055); - -const APPDISPLAY_HEIGHT = 50; -const APPDISPLAY_PADDING = 4; - -function AppDisplayItem(node, width) { - this._init(node, width); +/* This class represents a single display item containing information about an application. + * + * appInfo - GAppInfo object containing information about the application + * availableWidth - total width available for the item + */ +function AppDisplayItem(appInfo, availableWidth) { + this._init(appInfo, availableWidth); } AppDisplayItem.prototype = { - _init: function(appInfo, width) { - let me = this; + __proto__: GenericDisplay.GenericDisplayItem.prototype, + + _init : function(appInfo, availableWidth) { + GenericDisplay.GenericDisplayItem.prototype._init.call(this, availableWidth); this._appInfo = appInfo; let name = appInfo.get_name(); + let description = appInfo.get_description(); + let iconTheme = Gtk.IconTheme.get_default(); - this._group = new Clutter.Group({reactive: true, - width: width, - height: APPDISPLAY_HEIGHT}); - this._group.connect('button-press-event', function(group, e) { - me.emit('activate'); - return true; - }); - this._bg = new Big.Box({ background_color: APPDISPLAY_BACKGROUND_COLOR, - corner_radius: 4, - x: 0, y: 0, - width: width, height: APPDISPLAY_HEIGHT }); - this._group.add_actor(this._bg); - - this._icon = new Clutter.Texture({ width: 48, height: 48, x: 0, y: 0 }); + let icon = new Clutter.Texture({ width: 48, height: 48}); let gicon = appInfo.get_icon(); let path = null; if (gicon != null) { @@ -84,266 +65,129 @@ AppDisplayItem.prototype = { path = iconinfo.get_filename(); } - if (path) - this._icon.set_from_file(path); - this._group.add_actor(this._icon); + if (path) { + try { + icon.set_from_file(path); + } catch (e) { + // we can get an error here if the file path doesn't exist on the system + log('Error loading AppDisplayItem icon ' + e); + } + } + this._setItemInfo(name, description, icon); + }, - let comment = appInfo.get_description(); - let text_width = width - (me._icon.width + 4); - this._name = new Clutter.Label({ color: APPDISPLAY_NAME_COLOR, - font_name: "Sans 14px", - width: text_width, - ellipsize: Pango.EllipsizeMode.END, - text: name, - x: this._icon.width + 4, - y: 0}); - this._group.add_actor(this._name); - this._comment = new Clutter.Label({ color: APPDISPLAY_COMMENT_COLOR, - font_name: "Sans 12px", - width: text_width, - ellipsize: Pango.EllipsizeMode.END, - text: comment, - x: this._name.x, - y: this._name.height + 4}) - this._group.add_actor(this._comment); - this.actor = this._group; - }, - launch: function() { - this._appInfo.launch([], null); - }, - appInfo: function () { + //// Public methods //// + + // Returns the application info associated with this display item. + getAppInfo : function () { return this._appInfo; }, - markSelected: function(isSelected) { - let color; - if (isSelected) - color = APPDISPLAY_SELECTED_BACKGROUND_COLOR; - else - color = APPDISPLAY_BACKGROUND_COLOR; - this._bg.background_color = color; + + //// Public method overrides //// + + // Opens an application represented by this display item. + launch : function() { + this._appInfo.launch([], null); } + }; -Signals.addSignalMethods(AppDisplayItem.prototype); - +/* This class represents a display containing a collection of application items. + * The applications are sorted based on their popularity by default, and based on + * their name if some search filter is applied. + * + * width - width available for the display + * height - height available for the display + */ function AppDisplay(width, height) { this._init(width, height); } AppDisplay.prototype = { + __proto__: GenericDisplay.GenericDisplay.prototype, + _init : function(width, height) { + GenericDisplay.GenericDisplay.prototype._init.call(this, width, height); let me = this; - let global = Shell.Global.get(); - this._search = ''; - this._width = width; - this._height = height; this._appMonitor = new Shell.AppMonitor(); this._appsStale = true; this._appMonitor.connect('changed', function(mon) { me._appsStale = true; + // We still need to determine what events other than search can trigger + // a change in the set of applications that are being shown while the + // user in in the overlay mode, however let's redisplay just in case. + me._redisplay(); }); - this._grid = new Tidy.Grid({width: width, height: height}); - this._appSet = {}; // Map - this._displayed = {}; // Map - this._selectedIndex = -1; - this._maxItems = this._height / (APPDISPLAY_HEIGHT + APPDISPLAY_PADDING); - this.actor = this._grid; }, - _refreshCache: function() { - let me = this; + //// Protected method overrides //// + // Gets information about all applications by calling Gio.app_info_get_all(). + _refreshCache : function() { + let me = this; if (!this._appsStale) return; - for (id in this._displayed) - this._displayed[id].destroy(); - this._appSet = {}; - this._displayed = {}; - this._selectedIndex = -1; + this._allItems = {}; let apps = Gio.app_info_get_all(); for (let i = 0; i < apps.length; i++) { let appInfo = apps[i]; let appId = appInfo.get_id(); - this._appSet[appId] = appInfo; + this._allItems[appId] = appInfo; } this._appsStale = false; }, - _removeItem: function(appId) { - let item = this._displayed[appId]; - let group = item.actor; - group.destroy(); - delete this._displayed[appId]; - }, - - _removeAll: function() { - for (appId in this._displayed) - this._removeItem(appId); - }, - - _setDefaultList: function() { - this._removeAll(); + // Sets the list of the displayed items based on the list of DEFAULT_APPLICATIONS. + _setDefaultList : function() { + this._removeAllDisplayItems(); let added = 0; for (let i = 0; i < DEFAULT_APPLICATIONS.length && added < this._maxItems; i++) { let appId = DEFAULT_APPLICATIONS[i]; - let appInfo = this._appSet[appId]; + let appInfo = this._allItems[appId]; if (appInfo) { - this._filterAdd(appId); - added += 1; + this._addDisplayItem(appId); + added += 1; } } }, - _getNDisplayed: function() { - // Is there a better way to do .size() ? - let c = 0; for (i in this._displayed) { c += 1; }; - return c; - }, - - _filterAdd: function(appId) { + // Sorts the list of item ids in-place based on the alphabetical order of the names of + // the items associated with the ids. + _sortItems : function(itemIds) { let me = this; - - let appInfo = this._appSet[appId]; - - let appDisplayItem = new AppDisplayItem(appInfo, this._width); - appDisplayItem.connect('activate', function() { - appDisplayItem.launch(); - me.emit('activated'); + itemIds.sort(function (a,b) { + let appA = me._allItems[a]; + let appB = me._allItems[b]; + return appA.get_name().localeCompare(appB.get_name()); }); - let group = appDisplayItem.actor; - this._grid.add_actor(group); - this._displayed[appId] = appDisplayItem; }, - _filterRemove: function(appId) { - // In the future, do some sort of fade out or other effect here - let item = this._displayed[appId]; - this._removeItem(item); - }, - - _appInfoMatches: function(appInfo, search) { + // Checks if the item info can be a match for the search string by checking + // the name, description, and execution command for the application. + // Item info is expected to be GAppInfo. + // Returns a boolean flag indicating if itemInfo is a match. + _isInfoMatching : function(itemInfo, search) { if (search == null || search == '') return true; - let name = appInfo.get_name().toLowerCase(); + let name = itemInfo.get_name().toLowerCase(); if (name.indexOf(search) >= 0) return true; - let description = appInfo.get_description(); + let description = itemInfo.get_description(); if (description) { description = description.toLowerCase(); if (description.indexOf(search) >= 0) return true; } - let exec = appInfo.get_executable().toLowerCase(); + let exec = itemInfo.get_executable().toLowerCase(); if (exec.indexOf(search) >= 0) return true; return false; }, - _sortApps: function(appIds) { - let me = this; - return appIds.sort(function (a,b) { - let appA = me._appSet[a]; - let appB = me._appSet[b]; - return appA.get_name().localeCompare(appB.get_name()); - }); - }, - - _doSearchFilter: function() { - this._removeAll(); - let matchedApps = []; - for (appId in this._appSet) { - if (matchedApps.length >= this._maxItems) - break; - if (this._displayed[appId]) - continue; - let app = this._appSet[appId]; - if (this._appInfoMatches(app, this._search)) - matchedApps.push(appId); - } - this._sortApps(matchedApps); - for (let i = 0; i < matchedApps.length; i++) { - this._filterAdd(matchedApps[i]); - } - }, - - _redisplay: function() { - this._refreshCache(); - if (!this._search) - this._setDefaultList(); - else - this._doSearchFilter(); - }, - - setSearch: function(text) { - this._search = text.toLowerCase(); - this._redisplay(); - }, - - _findDisplayedByIndex: function(index) { - let displayedActors = this._grid.get_children(); - let actor = displayedActors[index]; - return this._findDisplayedByActor(actor); - }, - - _findDisplayedByActor: function(actor) { - for (appId in this._displayed) { - let item = this._displayed[appId]; - if (item.actor == actor) { - return item; - } - } - return null; - }, - - searchActivate: function() { - if (this._selectedIndex != -1) { - let selected = this._findDisplayedByIndex(this._selectedIndex); - selected.launch(); - this.emit('activated'); - return; - } - let displayedActors = this._grid.get_children(); - if (displayedActors.length != 1) - return; - let selectedActor = displayedActors[0]; - let selectedMenuItem = this._findDisplayedByActor(selectedActor); - selectedMenuItem.launch(); - this.emit('activated'); - }, - - _selectIndex: function(index) { - if (this._selectedIndex != -1) { - let prev = this._findDisplayedByIndex(this._selectedIndex); - prev.markSelected(false); - } - this._selectedIndex = index; - let item = this._findDisplayedByIndex(index); - item.markSelected(true); - }, - - selectUp: function() { - let prev = this._selectedIndex-1; - if (prev < 0) - return; - this._selectIndex(prev); - }, - - selectDown: function() { - let next = this._selectedIndex+1; - let nDisplayed = this._getNDisplayed(); - if (next >= nDisplayed) - return; - this._selectIndex(next); - }, - - show: function() { - this._redisplay(); - this._grid.show(); - }, - - hide: function() { - this._grid.hide(); - } + // Creates an AppDisplayItem based on itemInfo, which is expected be a GAppInfo object. + _createDisplayItem: function(itemInfo) { + return new AppDisplayItem(itemInfo, this._width); + } }; Signals.addSignalMethods(AppDisplay.prototype); diff --git a/js/ui/docDisplay.js b/js/ui/docDisplay.js new file mode 100644 index 000000000..9644c23fb --- /dev/null +++ b/js/ui/docDisplay.js @@ -0,0 +1,184 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const Signals = imports.signals; +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const Gtk = imports.gi.Gtk; + +const Shell = imports.gi.Shell; + +const GenericDisplay = imports.ui.genericDisplay; + +/* This class represents a single display item containing information about a document. + * + * docInfo - GtkRecentInfo object containing information about the document + * availableWidth - total width available for the item + */ +function DocDisplayItem(docInfo, availableWidth) { + this._init(docInfo, availableWidth); +} + +DocDisplayItem.prototype = { + __proto__: GenericDisplay.GenericDisplayItem.prototype, + + _init : function(docInfo, availableWidth) { + GenericDisplay.GenericDisplayItem.prototype._init.call(this, availableWidth); + this._docInfo = docInfo; + + let name = docInfo.get_display_name(); + + // we can possibly display tags in the space for description in the future + let description = ""; + + let icon = new Clutter.Texture({width: 48, height: 48}); + Shell.clutter_texture_set_from_pixbuf(icon, docInfo.get_icon(48)); + + this._setItemInfo(name, description, icon); + }, + + //// Public methods //// + + // Returns the document info associated with this display item. + getDocInfo : function() { + return this._docInfo; + }, + + //// Public method overrides //// + + // Opens a document represented by this display item. + launch : function() { + let appName = this._docInfo.last_application(); + let appData = this._docInfo.get_application_info(appName); + let success = appData[0]; + let appExec = appData[1]; + let count = appData[2]; + let time = appData[3]; + if (success) { + log("Will open a document with the following command: " + appExec); + // TODO: Change this once better support for creating GAppInfo is added to + // GtkRecentInfo, as right now this relies on the fact that the file uri is + // already a part of appExec, so we don't supply any files to appInfo.launch(). + let appInfo = Gio.app_info_create_from_commandline(appExec, null, 0, null); + appInfo.launch([], null, null); + } else { + log("Failed to get application info for " + this._docInfo.get_uri()); + } + } + +}; + +/* This class represents a display containing a collection of document items. + * The documents are sorted by how recently they were last visited. + * + * width - width available for the display + * height - height available for the display + */ +function DocDisplay(width, height) { + this._init(width, height); +} + +DocDisplay.prototype = { + __proto__: GenericDisplay.GenericDisplay.prototype, + + _init : function(width, height) { + GenericDisplay.GenericDisplay.prototype._init.call(this, width, height); + let me = this; + this._recentManager = Gtk.RecentManager.get_default(); + this._docsStale = true; + this._recentManager.connect('changed', function(recentManager, 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. + // Also, we might be displaying remote documents, like Google Docs, in the future + // which might be edited by someone else. + me._redisplay(); + }); + }, + + //// Protected method overrides //// + + // Gets the list of recent items from the recent items manager. + _refreshCache : function() { + let me = this; + if (!this._docsStale) + return; + this._allItems = {}; + let docs = this._recentManager.get_items(); + for (let i = 0; i < docs.length; i++) { + let docInfo = docs[i]; + let docId = docInfo.get_uri(); + // we use GtkRecentInfo URI as an item Id + this._allItems[docId] = docInfo; + } + this._docsStale = false; + }, + + // Sets the list of the displayed items based on how recently they were last visited. + _setDefaultList : function() { + this._removeAllDisplayItems(); + + // It seems to be an implementation detail of the Mozilla JavaScript that object + // properties are returned during the iteration in the same order in which they were + // defined, but it is not a guarantee according to this + // https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Statements/for...in + // So while this._allItems associative array seems to always be ordered by last visited, + // as the results of this._recentManager.get_items() based on which it is constructed are, + // we should do the sorting manually anyway. + // TODO: would it be better to store an additional array of doc ids as they are + // returned by this._recentManager.get_items() to avoid having to do this sorting? + // This function is called each time the search string is set back to '', so we are + // doing the sorting over the same items multiple times. + let docIds = []; + for (docId in this._allItems) { + docIds.push(docId); + } + this._sortItems(docIds); + + let added = 0; + for (let i = 0; i < docIds.length && added < this._maxItems; i++) { + let docInfo = this._allItems[docIds[i]]; + // docInfo.exists() checks if the resource still exists + if (docInfo.exists()) { + this._addDisplayItem(docIds[i]); + added += 1; + } + } + }, + + // Sorts the list of item ids in-place based on how recently the items associated with + // the ids were last visited. + _sortItems : function(itemIds) { + let me = this; + itemIds.sort(function (a,b) { + let docA = me._allItems[a]; + let docB = me._allItems[b]; + if (docA.get_visited() > docB.get_visited()) + return -1; + else if (docA.get_visited() < docB.get_visited()) + return 1; + else + return 0; + }); + }, + + // Checks if the item info can be a match for the search string by checking + // the name of the document. Item info is expected to be GtkRecentInfo. + // Returns a boolean flag indicating if itemInfo is a match. + _isInfoMatching : function(itemInfo, search) { + if (search == null || search == '') + return true; + let name = itemInfo.get_display_name().toLowerCase(); + if (name.indexOf(search) >= 0) + return true; + // TODO: we can also check doc URIs, so that + // if you search for a directory name, we display recent files from it + return false; + }, + + // Creates a DocDisplayItem based on itemInfo, which is expected be a GtkRecentInfo object. + _createDisplayItem: function(itemInfo) { + return new DocDisplayItem(itemInfo, this._width); + } +}; + +Signals.addSignalMethods(DocDisplay.prototype); diff --git a/js/ui/genericDisplay.js b/js/ui/genericDisplay.js new file mode 100644 index 000000000..82dd209f6 --- /dev/null +++ b/js/ui/genericDisplay.js @@ -0,0 +1,415 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const Signals = imports.signals; +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const Pango = imports.gi.Pango; +const Gtk = imports.gi.Gtk; + +const Tidy = imports.gi.Tidy; +const Big = imports.gi.Big; +const Shell = imports.gi.Shell; + +const ITEM_DISPLAY_NAME_COLOR = new Clutter.Color(); +ITEM_DISPLAY_NAME_COLOR.from_pixel(0xffffffff); +const ITEM_DISPLAY_DESCRIPTION_COLOR = new Clutter.Color(); +ITEM_DISPLAY_DESCRIPTION_COLOR.from_pixel(0xffffffbb); +const ITEM_DISPLAY_BACKGROUND_COLOR = new Clutter.Color(); +ITEM_DISPLAY_BACKGROUND_COLOR.from_pixel(0x00000000); +const ITEM_DISPLAY_SELECTED_BACKGROUND_COLOR = new Clutter.Color(); +ITEM_DISPLAY_SELECTED_BACKGROUND_COLOR.from_pixel(0x00ff0055); + +const ITEM_DISPLAY_HEIGHT = 50; +const ITEM_DISPLAY_PADDING = 4; + +/* 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. + * + * availableWidth - total width available for the item + */ +function GenericDisplayItem(availableWidth) { + this._init(name, description, icon, availableWidth); +} + +GenericDisplayItem.prototype = { + _init: function(availableWidth) { + this._availableWidth = availableWidth; + let me = this; + + this._group = new Clutter.Group({reactive: true, + width: availableWidth, + height: ITEM_DISPLAY_HEIGHT}); + this._group.connect('button-press-event', function(group, e) { + me.emit('activate'); + return true; + }); + + this._bg = new Big.Box({ background_color: ITEM_DISPLAY_BACKGROUND_COLOR, + corner_radius: 4, + x: 0, y: 0, + width: availableWidth, height: ITEM_DISPLAY_HEIGHT }); + this._group.add_actor(this._bg); + + this._name = null; + this._description = null; + this._icon = null; + + this.actor = this._group; + }, + + //// Public methods //// + + // Highlights the item by setting a different background color than the default + // if isSelected is true, removes the highlighting otherwise. + markSelected: function(isSelected) { + let color; + if (isSelected) + color = ITEM_DISPLAY_SELECTED_BACKGROUND_COLOR; + else + color = ITEM_DISPLAY_BACKGROUND_COLOR; + this._bg.background_color = color; + }, + + //// Pure virtual public methods //// + + // Performes an action associated with launching this item, such as opening a file or an application. + launch: function() { + throw new Error("Not implemented"); + }, + + //// Protected methods //// + + /* + * Creates the graphical elements for the item based on the item information. + * + * nameText - name of the item + * descriptionText - short description of the item + * iconActor - ClutterTexture containing the icon image which should be 48x48 pixels + */ + _setItemInfo: function(nameText, descriptionText, iconActor) { + if (this._name != null) { + // this also removes this._name from the parent container, + // so we don't need to call this._group.remove_actor(this._name) directly + this._name.destroy(); + this._name = null; + } + if (this._description != null) { + this._description.destroy(); + this._description = null; + } + if (this._icon != null) { + // though we get the icon from elsewhere, we assume its ownership here, + // and therefore should be responsible for distroying it + this._icon.destroy(); + this._icon = null; + } + + this._icon = iconActor; + this._icon.x = 0; + this._icon.y = 0; + this._group.add_actor(this._icon); + + let text_width = this._availableWidth - (this._icon.width + 4); + this._name = new Clutter.Label({ color: ITEM_DISPLAY_NAME_COLOR, + font_name: "Sans 14px", + width: text_width, + ellipsize: Pango.EllipsizeMode.END, + text: nameText, + x: this._icon.width + 4, + y: 0}); + this._group.add_actor(this._name); + this._description = new Clutter.Label({ color: ITEM_DISPLAY_DESCRIPTION_COLOR, + font_name: "Sans 12px", + width: text_width, + ellipsize: Pango.EllipsizeMode.END, + text: descriptionText, + x: this._name.x, + y: this._name.height + 4}) + this._group.add_actor(this._description); + } +}; + +Signals.addSignalMethods(GenericDisplayItem.prototype); + +/* This is a virtual class that represents a display containing a collection of items + * that can be filtered with a search string. + * + * width - width available for the display + * height - height available for the display + */ +function GenericDisplay(width, height) { + this._init(width, height); +} + +GenericDisplay.prototype = { + _init : function(width, height) { + this._search = ''; + this._width = width; + this._height = height; + this._grid = new Tidy.Grid({width: width, height: height}); + // map where Object represents the item info + this._allItems = {}; + // map + this._displayedItems = {}; + this._displayedItemsCount = 0; + // GenericDisplayItem + this._activatedItem = null; + this._selectedIndex = -1; + this._keepDisplayCurrent = false; + this._maxItems = this._height / (ITEM_DISPLAY_HEIGHT + ITEM_DISPLAY_PADDING); + this.actor = this._grid; + }, + + //// Public methods //// + + // Sets the search string and displays the matching items. + setSearch: function(text) { + this._search = text.toLowerCase(); + this._redisplay(); + }, + + // 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. + activateSelected: function() { + if (this._selectedIndex != -1) { + let selected = this._findDisplayedByIndex(this._selectedIndex); + this._activatedItem = selected; + this.emit('activated'); + } + }, + + // Moves the selection one up. If the selection was already on the top item, it's moved + // to the bottom one. Returns true if the selection actually moved up, false if it wrapped + // around to the bottom. + selectUp: function() { + let selectedUp = true; + let prev = this._selectedIndex - 1; + if (this._selectedIndex <= 0) { + prev = this._displayedItemsCount - 1; + selectedUp = false; + } + this._selectIndex(prev); + return selectedUp; + }, + + // Moves the selection one down. If the selection was already on the bottom item, it's moved + // to the top one. Returns true if the selection actually moved down, false if it wrapped + // around to the top. + selectDown: function() { + let selectedDown = true; + let next = this._selectedIndex + 1; + if (this._selectedIndex == this._displayedItemsCount - 1) { + next = 0; + selectedDown = false; + } + this._selectIndex(next); + return selectedDown; + }, + + // Selects the first item among the displayed items. + selectFirstItem: function() { + if (this.hasItems()) + this._selectIndex(0); + }, + + // Selects the last item among the displayed items. + selectLastItem: function() { + if (this.hasItems()) + this._selectIndex(this._displayedItemsCount - 1); + }, + + // Returns true if the display has some item selected. + hasSelected: function() { + return this._selectedIndex != -1; + }, + + // Removes selection if some display item is selected. + unsetSelected: function() { + this._selectIndex(-1); + }, + + // Returns true if the display has any displayed items. + hasItems: function() { + 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(); + } + }, + + // Updates the displayed items and makes the display actor visible. + show: function() { + this._keepDisplayCurrent = true; + this._redisplay(); + this._grid.show(); + }, + + // Hides the display actor. + hide: function() { + this._grid.hide(); + this._keepDisplayCurrent = false; + }, + + //// Protected methods //// + + // Creates a display item based on the information associated with itemId + // and adds it to the displayed items. + _addDisplayItem : function(itemId) { + if (this._displayedItems.hasOwnProperty(itemId)) { + log("Tried adding a display item for " + itemId + ", but an item with this item id is already among displayed items."); + return; + } + + let me = this; + + let itemInfo = this._allItems[itemId]; + let displayItem = this._createDisplayItem(itemInfo); + + displayItem.connect('activate', function() { + me._activatedItem = displayItem; + me.emit('activated'); + }); + this._grid.add_actor(displayItem.actor); + this._displayedItems[itemId] = displayItem; + this._displayedItemsCount++; + }, + + // Removes an item identifed by the itemId from the displayed items. + _removeDisplayItem: function(itemId) { + let displayItem = this._displayedItems[itemId]; + displayItem.actor.destroy(); + delete this._displayedItems[itemId]; + this._displayedItemsCount--; + }, + + // Removes all displayed items. + _removeAllDisplayItems: function() { + for (itemId in this._displayedItems) + this._removeDisplayItem(itemId); + }, + + // Updates the displayed items, applying the search string if one exists. + _redisplay: function() { + if (!this._keepDisplayCurrent) + return; + + this._refreshCache(); + if (!this._search) + this._setDefaultList(); + else + this._doSearchFilter(); + + if (this.hasSelected()) { + this._selectedIndex = -1; + this.selectFirstItem(); + } + + this.emit('redisplayed'); + }, + + //// Pure virtual protected methods //// + + // Performs the steps needed to have the latest information about the items. + _refreshCache: function() { + throw new Error("Not implemented"); + }, + + // Sets the list of the displayed items based on the default sorting order. + // The default sorting order is specific to each implementing class. + _setDefaultList: function() { + throw new Error("Not implemented"); + }, + + // Sorts the list of item ids in-place based on the order in which the items + // associated with the ids should be displayed. + _sortItems: function(itemIds) { + throw new Error("Not implemented"); + }, + + // Checks if the item info can be a match for the search string. + // Returns a boolean flag indicating if that's the case. + _isInfoMatching: function(itemInfo, search) { + throw new Error("Not implemented"); + }, + + // Creates a display item based on itemInfo. + _createDisplayItem: function(itemInfo) { + throw new Error("Not implemented"); + }, + + //// Private methods //// + + // Applies the search string to the list of items to find matches, + // and displays up to this._maxItems that matched. + _doSearchFilter: function() { + this._removeAllDisplayItems(); + let matchedItems = []; + for (itemId in this._allItems) { + let item = this._allItems[itemId]; + if (this._isInfoMatching(item, this._search)) + matchedItems.push(itemId); + } + this._sortItems(matchedItems); + for (let i = 0; i < matchedItems.length && i < this._maxItems; i++) { + this._addDisplayItem(matchedItems[i]); + } + }, + + // Returns a display item based on its index in the ordering of the + // display children. + _findDisplayedByIndex: function(index) { + let displayedActors = this._grid.get_children(); + let actor = displayedActors[index]; + return this._findDisplayedByActor(actor); + }, + + // Returns a display item based on the actor that represents it in + // the display. + _findDisplayedByActor: function(actor) { + for (itemId in this._displayedItems) { + let item = this._displayedItems[itemId]; + if (item.actor == actor) { + return item; + } + } + return null; + }, + + // Returns and index that the actor has in the ordering of the display's + // children. + _getIndexOfDisplayedActor: function(actor) { + let children = this._grid.get_children(); + for (let i = 0; i < children.length; i++) { + if (children[i] == actor) + return i; + } + return -1; + }, + + // Selects (e.g. highlights) a display item at the provided index. + _selectIndex: function(index) { + if (this._selectedIndex != -1) { + let prev = this._findDisplayedByIndex(this._selectedIndex); + prev.markSelected(false); + } + this._selectedIndex = index; + if (index != -1 && index < this._displayedItemsCount) { + let item = this._findDisplayedByIndex(index); + item.markSelected(true); + } + } +}; + +Signals.addSignalMethods(GenericDisplay.prototype); diff --git a/js/ui/overlay.js b/js/ui/overlay.js index c6e495f19..d7bea99fc 100644 --- a/js/ui/overlay.js +++ b/js/ui/overlay.js @@ -10,16 +10,19 @@ const Gtk = imports.gi.Gtk; const Workspaces = imports.ui.workspaces; const Main = imports.ui.main; const Panel = imports.ui.panel; -const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; const Big = imports.gi.Big; const AppDisplay = imports.ui.appDisplay; +const DocDisplay = imports.ui.docDisplay; const OVERLAY_BACKGROUND_COLOR = new Clutter.Color(); OVERLAY_BACKGROUND_COLOR.from_pixel(0x000000ff); const SIDESHOW_PAD = 6; +const SIDESHOW_PAD_BOTTOM = 60; const SIDESHOW_MIN_WIDTH = 250; +const SIDESHOW_SECTION_PAD = 10; +const SIDESHOW_SECTION_LABEL_PAD_BOTTOM = 6; const SIDESHOW_SEARCH_BG_COLOR = new Clutter.Color(); SIDESHOW_SEARCH_BG_COLOR.from_pixel(0xffffffff); const SIDESHOW_TEXT_COLOR = new Clutter.Color(); @@ -57,15 +60,18 @@ Sideshow.prototype = { searchIconTexture.set_from_file(searchIconPath); this.actor.add_actor(searchIconTexture); + // We need to initialize the text for the entry to have the cursor displayed + // in it. See http://bugzilla.openedhand.com/show_bug.cgi?id=1365 this._searchEntry = new Clutter.Entry({ font_name: "Sans 14px", x: searchIconTexture.x + searchIconTexture.width + 4, y: searchIconTexture.y, width: rect.width - (searchIconTexture.x), - height: searchIconTexture.height}); + height: searchIconTexture.height, + text: ""}); this.actor.add_actor(this._searchEntry); - global.stage.set_key_focus(this._searchEntry); + global.stage.set_key_focus(this._searchEntry); this._searchQueued = false; this._searchActive = false; this._searchEntry.connect('notify::text', function (se, prop) { @@ -76,12 +82,15 @@ Sideshow.prototype = { me._searchQueued = false; me._searchActive = text != ''; me._appDisplay.setSearch(text); + me._docDisplay.setSearch(text); return false; }); }); this._searchEntry.connect('activate', function (se) { - me._searchEntry.text = ''; - me._appDisplay.searchActivate(); + // only one of the displays will have an item selected, so it's ok to + // call activateSelected() on both of them + me._appDisplay.activateSelected(); + me._docDisplay.activateSelected(); return true; }); this._searchEntry.connect('key-press-event', function (se, e) { @@ -90,10 +99,26 @@ Sideshow.prototype = { me._searchEntry.text = ''; return true; } else if (code == 111) { - me._appDisplay.selectUp(); + // selectUp and selectDown wrap around in their respective displays + // too, but there doesn't seem to be any flickering if we first select + // something in one display, but then unset the selection, and move + // it to the other display, so it's ok to do that. + if (me._appDisplay.hasSelected() && !me._appDisplay.selectUp() && me._docDisplay.hasItems()) { + me._appDisplay.unsetSelected(); + me._docDisplay.selectLastItem(); + } else if (me._docDisplay.hasSelected() && !me._docDisplay.selectUp() && me._appDisplay.hasItems()) { + me._docDisplay.unsetSelected(); + me._appDisplay.selectLastItem(); + } return true; } else if (code == 116) { - me._appDisplay.selectDown(); + if (me._appDisplay.hasSelected() && !me._appDisplay.selectDown() && me._docDisplay.hasItems()) { + me._appDisplay.unsetSelected(); + me._docDisplay.selectFirstItem(); + } else if (me._docDisplay.hasSelected() && !me._docDisplay.selectDown() && me._appDisplay.hasItems()) { + me._docDisplay.unsetSelected(); + me._appDisplay.selectFirstItem(); + } return true; } return false; @@ -103,24 +128,75 @@ Sideshow.prototype = { font_name: "Sans Bold 14px", text: "Applications", x: SIDESHOW_PAD, - y: this._searchEntry.y + this._searchEntry.height + 10, + y: this._searchEntry.y + this._searchEntry.height + SIDESHOW_SECTION_PAD, height: 16}); this.actor.add_actor(appsText); - let menuY = appsText.y + appsText.height + 6; - this._appDisplay = new AppDisplay.AppDisplay(width, global.screen_height - menuY); + let sectionLabelHeight = appsText.height + SIDESHOW_SECTION_LABEL_PAD_BOTTOM + let menuY = appsText.y + sectionLabelHeight; + + let itemDisplayHeight = global.screen_height - menuY - SIDESHOW_SECTION_PAD - sectionLabelHeight - SIDESHOW_PAD_BOTTOM; + this._appDisplay = new AppDisplay.AppDisplay(width, itemDisplayHeight / 2); this._appDisplay.actor.x = SIDESHOW_PAD; this._appDisplay.actor.y = menuY; this.actor.add_actor(this._appDisplay.actor); - /* Proxy the activated signal */ + let docsText = new Clutter.Label({ color: SIDESHOW_TEXT_COLOR, + font_name: "Sans Bold 14px", + text: "Recent Documents", + x: SIDESHOW_PAD, + y: menuY + (itemDisplayHeight / 2) + SIDESHOW_SECTION_PAD, + height: 16}); + this.actor.add_actor(docsText); + + this._docDisplay = new DocDisplay.DocDisplay(width, itemDisplayHeight / 2); + this._docDisplay.actor.x = SIDESHOW_PAD; + this._docDisplay.actor.y = docsText.y + docsText.height + 6; + this.actor.add_actor(this._docDisplay.actor); + + /* Proxy the activated signals */ this._appDisplay.connect('activated', function(appDisplay) { - me.emit('activated'); + me._searchEntry.text = ''; + // 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.doActivate(); + me.emit('activated'); + }); + this._docDisplay.connect('activated', function(docDisplay) { + me._searchEntry.text = ''; + // 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._docDisplay.doActivate(); + me.emit('activated'); + }); + 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(); + }); + this._docDisplay.connect('redisplayed', function(docDisplay) { + if (!me._docDisplay.hasSelected() && !me._appDisplay.hasSelected()) + me._appDisplay.selectFirstItem(); }); }, show: function() { - this._appDisplay.show(); + this._appDisplay.selectFirstItem(); + if (!this._appDisplay.hasSelected()) + this._docDisplay.selectFirstItem(); + else + this._docDisplay.unsetSelected(); + this._appDisplay.show(); + this._docDisplay.show(); + }, + + hide: function() { } }; Signals.addSignalMethods(Sideshow.prototype);