diff --git a/js/ui/docDisplay.js b/js/ui/docDisplay.js index 17c3d415f..d2afe04f6 100644 --- a/js/ui/docDisplay.js +++ b/js/ui/docDisplay.js @@ -1,487 +1,11 @@ /* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ -const Clutter = imports.gi.Clutter; -const Lang = imports.lang; -const Shell = imports.gi.Shell; -const Signals = imports.signals; -const St = imports.gi.St; -const Mainloop = imports.mainloop; const Gettext = imports.gettext.domain('gnome-shell'); const _ = Gettext.gettext; const DocInfo = imports.misc.docInfo; -const DND = imports.ui.dnd; -const GenericDisplay = imports.ui.genericDisplay; -const Main = imports.ui.main; const Search = imports.ui.search; -const MAX_DASH_DOCS = 50; -const DASH_DOCS_ICON_SIZE = 16; - -const DEFAULT_SPACING = 4; - -/* This class represents a single display item containing information about a document. - * We take the current number of seconds in the constructor to avoid looking up the current - * time for every item when they are created in a batch. - * - * docInfo - DocInfo object containing information about the document - * currentSeconds - current number of seconds since the epoch - */ -function DocDisplayItem(docInfo, currentSecs) { - this._init(docInfo, currentSecs); -} - -DocDisplayItem.prototype = { - __proto__: GenericDisplay.GenericDisplayItem.prototype, - - _init : function(docInfo, currentSecs) { - GenericDisplay.GenericDisplayItem.prototype._init.call(this); - this._docInfo = docInfo; - - this._setItemInfo(docInfo.name, ''); - - this._timeoutTime = -1; - this._resetTimeDisplay(currentSecs); - }, - - //// Public methods //// - - getUpdateTimeoutTime: function() { - return this._timeoutTime; - }, - - // Update any relative-time based displays for this item. - redisplay: function(currentSecs) { - this._resetTimeDisplay(currentSecs); - }, - - //// Public method overrides //// - - // Opens a document represented by this display item. - launch : function() { - this._docInfo.launch(); - }, - - //// Protected method overrides //// - - // Returns an icon for the item. - _createIcon : function() { - return this._docInfo.createIcon(GenericDisplay.ITEM_DISPLAY_ICON_SIZE); - }, - - // Returns a preview icon for the item. - _createPreviewIcon : function() { - return this._docInfo.createIcon(GenericDisplay.PREVIEW_ICON_SIZE); - }, - - // Creates and returns a large preview icon, but only if this._docInfo is an image file - // and we were able to generate a pixbuf from it successfully. - _createLargePreviewIcon : function() { - if (this._docInfo.mimeType == null || this._docInfo.mimeType.indexOf('image/') != 0) - return null; - - try { - return St.TextureCache.get_default().load_uri_sync(St.TextureCachePolicy.NONE, - this._docInfo.uri, -1, -1); - } catch (e) { - // An exception will be raised when the image format isn't know - /* FIXME: http://bugzilla.gnome.org/show_bug.cgi?id=591480: should - * only ignore GDK_PIXBUF_ERROR_UNKNOWN_TYPE. */ - return null; - } - }, - - //// Drag and Drop //// - - shellWorkspaceLaunch: function() { - this.launch(); - }, - - //// Private Methods //// - - // Updates the last visited time displayed in the description text for the item. - _resetTimeDisplay: function(currentSecs) { - let lastSecs = this._docInfo.timestamp; - let timeDelta = currentSecs - lastSecs; - let [text, nextUpdate] = global.format_time_relative_pretty(timeDelta); - this._timeoutTime = currentSecs + nextUpdate; - this._setDescriptionText(text); - } -}; - -/* This class represents a display containing a collection of document items. - * The documents are sorted by how recently they were last visited. - */ -function DocDisplay() { - this._init(); -} - -DocDisplay.prototype = { - __proto__: GenericDisplay.GenericDisplay.prototype, - - _init : function() { - GenericDisplay.GenericDisplay.prototype._init.call(this); - // We keep a single timeout callback for updating last visited times - // for all the items in the display. This avoids creating individual - // callbacks for each item in the display. So proper time updates - // for individual items and item details depend on the item being - // associated with one of the displays. - this._updateTimeoutTargetTime = -1; - this._updateTimeoutId = 0; - - this._docManager = DocInfo.getDocManager(); - this._docsStale = true; - this._docManager.connect('changed', Lang.bind(this, function(mgr, userData) { - this._docsStale = true; - // Changes in local recent files should not happen when we are in the Overview 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. - this._redisplay(GenericDisplay.RedisplayFlags.NONE); - })); - - this.connect('destroy', Lang.bind(this, function (o) { - if (this._updateTimeoutId > 0) - Mainloop.source_remove(this._updateTimeoutId); - })); - }, - - //// Protected method overrides //// - - // Gets the list of recent items from the recent items manager. - _refreshCache : function() { - if (!this._docsStale) - return true; - this._allItems = {}; - Lang.copyProperties(this._docManager.getInfosByUri(), this._allItems); - this._docsStale = false; - return false; - }, - - // Sets the list of the displayed items based on how recently they were last visited. - _setDefaultList : function() { - // 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 - // While this._allItems associative array seems to always be ordered by last added, - // as the results of this._recentManager.get_items() based on which it is constructed are, - // we should do the sorting manually because we want the order to be based on last visited. - // - // This function is called each time the search string is set back to '' or we display - // the Overview, so we are doing the sorting over the same items multiple times if the list - // of recent items didn't change. We could store an additional array of doc ids and sort - // them once when they are returned by this._recentManager.get_items() to avoid having to do - // this sorting each time, but the sorting seems to be very fast anyway, so there is no need - // to introduce an additional class variable. - this._matchedItems = {}; - this._matchedItemKeys = []; - let docIdsToRemove = []; - for (docId in this._allItems) { - this._matchedItems[docId] = 1; - this._matchedItemKeys.push(docId); - } - - for (docId in docIdsToRemove) { - delete this._allItems[docId]; - } - - this._matchedItemKeys.sort(Lang.bind(this, this._compareItems)); - }, - - // Compares items associated with the item ids based on how recently the items - // were last visited. - // Returns an integer value indicating the result of the comparison. - _compareItems : function(itemIdA, itemIdB) { - let docA = this._allItems[itemIdA]; - let docB = this._allItems[itemIdB]; - - return docB.timestamp - docA.timestamp; - }, - - // 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 (!itemInfo.exists()) - return false; - - if (search == null || search == '') - return true; - - let name = itemInfo.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 to be a DocInfo object. - _createDisplayItem: function(itemInfo) { - let currentSecs = new Date().getTime() / 1000; - let docDisplayItem = new DocDisplayItem(itemInfo, currentSecs); - this._updateTimeoutCallback(docDisplayItem, currentSecs); - return docDisplayItem; - }, - - //// Private Methods //// - - // A callback function that redisplays the items, updating their descriptions, - // and sets up a new timeout callback. - _docTimeout: function () { - let currentSecs = new Date().getTime() / 1000; - this._updateTimeoutId = 0; - this._updateTimeoutTargetTime = -1; - for (let docId in this._displayedItems) { - let docDisplayItem = this._displayedItems[docId]; - docDisplayItem.redisplay(currentSecs); - this._updateTimeoutCallback(docDisplayItem, currentSecs); - } - return false; - }, - - // Updates the timeout callback if the timeout time for the docDisplayItem - // is earlier than the target time for the current timeout callback. - _updateTimeoutCallback: function (docDisplayItem, currentSecs) { - let timeoutTime = docDisplayItem.getUpdateTimeoutTime(); - if (this._updateTimeoutTargetTime < 0 || timeoutTime < this._updateTimeoutTargetTime) { - if (this._updateTimeoutId > 0) - Mainloop.source_remove(this._updateTimeoutId); - this._updateTimeoutId = Mainloop.timeout_add_seconds(timeoutTime - currentSecs, Lang.bind(this, this._docTimeout)); - this._updateTimeoutTargetTime = timeoutTime; - } - } -}; - -Signals.addSignalMethods(DocDisplay.prototype); - -function DashDocDisplayItem(docInfo) { - this._init(docInfo); -} - -DashDocDisplayItem.prototype = { - _init: function(docInfo) { - this._info = docInfo; - this._icon = docInfo.createIcon(DASH_DOCS_ICON_SIZE); - - this.actor = new St.Clickable({ style_class: 'recent-docs-item', - reactive: true, - x_align: St.Align.START }); - - let box = new St.BoxLayout({ style_class: 'recent-docs-item-box' }); - this.actor.set_child(box); - - box.add(this._icon); - - let text = new St.Label({ text: docInfo.name }); - box.add(text); - - this.actor.connect('clicked', Lang.bind(this, function () { - docInfo.launch(); - Main.overview.hide(); - })); - - this.actor._delegate = this; - let draggable = DND.makeDraggable(this.actor); - draggable.connect('drag-begin', - Lang.bind(this, function() { - Main.overview.beginItemDrag(this); - })); - draggable.connect('drag-end', - Lang.bind(this, function() { - Main.overview.endItemDrag(this); - })); - }, - - getUri: function() { - return this._info.uri; - }, - - getDragActorSource: function() { - return this._icon; - }, - - getDragActor: function(stageX, stageY) { - this.dragActor = this._info.createIcon(DASH_DOCS_ICON_SIZE); - return this.dragActor; - }, - - //// Drag and drop functions //// - - shellWorkspaceLaunch: function () { - this._info.launch(); - } -}; - -/** - * Class used to display two column recent documents in the dash - */ -function DashDocDisplay() { - this._init(); -} - -DashDocDisplay.prototype = { - _init: function() { - this.actor = new Shell.GenericContainer(); - this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth)); - this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight)); - this.actor.connect('allocate', Lang.bind(this, this._allocate)); - this._workId = Main.initializeDeferredWork(this.actor, Lang.bind(this, this._redisplay)); - - this._actorsByUri = {}; - - this._docManager = DocInfo.getDocManager(); - this._docManager.connect('changed', Lang.bind(this, this._onDocsChanged)); - this._pendingDocsChange = true; - this._checkDocExistence = false; - }, - - _getPreferredWidth: function(actor, forHeight, alloc) { - let children = actor.get_children(); - - // We use two columns maximum. Just take the min and natural size of the - // first two items, even though strictly speaking it's not correct; we'd - // need to calculate how many items we could fit for the height, then - // take the biggest preferred width for each column. - // In practice the dash gets a fixed width anyways. - - // If we have one child, add its minimum and natural size - if (children.length > 0) { - let [minSize, naturalSize] = children[0].get_preferred_width(forHeight); - alloc.min_size += minSize; - alloc.natural_size += naturalSize; - } - // If we have two, add its size, plus DEFAULT_SPACING - if (children.length > 1) { - let [minSize, naturalSize] = children[1].get_preferred_width(forHeight); - alloc.min_size += DEFAULT_SPACING + minSize; - alloc.natural_size += DEFAULT_SPACING + naturalSize; - } - }, - - _getPreferredHeight: function(actor, forWidth, alloc) { - let children = actor.get_children(); - - // The width of an item is our allocated width, minus spacing, divided in half. - this._itemWidth = Math.floor((forWidth - DEFAULT_SPACING) / 2); - - let maxNatural = 0; - for (let i = 0; i < children.length; i++) { - let child = children[i]; - let [minSize, naturalSize] = child.get_preferred_height(this._itemWidth); - maxNatural = Math.max(maxNatural, naturalSize); - } - - this._itemHeight = maxNatural; - - let firstColumnChildren = Math.ceil(children.length / 2); - alloc.natural_size = (firstColumnChildren * maxNatural + - (firstColumnChildren - 1) * DEFAULT_SPACING); - }, - - _allocate: function(actor, box, flags) { - let width = box.x2 - box.x1; - let height = box.y2 - box.y1; - - // Make sure this._itemWidth/Height have been computed, even - // if the parent actor didn't check our size before allocating. - // (Not clear if that is required or not as a Clutter - // invariant; this is safe and cheap because of caching.) - actor.get_preferred_height(width); - - let children = actor.get_children(); - - let x = 0; - let y = 0; - let columnIndex = 0; - let i = 0; - // Loop over the children, going vertically down first. When we run - // out of vertical space (our y variable is bigger than box.y2), switch - // to the second column. - while (i < children.length) { - let child = children[i]; - - if (y + this._itemHeight > box.y2) { - // Is this the second column, or we're in - // the first column and can't even fit one - // item? In that case, break. - if (columnIndex == 1 || i == 0) { - break; - } - // Set x to the halfway point. - columnIndex += 1; - x = x + this._itemWidth + DEFAULT_SPACING; - // And y is back to the top. - y = 0; - // Retry this same item, now that we're in the second column. - // By looping back to the top here, we re-test the size - // again for the second column. - continue; - } - - let childBox = new Clutter.ActorBox(); - childBox.x1 = x; - childBox.y1 = y; - childBox.x2 = childBox.x1 + this._itemWidth; - childBox.y2 = y + this._itemHeight; - - y = childBox.y2 + DEFAULT_SPACING; - - child.allocate(childBox, flags); - this.actor.set_skip_paint(child, false); - - i++; - } - - if (this._checkDocExistence) { - // Now we know how many docs we are displaying, queue a check to see if any of them - // have been deleted. If they are deleted, then we'll get a 'changed' signal; since - // we'll now be displaying items we weren't previously, we'll check again to see - // if they were deleted, and so forth and so on. - // TODO: We should change this to ask for as many as we can fit in the given space: - // https://bugzilla.gnome.org/show_bug.cgi?id=603522#c23 - this._docManager.queueExistenceCheck(i); - this._checkDocExistence = false; - } - - for (; i < children.length; i++) - this.actor.set_skip_paint(children[i], true); - }, - - _onDocsChanged: function() { - this._checkDocExistence = true; - Main.queueDeferredWork(this._workId); - }, - - _redisplay: function() { - // Should be kept alive by the _actorsByUri - this.actor.remove_all(); - let docs = this._docManager.getTimestampOrderedInfos(); - for (let i = 0; i < docs.length && i < MAX_DASH_DOCS; i++) { - let doc = docs[i]; - let display = this._actorsByUri[doc.uri]; - if (display) { - this.actor.add_actor(display.actor); - } else { - let display = new DashDocDisplayItem(doc); - this.actor.add_actor(display.actor); - this._actorsByUri[doc.uri] = display; - } - } - // Any unparented actors must have been deleted - for (let uri in this._actorsByUri) { - let display = this._actorsByUri[uri]; - if (display.actor.get_parent() == null) { - display.actor.destroy(); - delete this._actorsByUri[uri]; - } - } - this.emit('changed'); - } -}; - -Signals.addSignalMethods(DashDocDisplay.prototype); function DocSearchProvider() { this._init();