From f8ad8f9ccd86e589f3880fe6bea69ece7d294dff Mon Sep 17 00:00:00 2001 From: roytam1 Date: Fri, 22 Jan 2021 15:03:08 +0800 Subject: [PATCH] browser-omni: update download UI (omni.ja dated 2018-12-15 11:38) --- .../downloads/allDownloadsViewOverlay.css | 17 +- .../downloads/allDownloadsViewOverlay.js | 1493 +++++++++-------- .../downloads/allDownloadsViewOverlay.xul | 8 - .../downloads/contentAreaDownloadsView.js | 4 +- .../downloads/contentAreaDownloadsView.xul | 3 +- .../content/browser/downloads/download.css | 7 - .../content/browser/downloads/download.xml | 96 +- .../content/browser/downloads/downloads.css | 62 +- .../content/browser/downloads/downloads.js | 822 ++++++--- .../browser/downloads/downloadsOverlay.xul | 11 +- .../content/browser/downloads/indicator.js | 358 ++-- .../browser/downloads/indicatorOverlay.xul | 60 +- 12 files changed, 1754 insertions(+), 1187 deletions(-) diff --git a/browser-omni/chrome/kmeleon/content/browser/downloads/allDownloadsViewOverlay.css b/browser-omni/chrome/kmeleon/content/browser/downloads/allDownloadsViewOverlay.css index 54e7be78..c062ae46 100644 --- a/browser-omni/chrome/kmeleon/content/browser/downloads/allDownloadsViewOverlay.css +++ b/browser-omni/chrome/kmeleon/content/browser/downloads/allDownloadsViewOverlay.css @@ -21,6 +21,15 @@ richlistitem.download[active] { -moz-binding: url('chrome://browser/content/downloads/download.xml#download-full-ui'); } +richlistitem.download[active]:-moz-any([state="-1"],/* Starting (initial) */ + [state="0"], /* Downloading */ + [state="4"], /* Paused */ + [state="5"], /* Starting (queued) */ + [state="7"]) /* Scanning */ +{ + -moz-binding: url('chrome://browser/content/downloads/download.xml#download-in-progress-full-ui'); +} + .download-state:not( [state="0"] /* Downloading */) .downloadPauseMenuItem, .download-state:not( [state="4"] /* Paused */) @@ -28,12 +37,6 @@ richlistitem.download[active] { .download-state:not(:-moz-any([state="2"], /* Failed */ [state="4"]) /* Paused */) .downloadCancelMenuItem, -/* Blocked (dirty) downloads that have not been confirmed and - have temporary data. */ -.download-state:not( [state="8"] /* Blocked (dirty) */) - .downloadUnblockMenuItem, -.download-state[state="8"]:not(.temporary-block) - .downloadUnblockMenuItem, .download-state[state]:not(:-moz-any([state="1"], /* Finished */ [state="2"], /* Failed */ [state="3"], /* Canceled */ @@ -47,7 +50,7 @@ richlistitem.download[active] { [state="4"], /* Paused */ [state="5"]) /* Starting (queued) */) .downloadShowMenuItem, -.download-state[state="7"] .downloadCommandsSeparator +.download-state[state="7"] /* Scanning */ .downloadCommandsSeparator { display: none; } diff --git a/browser-omni/chrome/kmeleon/content/browser/downloads/allDownloadsViewOverlay.js b/browser-omni/chrome/kmeleon/content/browser/downloads/allDownloadsViewOverlay.js index 3884d026..e1d0e75d 100644 --- a/browser-omni/chrome/kmeleon/content/browser/downloads/allDownloadsViewOverlay.js +++ b/browser-omni/chrome/kmeleon/content/browser/downloads/allDownloadsViewOverlay.js @@ -2,32 +2,30 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -let { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; +/** + * THE PLACES VIEW IMPLEMENTED IN THIS FILE HAS A VERY PARTICULAR USE CASE. + * IT IS HIGHLY RECOMMENDED NOT TO EXTEND IT FOR ANY OTHER USE CASES OR RELY + * ON IT AS AN API. + */ -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +let Cu = Components.utils; +let Ci = Components.interfaces; +let Cc = Components.classes; -XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", - "resource://gre/modules/DownloadUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", - "resource:///modules/DownloadsCommon.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "DownloadsViewUI", - "resource:///modules/DownloadsViewUI.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", - "resource://gre/modules/FileUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", - "resource://gre/modules/NetUtil.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "OS", - "resource://gre/modules/osfile.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", - "resource://gre/modules/PlacesUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Promise", - "resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/DownloadUtils.jsm"); +Cu.import("resource:///modules/DownloadsCommon.jsm"); +Cu.import("resource://gre/modules/PlacesUtils.jsm"); +Cu.import("resource://gre/modules/osfile.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", "resource:///modules/RecentWindow.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Task", - "resource://gre/modules/Task.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); const nsIDM = Ci.nsIDownloadManager; @@ -36,375 +34,677 @@ const DOWNLOAD_META_DATA_ANNO = "downloads/metaData"; const DOWNLOAD_VIEW_SUPPORTED_COMMANDS = ["cmd_delete", "cmd_copy", "cmd_paste", "cmd_selectAll", - "downloadsCmd_pauseResume", "downloadsCmd_cancel", "downloadsCmd_unblock", - "downloadsCmd_confirmBlock", "downloadsCmd_open", "downloadsCmd_show", - "downloadsCmd_retry", "downloadsCmd_openReferrer", "downloadsCmd_clearDownloads"]; + "downloadsCmd_pauseResume", "downloadsCmd_cancel", + "downloadsCmd_open", "downloadsCmd_show", "downloadsCmd_retry", + "downloadsCmd_openReferrer", "downloadsCmd_clearDownloads"]; + +const NOT_AVAILABLE = Number.MAX_VALUE; /** - * Represents a download from the browser history. It implements part of the - * interface of the Download object. + * Download a URL. + * + * @param aURL + * the url to download (nsIURI object) + * @param [optional] aFileName + * the destination file name + */ +function DownloadURL(aURL, aFileName) { + // For private browsing, try to get document out of the most recent browser + // window, or provide our own if there's no browser window. + let browserWin = RecentWindow.getMostRecentBrowserWindow(); + let initiatingDoc = browserWin ? browserWin.document : document; + saveURL(aURL, aFileName, null, true, true, undefined, initiatingDoc); +} + +/** + * A download element shell is responsible for handling the commands and the + * displayed data for a single download view element. The download element + * could represent either a past download (for which we get data from places) or + * a "session" download (using a data-item object. See DownloadsCommon.jsm), or both. + * + * Once initialized with either a data item or a places node, the created richlistitem + * can be accessed through the |element| getter, and can then be inserted/removed from + * a richlistbox. + * + * The shell doesn't take care of inserting the item, or removing it when it's no longer + * valid. That's the caller (a DownloadsPlacesView object) responsibility. * - * @param aPlacesNode - * The Places node from which the history download should be initialized. + * The caller is also responsible for "passing over" notification from both the + * download-view and the places-result-observer, in the following manner: + * - The DownloadsPlacesView object implements getViewItem of the download-view + * pseudo interface. It returns this object (therefore we implement + * onStateChangea and onProgressChange here). + * - The DownloadsPlacesView object adds itself as a places result observer and + * calls this object's placesNodeIconChanged, placesNodeTitleChanged and + * placeNodeAnnotationChanged from its callbacks. + * + * @param [optional] aDataItem + * The data item of a the session download. Required if aPlacesNode is not set + * @param [optional] aPlacesNode + * The places node for a past download. Required if aDataItem is not set. + * @param [optional] aAnnotations + * Map containing annotations values, to speed up the initial loading. */ -function HistoryDownload(aPlacesNode) { - // TODO (bug 829201): history downloads should get the referrer from Places. - this.source = { - url: aPlacesNode.uri, - }; - this.target = { - path: undefined, - exists: false, - size: undefined, - }; - - // In case this download cannot obtain its end time from the Places metadata, - // use the time from the Places node, that is the start time of the download. - this.endTime = aPlacesNode.time / 1000; +function DownloadElementShell(aDataItem, aPlacesNode, aAnnotations) { + this._element = document.createElement("richlistitem"); + this._element._shell = this; + + this._element.classList.add("download"); + this._element.classList.add("download-state"); + + if (aAnnotations) + this._annotations = aAnnotations; + if (aDataItem) + this.dataItem = aDataItem; + if (aPlacesNode) + this.placesNode = aPlacesNode; } -HistoryDownload.prototype = { +DownloadElementShell.prototype = { + // The richlistitem for the download + get element() this._element, + /** - * Pushes information from Places metadata into this object. + * Manages the "active" state of the shell. By default all the shells + * without a dataItem are inactive, thus their UI is not updated. They must + * be activated when entering the visible area. Session downloads are + * always active since they always have a dataItem. */ - updateFromMetaData(metaData) { - try { - this.target.path = Cc["@mozilla.org/network/protocol;1?name=file"] - .getService(Ci.nsIFileProtocolHandler) - .getFileFromURLSpec(metaData.targetFileSpec).path; - } catch (ex) { - this.target.path = undefined; + ensureActive: function DES_ensureActive() { + if (!this._active) { + this._active = true; + this._element.setAttribute("active", true); + this._updateUI(); } + }, + get active() !!this._active, + + // The data item for the download + _dataItem: null, + get dataItem() this._dataItem, - if ("state" in metaData) { - this.succeeded = metaData.state == nsIDM.DOWNLOAD_FINISHED; - this.error = metaData.state == nsIDM.DOWNLOAD_FAILED - ? { message: "History download failed." } - : metaData.state == nsIDM.DOWNLOAD_BLOCKED_PARENTAL - ? { becauseBlockedByParentalControls: true } - : metaData.state == nsIDM.DOWNLOAD_DIRTY - ? { becauseBlockedByReputationCheck: true } - : null; - this.canceled = metaData.state == nsIDM.DOWNLOAD_CANCELED || - metaData.state == nsIDM.DOWNLOAD_PAUSED; - this.endTime = metaData.endTime; - - // Normal history downloads are assumed to exist until the user interface - // is refreshed, at which point these values may be updated. - this.target.exists = true; - this.target.size = metaData.fileSize; - } else { - // Metadata might be missing from a download that has started but hasn't - // stopped already. Normally, this state is overridden with the one from - // the corresponding in-progress session download. But if the browser is - // terminated abruptly and additionally the file with information about - // in-progress downloads is lost, we may end up using this state. We use - // the failed state to allow the download to be restarted. - // - // On the other hand, if the download is missing the target file - // annotation as well, it is just a very old one, and we can assume it - // succeeded. - this.succeeded = !this.target.path; - this.error = this.target.path ? { message: "Unstarted download." } : null; - this.canceled = false; - - // These properties may be updated if the user interface is refreshed. - this.exists = false; - this.target.size = undefined; + set dataItem(aValue) { + if (this._dataItem != aValue) { + if (!aValue && !this._placesNode) + throw new Error("Should always have either a dataItem or a placesNode"); + + this._dataItem = aValue; + if (!this.active) + this.ensureActive(); + else + this._updateUI(); } + return aValue; }, - /** - * History downloads are never in progress. - */ - stopped: true, - - /** - * No percentage indication is shown for history downloads. - */ - hasProgress: false, + _placesNode: null, + get placesNode() this._placesNode, + set placesNode(aValue) { + if (this._placesNode != aValue) { + if (!aValue && !this._dataItem) + throw new Error("Should always have either a dataItem or a placesNode"); + + // Preserve the annotations map if this is the first loading and we got + // cached values. + if (this._placesNode || !this._annotations) { + this._annotations = new Map(); + } - /** - * History downloads cannot be restarted using their partial data, even if - * they are indicated as paused in their Places metadata. The only way is to - * use the information from a persisted session download, that will be shown - * instead of the history download. In case this session download is not - * available, we show the history download as canceled, not paused. - */ - hasPartialData: false, + this._placesNode = aValue; - /** - * This method mimicks the "start" method of session downloads, and is called - * when the user retries a history download. - * - * At present, we always ask the user for a new target path when retrying a - * history download. In the future we may consider reusing the known target - * path if the folder still exists and the file name is not already used, - * except when the user preferences indicate that the target path should be - * requested every time a new download is started. - */ - start() { - let browserWin = RecentWindow.getMostRecentBrowserWindow(); - let initiatingDoc = browserWin ? browserWin.document : document; + // We don't need to update the UI if we had a data item, because + // the places information isn't used in this case. + if (!this._dataItem && this.active) + this._updateUI(); + } + return aValue; + }, - // Do not suggest a file name if we don't know the original target. - let leafName = this.target.path ? OS.Path.basename(this.target.path) : null; - DownloadURL(this.source.url, leafName, initiatingDoc); + // The download uri (as a string) + get downloadURI() { + if (this._dataItem) + return this._dataItem.uri; + if (this._placesNode) + return this._placesNode.uri; + throw new Error("Unexpected download element state"); + }, - return Promise.resolve(); + get _downloadURIObj() { + if (!("__downloadURIObj" in this)) + this.__downloadURIObj = NetUtil.newURI(this.downloadURI); + return this.__downloadURIObj; }, - /** - * This method mimicks the "refresh" method of session downloads, except that - * it cannot notify that the data changed to the Downloads View. - */ - refresh: Task.async(function* () { - try { - this.target.size = (yield OS.File.stat(this.target.path)).size; - this.target.exists = true; - } catch (ex) { - // We keep the known file size from the metadata, if any. - this.target.exists = false; + _getIcon: function DES__getIcon() { + let metaData = this.getDownloadMetaData(); + if ("filePath" in metaData) + return "moz-icon://" + metaData.filePath + "?size=32"; + + if (this._placesNode) { + // Try to extract an extension from the uri. + let ext = this._downloadURIObj.QueryInterface(Ci.nsIURL).fileExtension; + if (ext) + return "moz-icon://." + ext + "?size=32"; + return this._placesNode.icon || "moz-icon://.unknown?size=32"; } - }), -}; + if (this._dataItem) + throw new Error("Session-download items should always have a target file uri"); -/** - * A download element shell is responsible for handling the commands and the - * displayed data for a single download view element. - * - * The shell may contain a session download, a history download, or both. When - * both a history and a session download are present, the session download gets - * priority and its information is displayed. - * - * On construction, a new richlistitem is created, and can be accessed through - * the |element| getter. The shell doesn't insert the item in a richlistbox, the - * caller must do it and remove the element when it's no longer needed. - * - * The caller is also responsible for forwarding status notifications for - * session downloads, calling the onStateChanged and onChanged methods. - * - * @param [optional] aSessionDownload - * The session download, required if aHistoryDownload is not set. - * @param [optional] aHistoryDownload - * The history download, required if aSessionDownload is not set. - */ -function HistoryDownloadElementShell(aSessionDownload, aHistoryDownload) { - this.element = document.createElement("richlistitem"); - this.element._shell = this; + throw new Error("Unexpected download element state"); + }, - this.element.classList.add("download"); - this.element.classList.add("download-state"); + // Helper for getting a places annotation set for the download. + _getAnnotation: function DES__getAnnotation(aAnnotation, aDefaultValue) { + let value; + if (this._annotations.has(aAnnotation)) + value = this._annotations.get(aAnnotation); - if (aSessionDownload) { - this.sessionDownload = aSessionDownload; - } - if (aHistoryDownload) { - this.historyDownload = aHistoryDownload; - } -} + // If the value is cached, or we know it doesn't exist, avoid a database + // lookup. + if (value === undefined) { + try { + value = PlacesUtils.annotations.getPageAnnotation( + this._downloadURIObj, aAnnotation); + } + catch(ex) { + value = NOT_AVAILABLE; + } + } -HistoryDownloadElementShell.prototype = { - __proto__: DownloadsViewUI.DownloadElementShell.prototype, + if (value === NOT_AVAILABLE) { + if (aDefaultValue === undefined) { + throw new Error("Could not get required annotation '" + aAnnotation + + "' for download with url '" + this.downloadURI + "'"); + } + value = aDefaultValue; + } - /** - * Manages the "active" state of the shell. By default all the shells without - * a session download are inactive, thus their UI is not updated. They must - * be activated when entering the visible area. Session downloads are always - * active. - */ - ensureActive() { - if (!this._active) { - this._active = true; - this.element.setAttribute("active", true); - this._updateUI(); + this._annotations.set(aAnnotation, value); + return value; + }, + + _fetchTargetFileInfo: function DES__fetchTargetFileInfo(aUpdateMetaDataAndStatusUI = false) { + if (this._targetFileInfoFetched) + throw new Error("_fetchTargetFileInfo should not be called if the information was already fetched"); + if (!this.active) + throw new Error("Trying to _fetchTargetFileInfo on an inactive download shell"); + + let path = this.getDownloadMetaData().filePath; + + // In previous version, the target file annotations were not set, + // so we cannot tell where is the file. + if (path === undefined) { + this._targetFileInfoFetched = true; + this._targetFileExists = false; + if (aUpdateMetaDataAndStatusUI) { + this._metaData = null; + this._updateDownloadStatusUI(); + } + // Here we don't need to update the download commands, + // as the state is unknown as it was. + return; } + + OS.File.stat(path).then( + function onSuccess(fileInfo) { + this._targetFileInfoFetched = true; + this._targetFileExists = true; + this._targetFileSize = fileInfo.size; + if (aUpdateMetaDataAndStatusUI) { + this._metaData = null; + this._updateDownloadStatusUI(); + } + if (this._element.selected) + goUpdateDownloadCommands(); + }.bind(this), + + function onFailure(reason) { + if (reason instanceof OS.File.Error && reason.becauseNoSuchFile) { + this._targetFileInfoFetched = true; + this._targetFileExists = false; + } + else { + Cu.reportError("Could not fetch info for target file (reason: " + + reason + ")"); + } + + if (aUpdateMetaDataAndStatusUI) { + this._metaData = null; + this._updateDownloadStatusUI(); + } + + if (this._element.selected) + goUpdateDownloadCommands(); + }.bind(this) + ); + }, + + _getAnnotatedMetaData: function DES__getAnnotatedMetaData() + JSON.parse(this._getAnnotation(DOWNLOAD_META_DATA_ANNO)), + + _extractFilePathAndNameFromFileURI: + function DES__extractFilePathAndNameFromFileURI(aFileURI) { + let file = Cc["@mozilla.org/network/protocol;1?name=file"] + .getService(Ci.nsIFileProtocolHandler) + .getFileFromURLSpec(aFileURI); + return [file.path, file.leafName]; }, - get active() !!this._active, /** - * Overrides the base getter to return the Download or HistoryDownload object - * for displaying information and executing commands in the user interface. + * Retrieve the meta data object for the download. The following fields + * may be set. + * + * - state - any download state defined in nsIDownloadManager. If this field + * is not set, the download state is unknown. + * - endTime: the end time of the download. + * - filePath: the downloaded file path on the file system, when it + * was downloaded. The file may not exist. This is set for session + * downloads that have a local file set, and for history downloads done + * after the landing of bug 591289. + * - fileName: the downloaded file name on the file system. Set if filePath + * is set. + * - displayName: the user-facing label for the download. This is always + * set. If available, it's set to the downloaded file name. If not, + * the places title for the download uri is used. As a last resort, + * we fallback to the download uri. + * - fileSize (only set for downloads which completed successfully): + * the downloaded file size. For downloads done after the landing of + * bug 826991, this value is "static" - that is, it does not necessarily + * mean that the file is in place and has this size. */ - get download() this._sessionDownload || this._historyDownload, - - _sessionDownload: null, - get sessionDownload() this._sessionDownload, - set sessionDownload(aValue) { - if (this._sessionDownload != aValue) { - if (!aValue && !this._historyDownload) { - throw new Error("Should always have either a Download or a HistoryDownload"); + getDownloadMetaData: function DES_getDownloadMetaData() { + if (!this._metaData) { + if (this._dataItem) { + let s = DownloadsCommon.strings; + let referrer = this._dataItem.referrer || this._dataItem.uri; + let [displayHost, fullHost] = DownloadUtils.getURIHost(referrer); + this._metaData = { + state: this._dataItem.state, + endTime: this._dataItem.endTime, + fileName: this._dataItem.target, + displayName: this._dataItem.target, + extendedDisplayName: s.statusSeparator(this._dataItem.target, displayHost), + extendedDisplayNameTip: s.statusSeparator(this._dataItem.target, fullHost) + }; + if (this._dataItem.done) + this._metaData.fileSize = this._dataItem.maxBytes; + if (this._dataItem.localFile) + this._metaData.filePath = this._dataItem.localFile.path; } + else { + try { + this._metaData = this._getAnnotatedMetaData(); + } + catch(ex) { + this._metaData = { }; + if (this._targetFileInfoFetched && this._targetFileExists) { + this._metaData.state = this._targetFileSize > 0 ? + nsIDM.DOWNLOAD_FINISHED : nsIDM.DOWNLOAD_FAILED; + this._metaData.fileSize = this._targetFileSize; + } - this._sessionDownload = aValue; + // This is actually the start-time, but it's the best we can get. + this._metaData.endTime = this._placesNode.time / 1000; + } - this.ensureActive(); - this._updateUI(); + try { + let targetFileURI = this._getAnnotation(DESTINATION_FILE_URI_ANNO); + [this._metaData.filePath, this._metaData.fileName] = + this._extractFilePathAndNameFromFileURI(targetFileURI); + this._metaData.displayName = this._metaData.fileName; + } + catch(ex) { + this._metaData.displayName = this._placesNode.title || this.downloadURI; + } + } } - return aValue; + return this._metaData; }, - _historyDownload: null, - get historyDownload() this._historyDownload, - set historyDownload(aValue) { - if (this._historyDownload != aValue) { - if (!aValue && !this._sessionDownload) { - throw new Error("Should always have either a Download or a HistoryDownload"); + // The status text for the download + _getStatusText: function DES__getStatusText() { + let s = DownloadsCommon.strings; + if (this._dataItem && this._dataItem.inProgress) { + if (this._dataItem.paused) { + let transfer = + DownloadUtils.getTransferTotal(this._dataItem.currBytes, + this._dataItem.maxBytes); + + // We use the same XUL label to display both the state and the amount + // transferred, for example "Paused - 1.1 MB". + return s.statusSeparatorBeforeNumber(s.statePaused, transfer); + } + if (this._dataItem.state == nsIDM.DOWNLOAD_DOWNLOADING) { + let [status, newEstimatedSecondsLeft] = + DownloadUtils.getDownloadStatus(this.dataItem.currBytes, + this.dataItem.maxBytes, + this.dataItem.speed, + this._lastEstimatedSecondsLeft || Infinity); + this._lastEstimatedSecondsLeft = newEstimatedSecondsLeft; + return status; + } + if (this._dataItem.starting) { + return s.stateStarting; + } + if (this._dataItem.state == nsIDM.DOWNLOAD_SCANNING) { + return s.stateScanning; } - this._historyDownload = aValue; + throw new Error("_getStatusText called with a bogus download state"); + } - // We don't need to update the UI if we had a session data item, because - // the places information isn't used in this case. - if (!this._sessionDownload) { - this._updateUI(); + // This is a not-in-progress or history download. + let stateLabel = ""; + let state = this.getDownloadMetaData().state; + switch (state) { + case nsIDM.DOWNLOAD_FAILED: + stateLabel = s.stateFailed; + break; + case nsIDM.DOWNLOAD_CANCELED: + stateLabel = s.stateCanceled; + break; + case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: + stateLabel = s.stateBlockedParentalControls; + break; + case nsIDM.DOWNLOAD_BLOCKED_POLICY: + stateLabel = s.stateBlockedPolicy; + break; + case nsIDM.DOWNLOAD_DIRTY: + stateLabel = s.stateDirty; + break; + case nsIDM.DOWNLOAD_FINISHED:{ + // For completed downloads, show the file size (e.g. "1.5 MB") + let metaData = this.getDownloadMetaData(); + if ("fileSize" in metaData) { + let [size, unit] = DownloadUtils.convertByteUnits(metaData.fileSize); + stateLabel = s.sizeWithUnits(size, unit); + break; + } + // Fallback to default unknown state. } + default: + stateLabel = s.sizeUnknown; + break; } - return aValue; + + // TODO (bug 829201): history downloads should get the referrer from Places. + let referrer = this._dataItem && this._dataItem.referrer || + this.downloadURI; + let [displayHost, fullHost] = DownloadUtils.getURIHost(referrer); + + let date = new Date(this.getDownloadMetaData().endTime); + let [displayDate, fullDate] = DownloadUtils.getReadableDates(date); + + // We use the same XUL label to display the state, the host name, and the + // end time. + let firstPart = s.statusSeparator(stateLabel, displayHost); + return s.statusSeparator(firstPart, displayDate); }, - _updateUI() { - // There is nothing to do if the item has always been invisible. - if (!this.active) { + // The progressmeter element for the download + get _progressElement() { + if (!("__progressElement" in this)) { + this.__progressElement = + document.getAnonymousElementByAttribute(this._element, "anonid", + "progressmeter"); + } + return this.__progressElement; + }, + + // Updates the download state attribute (and by that hide/unhide the + // appropriate buttons and context menu items), the status text label, + // and the progress meter. + _updateDownloadStatusUI: function DES__updateDownloadStatusUI() { + if (!this.active) + throw new Error("_updateDownloadStatusUI called for an inactive item."); + + let state = this.getDownloadMetaData().state; + if (state !== undefined) + this._element.setAttribute("state", state); + + this._element.setAttribute("status", this._getStatusText()); + + // For past-downloads, we're done. For session-downloads, we may also need + // to update the progress-meter. + if (!this._dataItem) return; + + // Copied from updateProgress in downloads.js. + if (this._dataItem.starting) { + // Before the download starts, the progress meter has its initial value. + this._element.setAttribute("progressmode", "normal"); + this._element.setAttribute("progress", "0"); + } + else if (this._dataItem.state == nsIDM.DOWNLOAD_SCANNING || + this._dataItem.percentComplete == -1) { + // We might not know the progress of a running download, and we don't know + // the remaining time during the malware scanning phase. + this._element.setAttribute("progressmode", "undetermined"); + } + else { + // This is a running download of which we know the progress. + this._element.setAttribute("progressmode", "normal"); + this._element.setAttribute("progress", this._dataItem.percentComplete); } - // Since the state changed, we may need to check the target file again. - this._targetFileChecked = false; + // Dispatch the ValueChange event for accessibility, if possible. + if (this._progressElement) { + let event = document.createEvent("Events"); + event.initEvent("ValueChange", true, true); + this._progressElement.dispatchEvent(event); + } + }, + + _updateDisplayNameAndIcon: function DES__updateDisplayNameAndIcon() { + let metaData = this.getDownloadMetaData(); + this._element.setAttribute("displayName", metaData.displayName); + if ("extendedDisplayName" in metaData) + this._element.setAttribute("extendedDisplayName", metaData.extendedDisplayName); + if ("extendedDisplayNameTip" in metaData) + this._element.setAttribute("extendedDisplayNameTip", metaData.extendedDisplayNameTip); + this._element.setAttribute("image", this._getIcon()); + }, - this._updateState(); + _updateUI: function DES__updateUI() { + if (!this.active) + throw new Error("Trying to _updateUI on an inactive download shell"); + + this._metaData = null; + this._targetFileInfoFetched = false; + + this._updateDisplayNameAndIcon(); + + // For history downloads done in past releases, the downloads/metaData + // annotation is not set, and therefore we cannot tell the download + // state without the target file information. + if (this._dataItem || this.getDownloadMetaData().state !== undefined) + this._updateDownloadStatusUI(); + else + this._fetchTargetFileInfo(true); }, - get statusTextAndTip() { - let status = this.rawStatusTextAndTip; + placesNodeIconChanged: function DES_placesNodeIconChanged() { + if (!this._dataItem) + this._element.setAttribute("image", this._getIcon()); + }, - // The base object would show extended progress information in the tooltip, - // but we move this to the main view and never display a tooltip. - if (!this.download.stopped) { - status.text = status.tip; + placesNodeTitleChanged: function DES_placesNodeTitleChanged() { + // If there's a file path, we use the leaf name for the title. + if (!this._dataItem && this.active && !this.getDownloadMetaData().filePath) { + this._metaData = null; + this._updateDisplayNameAndIcon(); } - status.tip = ""; + }, + + placesNodeAnnotationChanged: function DES_placesNodeAnnotationChanged(aAnnoName) { + this._annotations.delete(aAnnoName); + if (!this._dataItem && this.active) { + if (aAnnoName == DOWNLOAD_META_DATA_ANNO) { + let metaData = this.getDownloadMetaData(); + let annotatedMetaData = this._getAnnotatedMetaData(); + metaData.endTime = annotatedMetaData.endTime; + if ("fileSize" in annotatedMetaData) + metaData.fileSize = annotatedMetaData.fileSize; + else + delete metaData.fileSize; + + if (metaData.state != annotatedMetaData.state) { + metaData.state = annotatedMetaData.state; + if (this._element.selected) + goUpdateDownloadCommands(); + } - return status; + this._updateDownloadStatusUI(); + } + else if (aAnnoName == DESTINATION_FILE_URI_ANNO) { + let metaData = this.getDownloadMetaData(); + let targetFileURI = this._getAnnotation(DESTINATION_FILE_URI_ANNO); + [metaData.filePath, metaData.fileName] = + this._extractFilePathAndNameFromFileURI(targetFileURI); + metaData.displayName = metaData.fileName; + this._updateDisplayNameAndIcon(); + + if (this._targetFileInfoFetched) { + // This will also update the download commands if necessary. + this._targetFileInfoFetched = false; + this._fetchTargetFileInfo(); + } + } + } }, - onStateChanged() { - this.element.setAttribute("image", this.image); - this.element.setAttribute("state", - DownloadsCommon.stateOfDownload(this.download)); + /* DownloadView */ + onStateChange: function DES_onStateChange(aOldState) { + let metaData = this.getDownloadMetaData(); + metaData.state = this.dataItem.state; + if (aOldState != nsIDM.DOWNLOAD_FINISHED && aOldState != metaData.state) { + // See comment in DVI_onStateChange in downloads.js (the panel-view) + this._element.setAttribute("image", this._getIcon() + "&state=normal"); + metaData.fileSize = this._dataItem.maxBytes; + if (this._targetFileInfoFetched) { + this._targetFileInfoFetched = false; + this._fetchTargetFileInfo(); + } + } - if (this.element.selected) { + this._updateDownloadStatusUI(); + if (this._element.selected) goUpdateDownloadCommands(); - } else { + else goUpdateCommand("downloadsCmd_clearDownloads"); - } }, - onChanged() { - // This cannot be placed within onStateChanged because - // when a download goes from hasBlockedData to !hasBlockedData - // it will still remain in the same state. - this.element.classList.toggle("temporary-block", - !!this.download.hasBlockedData); - this._updateProgress(); + /* DownloadView */ + onProgressChange: function DES_onProgressChange() { + this._updateDownloadStatusUI(); }, /* nsIController */ - isCommandEnabled(aCommand) { + isCommandEnabled: function DES_isCommandEnabled(aCommand) { // The only valid command for inactive elements is cmd_delete. - if (!this.active && aCommand != "cmd_delete") { + if (!this.active && aCommand != "cmd_delete") return false; - } switch (aCommand) { - case "downloadsCmd_open": - // This property is false if the download did not succeed. - return this.download.target.exists; - case "downloadsCmd_show": + case "downloadsCmd_open": { + // We cannot open a session download file unless it's done ("openable"). + // If it's finished, we need to make sure the file was not removed, + // as we do for past downloads. + if (this._dataItem && !this._dataItem.openable) + return false; + + if (this._targetFileInfoFetched) + return this._targetFileExists; + + // If the target file information is not yet fetched, + // temporarily assume that the file is in place. + return this.getDownloadMetaData().state == nsIDM.DOWNLOAD_FINISHED; + } + case "downloadsCmd_show": { // TODO: Bug 827010 - Handle part-file asynchronously. - if (this._sessionDownload && this.download.target.partFilePath) { - let partFile = new FileUtils.File(this.download.target.partFilePath); - if (partFile.exists()) { - return true; - } - } + if (this._dataItem && + this._dataItem.partFile && this._dataItem.partFile.exists()) + return true; + + if (this._targetFileInfoFetched) + return this._targetFileExists; - // This property is false if the download did not succeed. - return this.download.target.exists; + // If the target file information is not yet fetched, + // temporarily assume that the file is in place. + return this.getDownloadMetaData().state == nsIDM.DOWNLOAD_FINISHED; + } case "downloadsCmd_pauseResume": - return this.download.hasPartialData && !this.download.error; + return this._dataItem && this._dataItem.inProgress && this._dataItem.resumable; case "downloadsCmd_retry": - return this.download.canceled || this.download.error; + // An history download can always be retried. + return !this._dataItem || this._dataItem.canRetry; case "downloadsCmd_openReferrer": - return !!this.download.source.referrer; + return this._dataItem && !!this._dataItem.referrer; case "cmd_delete": - // We don't want in-progress downloads to be removed accidentally. - return this.download.stopped; + // The behavior in this case is somewhat unexpected, so we disallow that. + if (this._placesNode && this._dataItem && this._dataItem.inProgress) + return false; + return true; case "downloadsCmd_cancel": - return !!this._sessionDownload; - case "downloadsCmd_confirmBlock": - case "downloadsCmd_unblock": - return this.download.hasBlockedData; + return this._dataItem != null; } return false; }, + _retryAsHistoryDownload: function DES__retryAsHistoryDownload() { + // In future we may try to download into the same original target uri, when + // we have it. Though that requires verifying the path is still valid and + // may surprise the user if he wants to be requested every time. + DownloadURL(this.downloadURI, this.getDownloadMetaData().fileName); + }, + /* nsIController */ - doCommand(aCommand) { + doCommand: function DES_doCommand(aCommand) { switch (aCommand) { case "downloadsCmd_open": { - let file = new FileUtils.File(this.download.target.path); + let file = this._dataItem ? + this.dataItem.localFile : + new FileUtils.File(this.getDownloadMetaData().filePath); + DownloadsCommon.openDownloadedFile(file, null, window); break; } case "downloadsCmd_show": { - let file = new FileUtils.File(this.download.target.path); - DownloadsCommon.showDownloadedFile(file); + if (this._dataItem) { + this._dataItem.showLocalFile(); + } + else { + let file = new FileUtils.File(this.getDownloadMetaData().filePath); + DownloadsCommon.showDownloadedFile(file); + } break; } case "downloadsCmd_openReferrer": { - openURL(this.download.source.referrer); + openURL(this._dataItem.referrer); break; } case "downloadsCmd_cancel": { - this.download.cancel().catch(() => {}); - this.download.removePartialData().catch(Cu.reportError); + this._dataItem.cancel(); break; } case "cmd_delete": { - if (this._sessionDownload) { - DownloadsCommon.removeAndFinalizeDownload(this.download); - } - if (this._historyDownload) { - let uri = NetUtil.newURI(this.download.source.url); - PlacesUtils.bhistory.removePage(uri); - } + if (this._dataItem) + Downloads.getList(Downloads.ALL) + .then(list => list.remove(this._dataItem._download)) + .then(() => this._dataItem._download.finalize(true)) + .catch(Cu.reportError); + if (this._placesNode) + PlacesUtils.bhistory.removePage(this._downloadURIObj); break; - } + } case "downloadsCmd_retry": { - // Errors when retrying are already reported as download failures. - this.download.start().catch(() => {}); + if (this._dataItem) + this._dataItem.retry(); + else + this._retryAsHistoryDownload(); break; } case "downloadsCmd_pauseResume": { - // This command is only enabled for session downloads. - if (this.download.stopped) { - this.download.start(); - } else { - this.download.cancel(); - } - break; - } - case "downloadsCmd_unblock": { - DownloadsCommon.confirmUnblockDownload(DownloadsCommon.BLOCK_VERDICT_MALWARE, - window).then((confirmed) => { - if (confirmed) { - return this.download.unblock(); - } - }).catch(Cu.reportError); - break; - } - case "downloadsCmd_confirmBlock": { - this.download.confirmBlock().catch(Cu.reportError); + this._dataItem.togglePauseResume(); break; } } @@ -413,18 +713,17 @@ HistoryDownloadElementShell.prototype = { // Returns whether or not the download handled by this shell should // show up in the search results for the given term. Both the display // name for the download and the url are searched. - matchesSearchTerm(aTerm) { - if (!aTerm) { + matchesSearchTerm: function DES_matchesSearchTerm(aTerm) { + if (!aTerm) return true; - } aTerm = aTerm.toLowerCase(); - return this.displayName.toLowerCase().contains(aTerm) || - this.download.source.url.toLowerCase().contains(aTerm); + return this.getDownloadMetaData().displayName.toLowerCase().includes(aTerm) || + this.downloadURI.toLowerCase().includes(aTerm); }, // Handles return keypress on the element (the keypress listener is // set in the DownloadsPlacesView object). - doDefaultCommand() { + doDefaultCommand: function DES_doDefaultCommand() { function getDefaultCommandForState(aState) { switch (aState) { case nsIDM.DOWNLOAD_FINISHED: @@ -446,59 +745,30 @@ HistoryDownloadElementShell.prototype = { } return ""; } - let state = DownloadsCommon.stateOfDownload(this.download); - let command = getDefaultCommandForState(state); - if (command && this.isCommandEnabled(command)) { + let command = getDefaultCommandForState(this.getDownloadMetaData().state); + if (command && this.isCommandEnabled(command)) this.doCommand(command); - } }, /** - * This method is called by the outer download view, after the controller - * commands have already been updated. In case we did not check for the - * existence of the target file already, we can do it now and then update - * the commands as needed. + * At the first time an item is selected, we don't yet have + * the target file information. Thus the call to goUpdateDownloadCommands + * in DPV_onSelect would result in best-guess enabled/disabled result. + * That way we let the user perform command immediately. However, once + * we have the target file information, we can update the commands + * appropriately (_fetchTargetFileInfo() calls goUpdateDownloadCommands). */ - onSelect() { - if (!this.active) { + onSelect: function DES_onSelect() { + if (!this.active) return; - } - - // If this is a history download for which no target file information is - // available, we cannot retrieve information about the target file. - if (!this.download.target.path) { - return; - } - - // Start checking for existence. This may be done twice if onSelect is - // called again before the information is collected. - if (!this._targetFileChecked) { - this._checkTargetFileOnSelect().catch(Cu.reportError); - } - }, - - _checkTargetFileOnSelect: Task.async(function* () { - try { - yield this.download.refresh(); - } finally { - // Do not try to check for existence again if this failed once. - this._targetFileChecked = true; - } - - // Update the commands only if the element is still selected. - if (this.element.selected) { - goUpdateDownloadCommands(); - } - - // Ensure the interface has been updated based on the new values. We need to - // do this because history downloads can't trigger update notifications. - this._updateProgress(); - }), + if (!this._targetFileInfoFetched) + this._fetchTargetFileInfo(); + } }; /** * A Downloads Places View is a places view designed to show a places query - * for history downloads alongside the session downloads. + * for history downloads alongside the current "session"-downloads. * * As we don't use the places controller, some methods implemented by other * places views are not implemented by this view. @@ -517,7 +787,7 @@ function DownloadsPlacesView(aRichListBox, aActive = true) { this._downloadElementsShellsForURI = new Map(); // Map download data items to their element shells. - this._viewItemsForDownloads = new WeakMap(); + this._viewItemsForDataItems = new WeakMap(); // Points to the last session download element. We keep track of this // in order to keep all session downloads above past downloads. @@ -538,15 +808,15 @@ function DownloadsPlacesView(aRichListBox, aActive = true) { DownloadsCommon.getIndicatorData(window).attention = false; // Make sure to unregister the view if the window is closed. - window.addEventListener("unload", () => { + window.addEventListener("unload", function() { window.controllers.removeController(this); this._downloadsData.removeView(this); this.result = null; - }, true); + }.bind(this), true); // Resizing the window may change items visibility. - window.addEventListener("resize", () => { + window.addEventListener("resize", function() { this._ensureVisibleElementsAreActive(); - }, true); + }.bind(this), true); } DownloadsPlacesView.prototype = { @@ -560,83 +830,44 @@ DownloadsPlacesView.prototype = { return this._active; }, - /** - * This cache exists in order to optimize the load of the Downloads View, when - * Places annotations for history downloads must be read. In fact, annotations - * are stored in a single table, and reading all of them at once is much more - * efficient than an individual query. - * - * When this property is first requested, it reads the annotations for all the - * history downloads and stores them indefinitely. - * - * The historical annotations are not expected to change for the duration of - * the session, except in the case where a session download is running for the - * same URI as a history download. To ensure we don't use stale data, URIs - * corresponding to session downloads are permanently removed from the cache. - * This is a very small mumber compared to history downloads. - * - * This property returns a Map from each download source URI found in Places - * annotations to an object with the format: - * - * { targetFileSpec, state, endTime, fileSize, ... } - * - * The targetFileSpec property is the value of "downloads/destinationFileURI", - * while the other properties are taken from "downloads/metaData". Any of the - * properties may be missing from the object. - */ - get _cachedPlacesMetaData() { - if (!this.__cachedPlacesMetaData) { - this.__cachedPlacesMetaData = new Map(); - - // Read the metadata annotations first, but ignore invalid JSON. - for (let result of PlacesUtils.annotations.getAnnotationsWithName( - DOWNLOAD_META_DATA_ANNO)) { - try { - this.__cachedPlacesMetaData.set(result.uri.spec, - JSON.parse(result.annotationValue)); - } catch (ex) {} + _forEachDownloadElementShellForURI: + function DPV__forEachDownloadElementShellForURI(aURI, aCallback) { + if (this._downloadElementsShellsForURI.has(aURI)) { + let downloadElementShells = this._downloadElementsShellsForURI.get(aURI); + for (let des of downloadElementShells) { + aCallback(des); } + } + }, - // Add the target file annotations to the metadata. - for (let result of PlacesUtils.annotations.getAnnotationsWithName( - DESTINATION_FILE_URI_ANNO)) { - let metaData = this.__cachedPlacesMetaData.get(result.uri.spec); - if (!metaData) { - metaData = {}; - this.__cachedPlacesMetaData.set(result.uri.spec, metaData); + _getAnnotationsFor: function DPV_getAnnotationsFor(aURI) { + if (!this._cachedAnnotations) { + this._cachedAnnotations = new Map(); + for (let name of [ DESTINATION_FILE_URI_ANNO, + DOWNLOAD_META_DATA_ANNO ]) { + let results = PlacesUtils.annotations.getAnnotationsWithName(name); + for (let result of results) { + let url = result.uri.spec; + if (!this._cachedAnnotations.has(url)) + this._cachedAnnotations.set(url, new Map()); + let m = this._cachedAnnotations.get(url); + m.set(result.annotationName, result.annotationValue); } - metaData.targetFileSpec = result.annotationValue; } } - return this.__cachedPlacesMetaData; - }, - __cachedPlacesMetaData: null, - - /** - * Reads current metadata from Places annotations for the specified URI, and - * returns an object with the format: - * - * { targetFileSpec, state, endTime, fileSize, ... } - * - * The targetFileSpec property is the value of "downloads/destinationFileURI", - * while the other properties are taken from "downloads/metaData". Any of the - * properties may be missing from the object. - */ - _getPlacesMetaDataFor(spec) { - let metaData = {}; - - try { - let uri = NetUtil.newURI(spec); - try { - metaData = JSON.parse(PlacesUtils.annotations.getPageAnnotation( - uri, DOWNLOAD_META_DATA_ANNO)); - } catch (ex) {} - metaData.targetFileSpec = PlacesUtils.annotations.getPageAnnotation( - uri, DESTINATION_FILE_URI_ANNO); - } catch (ex) {} - - return metaData; + let annotations = this._cachedAnnotations.get(aURI); + if (!annotations) { + // There are no annotations for this entry, that means it is quite old. + // Make up a fake annotations entry with default values. + annotations = new Map(); + annotations.set(DESTINATION_FILE_URI_ANNO, NOT_AVAILABLE); + } + // The meta-data annotation has been added recently, so it's likely missing. + if (!annotations.has(DOWNLOAD_META_DATA_ANNO)) { + annotations.set(DOWNLOAD_META_DATA_ANNO, NOT_AVAILABLE); + } + return annotations; }, /** @@ -651,12 +882,14 @@ DownloadsPlacesView.prototype = { * alongside the other session downloads. If we don't, then we go ahead * and create a new element for the download. * - * @param [optional] sessionDownload - * A Download object, or null for history downloads. + * @param aDataItem + * The data item of a session download. Set to null for history + * downloads data. * @param [optional] aPlacesNode - * The Places node for a history download, or null for session downloads. + * The places node for a history download. Required if there's no data + * item. * @param [optional] aNewest - * @see onDownloadAdded. Ignored for history downloads. + * @see onDataItemAdded. Ignored for history downloads. * @param [optional] aDocumentFragment * To speed up the appending of multiple elements to the end of the * list which are coming in a single batch (i.e. invalidateContainer), @@ -664,28 +897,16 @@ DownloadsPlacesView.prototype = { * be appended. It's the caller's job to ensure the fragment is merged * to the richlistbox at the end. */ - _addDownloadData(sessionDownload, aPlacesNode, aNewest = false, - aDocumentFragment = null) { - let downloadURI = aPlacesNode ? aPlacesNode.uri - : sessionDownload.source.url; + _addDownloadData: + function DPV_addDownloadData(aDataItem, aPlacesNode, aNewest = false, + aDocumentFragment = null) { + let downloadURI = aPlacesNode ? aPlacesNode.uri : aDataItem.uri; let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI); if (!shellsForURI) { shellsForURI = new Set(); this._downloadElementsShellsForURI.set(downloadURI, shellsForURI); } - // When a session download is attached to a shell, we ensure not to keep - // stale metadata around for the corresponding history download. This - // prevents stale state from being used if the view is rebuilt. - // - // Note that we will eagerly load the data in the cache at this point, even - // if we have seen no history download. The case where no history download - // will appear at all is rare enough in normal usage, so we can apply this - // simpler solution rather than keeping a list of cache items to ignore. - if (sessionDownload) { - this._cachedPlacesMetaData.delete(sessionDownload.source.url); - } - let newOrUpdatedShell = null; // Trivial: if there are no shells for this download URI, we always @@ -703,63 +924,44 @@ DownloadsPlacesView.prototype = { // item). // // Note: If a cancelled session download is already in the list, and the - // download is retried, onDownloadAdded is called again for the same + // download is retired, onDataItemAdded is called again for the same // data item. Thus, we also check that we make sure we don't have a view item // already. if (!shouldCreateShell && - sessionDownload && !this._viewItemsForDownloads.has(sessionDownload)) { + aDataItem && this.getViewItem(aDataItem) == null) { // If there's a past-download-only shell for this download-uri with no // associated data item, use it for the new data item. Otherwise, go ahead // and create another shell. shouldCreateShell = true; for (let shell of shellsForURI) { - if (!shell.sessionDownload) { + if (!shell.dataItem) { shouldCreateShell = false; - shell.sessionDownload = sessionDownload; + shell.dataItem = aDataItem; newOrUpdatedShell = shell; - this._viewItemsForDownloads.set(sessionDownload, shell); + this._viewItemsForDataItems.set(aDataItem, shell); break; } } } if (shouldCreateShell) { - // If we are adding a new history download here, it means there is no - // associated session download, thus we must read the Places metadata, - // because it will not be obscured by the session download. - let historyDownload = null; - if (aPlacesNode) { - let metaData = this._cachedPlacesMetaData.get(aPlacesNode.uri) || - this._getPlacesMetaDataFor(aPlacesNode.uri); - historyDownload = new HistoryDownload(aPlacesNode); - historyDownload.updateFromMetaData(metaData); - } - let shell = new HistoryDownloadElementShell(sessionDownload, - historyDownload); - shell.element._placesNode = aPlacesNode; + // Bug 836271: The annotations for a url should be cached only when the + // places node is available, i.e. when we know we we'd be notified for + // annotation changes. + // Otherwise we may cache NOT_AVILABLE values first for a given session + // download, and later use these NOT_AVILABLE values when a history + // download for the same URL is added. + let cachedAnnotations = aPlacesNode ? this._getAnnotationsFor(downloadURI) : null; + let shell = new DownloadElementShell(aDataItem, aPlacesNode, cachedAnnotations); newOrUpdatedShell = shell; shellsForURI.add(shell); - if (sessionDownload) { - this._viewItemsForDownloads.set(sessionDownload, shell); - } - } else if (aPlacesNode) { - // We are updating information for a history download for which we have - // at least one download element shell already. There are two cases: - // 1) There are one or more download element shells for this source URI, - // each with an associated session download. We update the Places node - // because we may need it later, but we don't need to read the Places - // metadata until the last session download is removed. - // 2) Occasionally, we may receive a duplicate notification for a history - // download with no associated session download. We have exactly one - // download element shell in this case, but the metdata cannot have - // changed, just the reference to the Places node object is different. - // So, we update all the node references and keep the metadata intact. + if (aDataItem) + this._viewItemsForDataItems.set(aDataItem, shell); + } + else if (aPlacesNode) { for (let shell of shellsForURI) { - if (!shell.historyDownload) { - // Create the element to host the metadata when needed. - shell.historyDownload = new HistoryDownload(aPlacesNode); - } - shell.element._placesNode = aPlacesNode; + if (shell.placesNode != aPlacesNode) + shell.placesNode = aPlacesNode; } } @@ -774,12 +976,14 @@ DownloadsPlacesView.prototype = { // the top of the richlistbox, along with other session downloads. // More generally, if a new download is added, should be made visible. this._richlistbox.ensureElementIsVisible(newOrUpdatedShell.element); - } else if (sessionDownload) { + } + else if (aDataItem) { let before = this._lastSessionDownloadElement ? this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild; this._richlistbox.insertBefore(newOrUpdatedShell.element, before); this._lastSessionDownloadElement = newOrUpdatedShell.element; - } else { + } + else { let appendTo = aDocumentFragment || this._richlistbox; appendTo.appendChild(newOrUpdatedShell.element); } @@ -798,7 +1002,7 @@ DownloadsPlacesView.prototype = { } }, - _removeElement(aElement) { + _removeElement: function DPV__removeElement(aElement) { // If the element was selected exclusively, select its next // sibling first, if not, try for previous sibling, if any. if ((aElement.nextSibling || aElement.previousSibling) && @@ -809,9 +1013,8 @@ DownloadsPlacesView.prototype = { aElement.previousSibling); } - if (this._lastSessionDownloadElement == aElement) { + if (this._lastSessionDownloadElement == aElement) this._lastSessionDownloadElement = aElement.previousSibling; - } this._richlistbox.removeItemFromSelection(aElement); this._richlistbox.removeChild(aElement); @@ -819,14 +1022,16 @@ DownloadsPlacesView.prototype = { goUpdateCommand("downloadsCmd_clearDownloads"); }, - _removeHistoryDownloadFromView(aPlacesNode) { + _removeHistoryDownloadFromView: + function DPV__removeHistoryDownloadFromView(aPlacesNode) { let downloadURI = aPlacesNode.uri; let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI); if (shellsForURI) { for (let shell of shellsForURI) { - if (shell.sessionDownload) { - shell.historyDownload = null; - } else { + if (shell.dataItem) { + shell.placesNode = null; + } + else { this._removeElement(shell.element); shellsForURI.delete(shell); if (shellsForURI.size == 0) @@ -836,42 +1041,33 @@ DownloadsPlacesView.prototype = { } }, - _removeSessionDownloadFromView(download) { - let shells = this._downloadElementsShellsForURI - .get(download.source.url); - if (shells.size == 0) { + _removeSessionDownloadFromView: + function DPV__removeSessionDownloadFromView(aDataItem) { + let shells = this._downloadElementsShellsForURI.get(aDataItem.uri); + if (shells.size == 0) throw new Error("Should have had at leaat one shell for this uri"); - } - let shell = this._viewItemsForDownloads.get(download); - if (!shells.has(shell)) { + let shell = this.getViewItem(aDataItem); + if (!shells.has(shell)) throw new Error("Missing download element shell in shells list for url"); - } // If there's more than one item for this download uri, we can let the // view item for this this particular data item go away. // If there's only one item for this download uri, we should only // keep it if it is associated with a history download. - if (shells.size > 1 || !shell.historyDownload) { + if (shells.size > 1 || !shell.placesNode) { this._removeElement(shell.element); shells.delete(shell); - if (shells.size == 0) { - this._downloadElementsShellsForURI.delete(download.source.url); - } - } else { - // We have one download element shell containing both a session download - // and a history download, and we are now removing the session download. - // Previously, we did not use the Places metadata because it was obscured - // by the session download. Since this is no longer the case, we have to - // read the latest metadata before removing the session download. - let url = shell.historyDownload.source.url; - let metaData = this._getPlacesMetaDataFor(url); - shell.historyDownload.updateFromMetaData(metaData); - shell.sessionDownload = null; + if (shells.size == 0) + this._downloadElementsShellsForURI.delete(aDataItem.uri); + } + else { + shell.dataItem = null; // Move it below the session-download items; if (this._lastSessionDownloadElement == shell.element) { this._lastSessionDownloadElement = shell.element.previousSibling; - } else { + } + else { let before = this._lastSessionDownloadElement ? this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild; this._richlistbox.insertBefore(shell.element, before); @@ -879,17 +1075,15 @@ DownloadsPlacesView.prototype = { } }, - _ensureVisibleElementsAreActive() { - if (!this.active || this._ensureVisibleTimer || - !this._richlistbox.firstChild) { + _ensureVisibleElementsAreActive: + function DPV__ensureVisibleElementsAreActive() { + if (!this.active || this._ensureVisibleTimer || !this._richlistbox.firstChild) return; - } - this._ensureVisibleTimer = setTimeout(() => { + this._ensureVisibleTimer = setTimeout(function() { delete this._ensureVisibleTimer; - if (!this._richlistbox.firstChild) { + if (!this._richlistbox.firstChild) return; - } let rlbRect = this._richlistbox.getBoundingClientRect(); let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor) @@ -907,9 +1101,8 @@ DownloadsPlacesView.prototype = { // The first visible node is the last match. firstVisibleNode = node; // While the last visible node is the first match. - if (!lastVisibleNode) { + if (!lastVisibleNode) lastVisibleNode = node; - } } } @@ -917,16 +1110,14 @@ DownloadsPlacesView.prototype = { // above and below the visible area) to ensure proper keyboard navigation // in both directions. let nodeBelowVisibleArea = lastVisibleNode && lastVisibleNode.nextSibling; - if (nodeBelowVisibleArea && nodeBelowVisibleArea._shell) { + if (nodeBelowVisibleArea && nodeBelowVisibleArea._shell) nodeBelowVisibleArea._shell.ensureActive(); - } - let nodeAboveVisibleArea = firstVisibleNode && - firstVisibleNode.previousSibling; - if (nodeAboveVisibleArea && nodeAboveVisibleArea._shell) { - nodeAboveVisibleArea._shell.ensureActive(); - } - }, 10); + let nodeABoveVisibleArea = + firstVisibleNode && firstVisibleNode.previousSibling; + if (nodeABoveVisibleArea && nodeABoveVisibleArea._shell) + nodeABoveVisibleArea._shell.ensureActive(); + }.bind(this), 10); }, _place: "", @@ -944,9 +1135,8 @@ DownloadsPlacesView.prototype = { let history = PlacesUtils.history; let queries = { }, options = { }; history.queryStringToQueries(val, queries, { }, options); - if (!queries.value.length) { + if (!queries.value.length) queries.value = [history.getNewQuery()]; - } let result = history.executeQueries(queries.value, queries.value.length, options.value); @@ -957,9 +1147,8 @@ DownloadsPlacesView.prototype = { _result: null, get result() this._result, set result(val) { - if (this._result == val) { + if (this._result == val) return val; - } if (this._result) { this._result.removeObserver(this); @@ -971,7 +1160,8 @@ DownloadsPlacesView.prototype = { this._resultNode = val.root; this._resultNode.containerOpen = true; this._ensureInitialSelection(); - } else { + } + else { delete this._resultNode; delete this._result; } @@ -980,9 +1170,13 @@ DownloadsPlacesView.prototype = { }, get selectedNodes() { - return [for (element of this._richlistbox.selectedItems) - if (element._placesNode) - element._placesNode]; + let placesNodes = []; + let selectedElements = this._richlistbox.selectedItems; + for (let elt of selectedElements) { + if (elt._shell.placesNode) + placesNodes.push(elt._shell.placesNode); + } + return placesNodes; }, get selectedNode() { @@ -992,17 +1186,17 @@ DownloadsPlacesView.prototype = { get hasSelection() this.selectedNodes.length > 0, - containerStateChanged(aNode, aOldState, aNewState) { + containerStateChanged: + function DPV_containerStateChanged(aNode, aOldState, aNewState) { this.invalidateContainer(aNode) }, - invalidateContainer(aContainer) { - if (aContainer != this._resultNode) { + invalidateContainer: + function DPV_invalidateContainer(aContainer) { + if (aContainer != this._resultNode) throw new Error("Unexpected container node"); - } - if (!aContainer.containerOpen) { + if (!aContainer.containerOpen) throw new Error("Root container for the downloads query cannot be closed"); - } let suppressOnSelect = this._richlistbox.suppressOnSelect; this._richlistbox.suppressOnSelect = true; @@ -1012,11 +1206,11 @@ DownloadsPlacesView.prototype = { // Loop backwards since _removeHistoryDownloadFromView may removeChild(). for (let i = this._richlistbox.childNodes.length - 1; i >= 0; --i) { let element = this._richlistbox.childNodes[i]; - if (element._placesNode) { - this._removeHistoryDownloadFromView(element._placesNode); - } + if (element._shell.placesNode) + this._removeHistoryDownloadFromView(element._shell.placesNode); } - } finally { + } + finally { this._richlistbox.suppressOnSelect = suppressOnSelect; } @@ -1026,7 +1220,8 @@ DownloadsPlacesView.prototype = { try { this._addDownloadData(null, aContainer.getChild(i), false, elementsToAppendFragment); - } catch (ex) { + } + catch(ex) { Cu.reportError(ex); } } @@ -1042,7 +1237,7 @@ DownloadsPlacesView.prototype = { goUpdateDownloadCommands(); }, - _appendDownloadsFragment(aDOMFragment) { + _appendDownloadsFragment: function DPV__appendDownloadsFragment(aDOMFragment) { // Workaround multiple reflows hang by removing the richlistbox // and adding it back when we're done. @@ -1064,26 +1259,41 @@ DownloadsPlacesView.prototype = { } }, - nodeInserted(aParent, aPlacesNode) { + nodeInserted: function DPV_nodeInserted(aParent, aPlacesNode) { this._addDownloadData(null, aPlacesNode); }, - nodeRemoved(aParent, aPlacesNode, aOldIndex) { + nodeRemoved: function DPV_nodeRemoved(aParent, aPlacesNode, aOldIndex) { this._removeHistoryDownloadFromView(aPlacesNode); }, - nodeAnnotationChanged() {}, - nodeIconChanged() {}, - nodeTitleChanged() {}, - nodeKeywordChanged() {}, - nodeDateAddedChanged() {}, - nodeLastModifiedChanged() {}, - nodeHistoryDetailsChanged() {}, - nodeTagsChanged() {}, - sortingChanged() {}, - nodeMoved() {}, - nodeURIChanged() {}, - batching() {}, + nodeIconChanged: function DPV_nodeIconChanged(aNode) { + this._forEachDownloadElementShellForURI(aNode.uri, function(aDownloadElementShell) { + aDownloadElementShell.placesNodeIconChanged(); + }); + }, + + nodeAnnotationChanged: function DPV_nodeAnnotationChanged(aNode, aAnnoName) { + this._forEachDownloadElementShellForURI(aNode.uri, function(aDownloadElementShell) { + aDownloadElementShell.placesNodeAnnotationChanged(aAnnoName); + }); + }, + + nodeTitleChanged: function DPV_nodeTitleChanged(aNode, aNewTitle) { + this._forEachDownloadElementShellForURI(aNode.uri, function(aDownloadElementShell) { + aDownloadElementShell.placesNodeTitleChanged(); + }); + }, + + nodeKeywordChanged: function() {}, + nodeDateAddedChanged: function() {}, + nodeLastModifiedChanged: function() {}, + nodeHistoryDetailsChanged: function() {}, + nodeTagsChanged: function() {}, + sortingChanged: function() {}, + nodeMoved: function() {}, + nodeURIChanged: function() {}, + batching: function() {}, get controller() this._richlistbox.controller, @@ -1113,7 +1323,7 @@ DownloadsPlacesView.prototype = { * data is done loading. However, if the selection has changed in-between, * we assume the user has already started using the view and give up. */ - _ensureInitialSelection() { + _ensureInitialSelection: function DPV__ensureInitialSelection() { // Either they're both null, or the selection has not changed in between. if (this._richlistbox.selectedItem == this._initiallySelectedElement) { let firstDownloadElement = this._richlistbox.firstChild; @@ -1123,37 +1333,32 @@ DownloadsPlacesView.prototype = { // first item is activated, and pass the item to the richlistbox // setters only at a point we know for sure the binding is attached. firstDownloadElement._shell.ensureActive(); - Services.tm.mainThread.dispatch(() => { + Services.tm.mainThread.dispatch(function() { this._richlistbox.selectedItem = firstDownloadElement; this._richlistbox.currentItem = firstDownloadElement; this._initiallySelectedElement = firstDownloadElement; - }, Ci.nsIThread.DISPATCH_NORMAL); + }.bind(this), Ci.nsIThread.DISPATCH_NORMAL); } } }, - onDataLoadStarting() {}, - onDataLoadCompleted() { + onDataLoadStarting: function() { }, + onDataLoadCompleted: function DPV_onDataLoadCompleted() { this._ensureInitialSelection(); }, - onDownloadAdded(download, newest) { - this._addDownloadData(download, null, newest); - }, - - onDownloadStateChanged(download) { - this._viewItemsForDownloads.get(download).onStateChanged(); + onDataItemAdded: function DPV_onDataItemAdded(aDataItem, aNewest) { + this._addDownloadData(aDataItem, null, aNewest); }, - onDownloadChanged(download) { - this._viewItemsForDownloads.get(download).onChanged(); + onDataItemRemoved: function DPV_onDataItemRemoved(aDataItem) { + this._removeSessionDownloadFromView(aDataItem); }, - onDownloadRemoved(download) { - this._removeSessionDownloadFromView(download); - }, + getViewItem: function(aDataItem) + this._viewItemsForDataItems.get(aDataItem, null), - supportsCommand(aCommand) { + supportsCommand: function DPV_supportsCommand(aCommand) { if (DOWNLOAD_VIEW_SUPPORTED_COMMANDS.indexOf(aCommand) != -1) { // The clear-downloads command may be performed by the toolbar-button, // which can be focused on OS X. Thus enable this command even if the @@ -1171,7 +1376,7 @@ DownloadsPlacesView.prototype = { return false; }, - isCommandEnabled(aCommand) { + isCommandEnabled: function DPV_isCommandEnabled(aCommand) { switch (aCommand) { case "cmd_copy": return this._richlistbox.selectedItems.length > 0; @@ -1182,36 +1387,34 @@ DownloadsPlacesView.prototype = { case "downloadsCmd_clearDownloads": return this._canClearDownloads(); default: - return Array.every(this._richlistbox.selectedItems, - element => element._shell.isCommandEnabled(aCommand)); + return Array.every(this._richlistbox.selectedItems, function(element) { + return element._shell.isCommandEnabled(aCommand); + }); } }, - _canClearDownloads() { + _canClearDownloads: function DPV__canClearDownloads() { // Downloads can be cleared if there's at least one removable download in // the list (either a history download or a completed session download). // Because history downloads are always removable and are listed after the // session downloads, check from bottom to top. for (let elt = this._richlistbox.lastChild; elt; elt = elt.previousSibling) { - // Stopped, paused, and failed downloads with partial data are removed. - let download = elt._shell.download; - if (download.stopped && !(download.canceled && download.hasPartialData)) { + if (elt._shell.placesNode || !elt._shell.dataItem.inProgress) return true; - } } return false; }, - _copySelectedDownloadsToClipboard() { - let urls = [for (element of this._richlistbox.selectedItems) - element._shell.download.source.url]; + _copySelectedDownloadsToClipboard: + function DPV__copySelectedDownloadsToClipboard() { + let selectedElements = this._richlistbox.selectedItems; + let urls = [e._shell.downloadURI for each (e in selectedElements)]; - Cc["@mozilla.org/widget/clipboardhelper;1"] - .getService(Ci.nsIClipboardHelper) - .copyString(urls.join("\n"), document); + Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper).copyString(urls.join("\n"), document); }, - _getURLFromClipboardData() { + _getURLFromClipboardData: function DPV__getURLFromClipboardData() { let trans = Cc["@mozilla.org/widget/transferable;1"]. createInstance(Ci.nsITransferable); trans.init(null); @@ -1227,27 +1430,25 @@ DownloadsPlacesView.prototype = { trans.getAnyTransferData({}, data, {}); let [url, name] = data.value.QueryInterface(Ci.nsISupportsString) .data.split("\n"); - if (url) { + if (url) return [NetUtil.newURI(url, null, null).spec, name]; - } - } catch (ex) {} + } + catch(ex) { } return ["", ""]; }, - _canDownloadClipboardURL() { + _canDownloadClipboardURL: function DPV__canDownloadClipboardURL() { let [url, name] = this._getURLFromClipboardData(); return url != ""; }, - _downloadURLFromClipboard() { + _downloadURLFromClipboard: function DPV__downloadURLFromClipboard() { let [url, name] = this._getURLFromClipboardData(); - let browserWin = RecentWindow.getMostRecentBrowserWindow(); - let initiatingDoc = browserWin ? browserWin.document : document; - DownloadURL(url, name, initiatingDoc); + DownloadURL(url, name); }, - doCommand(aCommand) { + doCommand: function DPV_doCommand(aCommand) { switch (aCommand) { case "cmd_copy": this._copySelectedDownloadsToClipboard(); @@ -1282,32 +1483,26 @@ DownloadsPlacesView.prototype = { } }, - onEvent() {}, + onEvent: function() { }, - onContextMenu(aEvent) { + onContextMenu: function DPV_onContextMenu(aEvent) + { let element = this._richlistbox.selectedItem; - if (!element || !element._shell) { + if (!element || !element._shell) return false; - } // Set the state attribute so that only the appropriate items are displayed. let contextMenu = document.getElementById("downloadsContextMenu"); - let download = element._shell.download; - contextMenu.setAttribute("state", - DownloadsCommon.stateOfDownload(download)); - contextMenu.classList.toggle("temporary-block", - !!download.hasBlockedData); - - if (!download.stopped) { - // The hasPartialData property of a download may change at any time after - // it has started, so ensure we update the related command now. - goUpdateCommand("downloadsCmd_pauseResume"); - } + let state = element._shell.getDownloadMetaData().state; + if (state !== undefined) + contextMenu.setAttribute("state", state); + else + contextMenu.removeAttribute("state"); return true; }, - onKeyPress(aEvent) { + onKeyPress: function DPV_onKeyPress(aEvent) { let selectedElements = this._richlistbox.selectedItems; if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) { // In the content tree, opening bookmarks by pressing return is only @@ -1315,70 +1510,59 @@ DownloadsPlacesView.prototype = { // same here. if (selectedElements.length == 1) { let element = selectedElements[0]; - if (element._shell) { + if (element._shell) element._shell.doDefaultCommand(); - } } } else if (aEvent.charCode == " ".charCodeAt(0)) { // Pause/Resume every selected download for (let element of selectedElements) { - if (element._shell.isCommandEnabled("downloadsCmd_pauseResume")) { + if (element._shell.isCommandEnabled("downloadsCmd_pauseResume")) element._shell.doCommand("downloadsCmd_pauseResume"); - } } } }, - onDoubleClick(aEvent) { - if (aEvent.button != 0) { + onDoubleClick: function DPV_onDoubleClick(aEvent) { + if (aEvent.button != 0) return; - } let selectedElements = this._richlistbox.selectedItems; - if (selectedElements.length != 1) { + if (selectedElements.length != 1) return; - } let element = selectedElements[0]; - if (element._shell) { + if (element._shell) element._shell.doDefaultCommand(); - } }, - onScroll() { + onScroll: function DPV_onScroll() { this._ensureVisibleElementsAreActive(); }, - onSelect() { + onSelect: function DPV_onSelect() { goUpdateDownloadCommands(); let selectedElements = this._richlistbox.selectedItems; for (let elt of selectedElements) { - if (elt._shell) { + if (elt._shell) elt._shell.onSelect(); - } } }, - onDragStart(aEvent) { + onDragStart: function DPV_onDragStart(aEvent) { // TODO Bug 831358: Support d&d for multiple selection. // For now, we just drag the first element. let selectedItem = this._richlistbox.selectedItem; - if (!selectedItem) { + if (!selectedItem) return; - } - let targetPath = selectedItem._shell.download.target.path; - if (!targetPath) { + let metaData = selectedItem._shell.getDownloadMetaData(); + if (!("filePath" in metaData)) return; - } - - // We must check for existence synchronously because this is a DOM event. - let file = new FileUtils.File(targetPath); - if (!file.exists()) { + let file = new FileUtils.File(metaData.filePath); + if (!file.exists()) return; - } let dt = aEvent.dataTransfer; dt.mozSetDataAt("application/x-moz-file", file, 0); @@ -1389,7 +1573,7 @@ DownloadsPlacesView.prototype = { dt.addElement(selectedItem); }, - onDragOver(aEvent) { + onDragOver: function DPV_onDragOver(aEvent) { let types = aEvent.dataTransfer.types; if (types.contains("text/uri-list") || types.contains("text/x-moz-url") || @@ -1398,28 +1582,23 @@ DownloadsPlacesView.prototype = { } }, - onDrop(aEvent) { + onDrop: function DPV_onDrop(aEvent) { let dt = aEvent.dataTransfer; // If dragged item is from our source, do not try to // redownload already downloaded file. - if (dt.mozGetDataAt("application/x-moz-file", 0)) { + if (dt.mozGetDataAt("application/x-moz-file", 0)) return; - } - let name = {}; + let name = { }; let url = Services.droppedLinkHandler.dropLink(aEvent, name); - if (url) { - let browserWin = RecentWindow.getMostRecentBrowserWindow(); - let initiatingDoc = browserWin ? browserWin.document : document; - DownloadURL(url, name.value, initiatingDoc); - } - }, + if (url) + DownloadURL(url, name.value); + } }; for (let methodName of ["load", "applyFilter", "selectNode", "selectItems"]) { - DownloadsPlacesView.prototype[methodName] = function () { - throw new Error("|" + methodName + - "| is not implemented by the downloads view."); + DownloadsPlacesView.prototype[methodName] = function() { + throw new Error("|" + methodName + "| is not implemented by the downloads view."); } } diff --git a/browser-omni/chrome/kmeleon/content/browser/downloads/allDownloadsViewOverlay.xul b/browser-omni/chrome/kmeleon/content/browser/downloads/allDownloadsViewOverlay.xul index 7ba25542..1f19b307 100644 --- a/browser-omni/chrome/kmeleon/content/browser/downloads/allDownloadsViewOverlay.xul +++ b/browser-omni/chrome/kmeleon/content/browser/downloads/allDownloadsViewOverlay.xul @@ -58,10 +58,6 @@ oncommand="goDoCommand('downloadsCmd_pauseResume')"/> - - - + value="&downloadsListEmpty.label;"/> diff --git a/browser-omni/chrome/kmeleon/content/browser/downloads/download.css b/browser-omni/chrome/kmeleon/content/browser/downloads/download.css index c780e2b1..7412fa72 100644 --- a/browser-omni/chrome/kmeleon/content/browser/downloads/download.css +++ b/browser-omni/chrome/kmeleon/content/browser/downloads/download.css @@ -34,13 +34,6 @@ richlistitem.download button { [state="4"]) /* Paused */) > .downloadCancel, -/* Blocked (dirty) downloads that have not been confirmed and - have temporary data. */ -.download-state:not( [state="8"]) - > .downloadConfirmBlock, -.download-state[state="8"]:not(.temporary-block) - > .downloadConfirmBlock, - .download-state[state]:not(:-moz-any([state="2"], /* Failed */ [state="3"]) /* Canceled */) > .downloadRetry, diff --git a/browser-omni/chrome/kmeleon/content/browser/downloads/download.xml b/browser-omni/chrome/kmeleon/content/browser/downloads/download.xml index d907a653..6deef3e8 100644 --- a/browser-omni/chrome/kmeleon/content/browser/downloads/download.xml +++ b/browser-omni/chrome/kmeleon/content/browser/downloads/download.xml @@ -33,7 +33,7 @@ summary isn't being displayed, so we ensure that items share the same minimum width. --> - @@ -56,9 +56,46 @@ - + + + + + + + + + + + + + + + + + @@ -75,7 +112,7 @@ xbl:inherits="src=image"/> - - + - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/browser-omni/chrome/kmeleon/content/browser/downloads/downloads.css b/browser-omni/chrome/kmeleon/content/browser/downloads/downloads.css index abc517a0..825db683 100644 --- a/browser-omni/chrome/kmeleon/content/browser/downloads/downloads.css +++ b/browser-omni/chrome/kmeleon/content/browser/downloads/downloads.css @@ -8,6 +8,15 @@ richlistitem[type="download"] { -moz-binding: url('chrome://browser/content/downloads/download.xml#download'); } +richlistitem[type="download"]:-moz-any([state="-1"],/* Starting (initial) */ + [state="0"], /* Downloading */ + [state="4"], /* Paused */ + [state="5"], /* Starting (queued) */ + [state="7"]) /* Scanning */ +{ + -moz-binding: url('chrome://browser/content/downloads/download.xml#download-in-progress'); +} + richlistitem[type="download"]:not([selected]) button { /* Only focus buttons in the selected item. */ -moz-user-focus: none; @@ -26,9 +35,9 @@ richlistitem[type="download"]:not([selected]) button { .downloadTypeIcon.blockedIcon, .download-state:not(:-moz-any([state="-1"],/* Starting (initial) */ - [state="5"], /* Starting (queued) */ [state="0"], /* Downloading */ [state="4"], /* Paused */ + [state="5"], /* Starting (queued) */ [state="7"]) /* Scanning */) .downloadProgress, @@ -38,13 +47,6 @@ richlistitem[type="download"]:not([selected]) button { .download-state:not( [state="4"] /* Paused */) .downloadResumeMenuItem, -/* Blocked (dirty) downloads that have not been confirmed and - have temporary data. */ -.download-state:not( [state="8"] /* Blocked (dirty) */) - .downloadUnblockMenuItem, -.download-state[state="8"]:not(.temporary-block) - .downloadUnblockMenuItem, - .download-state:not(:-moz-any([state="2"], /* Failed */ [state="4"]) /* Paused */) .downloadCancelMenuItem, @@ -64,33 +66,35 @@ richlistitem[type="download"]:not([selected]) button { [state="5"]) /* Starting (queued) */) .downloadShowMenuItem, -.download-state[state="7"] .downloadCommandsSeparator +.download-state[state="7"] /* Scanning */ .downloadCommandsSeparator { display: none; } -/*** Visibility of download buttons ***/ +/*** Visibility of download buttons and indicator controls. ***/ .download-state:not(:-moz-any([state="-1"],/* Starting (initial) */ - [state="5"], /* Starting (queued) */ [state="0"], /* Downloading */ - [state="4"]) /* Paused */) + [state="4"], /* Paused */ + [state="5"]) /* Starting (queued) */) .downloadCancel, -/* Blocked (dirty) downloads that have not been confirmed and - have temporary data. */ -.download-state:not( [state="8"] /* Blocked (dirty) */) - .downloadConfirmBlock, -.download-state[state="8"]:not(.temporary-block) - .downloadConfirmBlock, - .download-state:not(:-moz-any([state="2"], /* Failed */ [state="3"]) /* Canceled */) .downloadRetry, .download-state:not( [state="1"] /* Finished */) - .downloadShow + .downloadShow, + +#downloads-indicator:-moz-any([progress], + [counter], + [paused]) #downloads-indicator-icon, + +#downloads-indicator:not(:-moz-any([progress], + [counter], + [paused])) + #downloads-indicator-progress-area { visibility: hidden; @@ -108,3 +112,21 @@ richlistitem[type="download"]:not([selected]) button { { display: none; } + +/* Hacks for toolbar full and text modes, until bug 573329 removes them */ + +toolbar[mode="text"] > #downloads-indicator { + display: -moz-box; + -moz-box-orient: vertical; + -moz-box-pack: center; +} + +toolbar[mode="text"] > #downloads-indicator > .toolbarbutton-text { + -moz-box-ordinal-group: 1; +} + +toolbar[mode="text"] > #downloads-indicator > .toolbarbutton-icon { + display: -moz-box; + -moz-box-ordinal-group: 2; + visibility: collapse; +} diff --git a/browser-omni/chrome/kmeleon/content/browser/downloads/downloads.js b/browser-omni/chrome/kmeleon/content/browser/downloads/downloads.js index d4ecd455..ce30e8bf 100644 --- a/browser-omni/chrome/kmeleon/content/browser/downloads/downloads.js +++ b/browser-omni/chrome/kmeleon/content/browser/downloads/downloads.js @@ -1,4 +1,4 @@ -/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, @@ -64,22 +64,21 @@ "use strict"; -let { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +//////////////////////////////////////////////////////////////////////////////// +//// Globals +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", + "resource://gre/modules/DownloadUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", "resource:///modules/DownloadsCommon.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "DownloadsViewUI", - "resource:///modules/DownloadsViewUI.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", - "resource://gre/modules/FileUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", - "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); //////////////////////////////////////////////////////////////////////////////// //// DownloadsPanel @@ -122,7 +121,8 @@ const DownloadsPanel = { * @param aCallback * Called when initialization is complete. */ - initialize(aCallback) { + initialize: function DP_initialize(aCallback) + { DownloadsCommon.log("Attempting to initialize DownloadsPanel for a window."); if (this._state != this.kStateUninitialized) { DownloadsCommon.log("DownloadsPanel is already initialized."); @@ -134,14 +134,17 @@ const DownloadsPanel = { window.addEventListener("unload", this.onWindowUnload, false); - // Load and resume active downloads if required. If there are downloads to - // be shown in the panel, they will be loaded asynchronously. + // Ensure that the Download Manager service is running. This resumes + // active downloads if required. If there are downloads to be shown in the + // panel, starting the service will make us load their data asynchronously. DownloadsCommon.initializeAllDataLinks(); + // Now that data loading has eventually started, load the required XUL // elements and initialize our views. DownloadsCommon.log("Ensuring DownloadsPanel overlay loaded."); - DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay, () => { + DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay, + function DP_I_callback() { DownloadsViewController.initialize(); DownloadsCommon.log("Attaching DownloadsView..."); DownloadsCommon.getData(window).addView(DownloadsView); @@ -160,7 +163,8 @@ const DownloadsPanel = { * downloads. The downloads panel can be reopened later, even after this * function has been called. */ - terminate() { + terminate: function DP_terminate() + { DownloadsCommon.log("Attempting to terminate DownloadsPanel for a window."); if (this._state == this.kStateUninitialized) { DownloadsCommon.log("DownloadsPanel was never initialized. Nothing to do."); @@ -191,7 +195,8 @@ const DownloadsPanel = { * Main panel element in the browser window, or null if the panel overlay * hasn't been loaded yet. */ - get panel() { + get panel() + { // If the downloads panel overlay hasn't loaded yet, just return null // without resetting this.panel. let downloadsPanel = document.getElementById("downloadsPanel"); @@ -208,7 +213,8 @@ const DownloadsPanel = { * initialized the first time this method is called, and the panel is shown * only when data is ready. */ - showPanel() { + showPanel: function DP_showPanel() + { DownloadsCommon.log("Opening the downloads panel."); if (this.isPanelShowing) { @@ -217,13 +223,13 @@ const DownloadsPanel = { return; } - this.initialize(() => { + this.initialize(function DP_SP_callback() { // Delay displaying the panel because this function will sometimes be // called while another window is closing (like the window for selecting // whether to save or open the file), and that would cause the panel to // close immediately. - setTimeout(() => this._openPopupIfDataReady(), 0); - }); + setTimeout(function () DownloadsPanel._openPopupIfDataReady(), 0); + }.bind(this)); DownloadsCommon.log("Waiting for the downloads panel to appear."); this._state = this.kStateWaitingData; @@ -233,7 +239,8 @@ const DownloadsPanel = { * Hides the downloads panel, if visible, but keeps the internal state so that * the panel can be reopened quickly if required. */ - hidePanel() { + hidePanel: function DP_hidePanel() + { DownloadsCommon.log("Closing the downloads panel."); if (!this.isPanelShowing) { @@ -253,7 +260,8 @@ const DownloadsPanel = { /** * Indicates whether the panel is shown or will be shown. */ - get isPanelShowing() { + get isPanelShowing() + { return this._state == this.kStateWaitingData || this._state == this.kStateWaitingAnchor || this._state == this.kStateShown; @@ -262,7 +270,8 @@ const DownloadsPanel = { /** * Returns whether the user has started keyboard navigation. */ - get keyFocusing() { + get keyFocusing() + { return this.panel.hasAttribute("keyfocus"); }, @@ -271,7 +280,8 @@ const DownloadsPanel = { * showing focusrings in the panel. Also adds a mousemove event handler to * the panel which disables keyFocusing. */ - set keyFocusing(aValue) { + set keyFocusing(aValue) + { if (aValue) { this.panel.setAttribute("keyfocus", "true"); this.panel.addEventListener("mousemove", this); @@ -286,7 +296,8 @@ const DownloadsPanel = { * Handles the mousemove event for the panel, which disables focusring * visualization. */ - handleEvent(aEvent) { + handleEvent: function DP_handleEvent(aEvent) + { if (aEvent.type == "mousemove") { this.keyFocusing = false; } @@ -298,19 +309,22 @@ const DownloadsPanel = { /** * Called after data loading finished. */ - onViewLoadCompleted() { + onViewLoadCompleted: function DP_onViewLoadCompleted() + { this._openPopupIfDataReady(); }, ////////////////////////////////////////////////////////////////////////////// //// User interface event functions - onWindowUnload() { + onWindowUnload: function DP_onWindowUnload() + { // This function is registered as an event listener, we can't use "this". DownloadsPanel.terminate(); }, - onPopupShown(aEvent) { + onPopupShown: function DP_onPopupShown(aEvent) + { // Ignore events raised by nested popups. if (aEvent.target != aEvent.currentTarget) { return; @@ -331,7 +345,8 @@ const DownloadsPanel = { this._focusPanel(); }, - onPopupHidden(aEvent) { + onPopupHidden: function DP_onPopupHidden(aEvent) + { // Ignore events raised by nested popups. if (aEvent.target != aEvent.currentTarget) { return; @@ -359,7 +374,8 @@ const DownloadsPanel = { /** * Shows or focuses the user interface dedicated to downloads history. */ - showDownloadsHistory() { + showDownloadsHistory: function DP_showDownloadsHistory() + { DownloadsCommon.log("Showing download history."); // Hide the panel before showing another window, otherwise focus will return // to the browser window when the panel closes automatically. @@ -376,7 +392,8 @@ const DownloadsPanel = { * removed in _unattachEventListeners. This is called automatically after the * panel has successfully loaded. */ - _attachEventListeners() { + _attachEventListeners: function DP__attachEventListeners() + { // Handle keydown to support accel-V. this.panel.addEventListener("keydown", this._onKeyDown.bind(this), false); // Handle keypress to be able to preventDefault() events before they reach @@ -388,14 +405,16 @@ const DownloadsPanel = { * Unattach event listeners that were added in _attachEventListeners. This * is called automatically on panel termination. */ - _unattachEventListeners() { + _unattachEventListeners: function DP__unattachEventListeners() + { this.panel.removeEventListener("keydown", this._onKeyDown.bind(this), false); this.panel.removeEventListener("keypress", this._onKeyPress.bind(this), false); }, - _onKeyPress(aEvent) { + _onKeyPress: function DP__onKeyPress(aEvent) + { // Handle unmodified keys only. if (aEvent.altKey || aEvent.ctrlKey || aEvent.shiftKey || aEvent.metaKey) { return; @@ -414,9 +433,8 @@ const DownloadsPanel = { this.keyFocusing = true; // Ensure there's a selection, we will show the focus ring around it and // prevent the richlistbox from changing the selection. - if (DownloadsView.richListBox.selectedIndex == -1) { + if (DownloadsView.richListBox.selectedIndex == -1) DownloadsView.richListBox.selectedIndex = 0; - } aEvent.preventDefault(); return; } @@ -443,7 +461,8 @@ const DownloadsPanel = { * as the the accel-V "paste" event, which initiates a file download if the * pasted item can be resolved to a URI. */ - _onKeyDown(aEvent) { + _onKeyDown: function DP__onKeyDown(aEvent) + { // If the footer is focused and the downloads list has at least 1 element // in it, focus the last element in the list when going up. if (aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP && @@ -456,9 +475,7 @@ const DownloadsPanel = { } let pasting = aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_V && -//@line 462 "g:\mozilla-esr38\browser\components\downloads\content\downloads.js" aEvent.ctrlKey; -//@line 464 "g:\mozilla-esr38\browser\components\downloads\content\downloads.js" if (!pasting) { return; @@ -486,7 +503,8 @@ const DownloadsPanel = { let uri = NetUtil.newURI(url); DownloadsCommon.log("Pasted URL seems valid. Starting download."); - DownloadURL(uri.spec, name, document); + saveURL(uri.spec, name || uri.spec, null, true, true, + undefined, document); } catch (ex) {} }, @@ -494,7 +512,8 @@ const DownloadsPanel = { * Move focus to the main element in the downloads panel, unless another * element in the panel is already focused. */ - _focusPanel() { + _focusPanel: function DP_focusPanel() + { // We may be invoked while the panel is still waiting to be shown. if (this._state != this.kStateShown) { return; @@ -516,7 +535,8 @@ const DownloadsPanel = { /** * Opens the downloads panel when data is ready to be displayed. */ - _openPopupIfDataReady() { + _openPopupIfDataReady: function DP_openPopupIfDataReady() + { // We don't want to open the popup if we already displayed it, or if we are // still loading data. if (this._state != this.kStateWaitingData || DownloadsView.loading) { @@ -527,12 +547,11 @@ const DownloadsPanel = { // Ensure the anchor is visible. If that is not possible, show the panel // anchored to the top area of the window, near the default anchor position. - DownloadsButton.getAnchor(anchor => { + DownloadsButton.getAnchor(function DP_OPIDR_callback(aAnchor) { // If somehow we've switched states already (by getting a panel hiding // event before an overlay is loaded, for example), bail out. - if (this._state != this.kStateWaitingAnchor) { + if (this._state != this.kStateWaitingAnchor) return; - } // At this point, if the window is minimized, opening the panel could fail // without any notification, and there would be no way to either open or @@ -544,23 +563,27 @@ const DownloadsPanel = { return; } - if (!anchor) { - DownloadsCommon.error("Downloads button cannot be found."); - return; - } - // When the panel is opened, we check if the target files of visible items // still exist, and update the allowed items interactions accordingly. We // do these checks on a background thread, and don't prevent the panel to // be displayed while these checks are being performed. - for (let viewItem of DownloadsView._visibleViewItems.values()) { - viewItem.download.refresh().catch(Cu.reportError); + for each (let viewItem in DownloadsView._viewItems) { + viewItem.verifyTargetExists(); } - DownloadsCommon.log("Opening downloads panel popup."); - this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, null); - }); - }, + if (aAnchor) { + DownloadsCommon.log("Opening downloads panel popup."); + this.panel.openPopup(aAnchor, "bottomcenter topright", 0, 0, false, + null); + } else { + DownloadsCommon.error("We can't find the anchor! Failure case - opening", + "downloads panel on TabsToolbar. We should never", + "get here!"); + Components.utils.reportError( + "Downloads button cannot be found"); + } + }.bind(this)); + } }; //////////////////////////////////////////////////////////////////////////////// @@ -598,7 +621,8 @@ const DownloadsOverlayLoader = { * Invoked when loading is completed. If the overlay is already * loaded, the function is called immediately. */ - ensureOverlayLoaded(aOverlay, aCallback) { + ensureOverlayLoaded: function DOL_ensureOverlayLoaded(aOverlay, aCallback) + { // The overlay is already loaded, invoke the callback immediately. if (aOverlay in this._loadedOverlays) { aCallback(); @@ -611,14 +635,21 @@ const DownloadsOverlayLoader = { return; } - this._overlayLoading = true; - DownloadsCommon.log("Loading overlay ", aOverlay); - document.loadOverlay(aOverlay, () => { + function DOL_EOL_loadCallback() { this._overlayLoading = false; this._loadedOverlays[aOverlay] = true; + // Loading the overlay causes all the persisted XUL attributes to be + // reapplied, including "iconsize" on the toolbars. Until bug 640158 is + // fixed, we must recalculate the correct "iconsize" attributes manually. + retrieveToolbarIconsizesFromTheme(); + this.processPendingRequests(); - }); + } + + this._overlayLoading = true; + DownloadsCommon.log("Loading overlay ", aOverlay); + document.loadOverlay(aOverlay, DOL_EOL_loadCallback.bind(this)); }, /** @@ -626,7 +657,8 @@ const DownloadsOverlayLoader = { * and/or loading more overlays as needed. In most cases, there will be a * single request for one overlay, that will be processed immediately. */ - processPendingRequests() { + processPendingRequests: function DOL_processPendingRequests() + { // Re-process all the currently pending requests, yet allow more requests // to be appended at the end of the array if we're not ready for them. let currentLength = this._loadRequests.length; @@ -638,7 +670,7 @@ const DownloadsOverlayLoader = { // for the associated overlay to load. this.ensureOverlayLoaded(request.overlay, request.callback); } - }, + } }; //////////////////////////////////////////////////////////////////////////////// @@ -664,27 +696,28 @@ const DownloadsView = { loading: false, /** - * Ordered array of all Download objects. We need to keep this array because - * only a limited number of items are shown at once, and if an item that is - * currently visible is removed from the list, we might need to take another - * item from the array and make it appear at the bottom. + * Ordered array of all DownloadsDataItem objects. We need to keep this array + * because only a limited number of items are shown at once, and if an item + * that is currently visible is removed from the list, we might need to take + * another item from the array and make it appear at the bottom. */ - _downloads: [], + _dataItems: [], /** - * Associates the visible Download objects with their corresponding - * DownloadsViewItem object. There is a limited number of view items in the - * panel at any given time. + * Object containing the available DownloadsViewItem objects, indexed by their + * numeric download identifier. There is a limited number of view items in + * the panel at any given time. */ - _visibleViewItems: new Map(), + _viewItems: {}, /** * Called when the number of items in the list changes. */ - _itemCountChanged() { + _itemCountChanged: function DV_itemCountChanged() + { DownloadsCommon.log("The downloads item count has changed - we are tracking", - this._downloads.length, "downloads in total."); - let count = this._downloads.length; + this._dataItems.length, "downloads in total."); + let count = this._dataItems.length; let hiddenCount = count - this.kItemCountLimit; if (count > 0) { @@ -704,7 +737,8 @@ const DownloadsView = { /** * Element corresponding to the list of downloads. */ - get richListBox() { + get richListBox() + { delete this.richListBox; return this.richListBox = document.getElementById("downloadsListBox"); }, @@ -712,7 +746,8 @@ const DownloadsView = { /** * Element corresponding to the button for showing more downloads. */ - get downloadsHistory() { + get downloadsHistory() + { delete this.downloadsHistory; return this.downloadsHistory = document.getElementById("downloadsHistory"); }, @@ -723,7 +758,8 @@ const DownloadsView = { /** * Called before multiple downloads are about to be loaded. */ - onDataLoadStarting() { + onDataLoadStarting: function DV_onDataLoadStarting() + { DownloadsCommon.log("onDataLoadStarting called for DownloadsView."); this.loading = true; }, @@ -731,7 +767,8 @@ const DownloadsView = { /** * Called after data loading finished. */ - onDataLoadCompleted() { + onDataLoadCompleted: function DV_onDataLoadCompleted() + { DownloadsCommon.log("onDataLoadCompleted called for DownloadsView."); this.loading = false; @@ -745,12 +782,32 @@ const DownloadsView = { DownloadsPanel.onViewLoadCompleted(); }, + /** + * Called when the downloads database becomes unavailable (for example, + * entering Private Browsing Mode). References to existing data should be + * discarded. + */ + onDataInvalidated: function DV_onDataInvalidated() + { + DownloadsCommon.log("Downloads data has been invalidated. Cleaning up", + "DownloadsView."); + + DownloadsPanel.terminate(); + + // Clear the list by replacing with a shallow copy. + let emptyView = this.richListBox.cloneNode(false); + this.richListBox.parentNode.replaceChild(emptyView, this.richListBox); + this.richListBox = emptyView; + this._viewItems = {}; + this._dataItems = []; + }, + /** * Called when a new download data item is available, either during the * asynchronous data load or when a new download is started. * - * @param aDownload - * Download object that was just added. + * @param aDataItem + * DownloadsDataItem object that was just added. * @param aNewest * When true, indicates that this item is the most recent and should be * added in the topmost position. This happens when a new download is @@ -758,27 +815,28 @@ const DownloadsView = { * and should be appended. The latter generally happens during the * asynchronous data load. */ - onDownloadAdded(download, aNewest) { + onDataItemAdded: function DV_onDataItemAdded(aDataItem, aNewest) + { DownloadsCommon.log("A new download data item was added - aNewest =", aNewest); if (aNewest) { - this._downloads.unshift(download); + this._dataItems.unshift(aDataItem); } else { - this._downloads.push(download); + this._dataItems.push(aDataItem); } - let itemsNowOverflow = this._downloads.length > this.kItemCountLimit; + let itemsNowOverflow = this._dataItems.length > this.kItemCountLimit; if (aNewest || !itemsNowOverflow) { // The newly added item is visible in the panel and we must add the // corresponding element. This is either because it is the first item, or // because it was added at the bottom but the list still doesn't overflow. - this._addViewItem(download, aNewest); + this._addViewItem(aDataItem, aNewest); } if (aNewest && itemsNowOverflow) { // If the list overflows, remove the last item from the panel to make room // for the new one that we just added at the top. - this._removeViewItem(this._downloads[this.kItemCountLimit]); + this._removeViewItem(this._dataItems[this.kItemCountLimit]); } // For better performance during batch loads, don't update the count for @@ -788,39 +846,26 @@ const DownloadsView = { } }, - onDownloadStateChanged(download) { - let viewItem = this._visibleViewItems.get(download); - if (viewItem) { - viewItem.onStateChanged(); - } - }, - - onDownloadChanged(download) { - let viewItem = this._visibleViewItems.get(download); - if (viewItem) { - viewItem.onChanged(); - } - }, - /** * Called when a data item is removed. Ensures that the widget associated * with the view item is removed from the user interface. * - * @param download - * Download object that is being removed. + * @param aDataItem + * DownloadsDataItem object that is being removed. */ - onDownloadRemoved(download) { + onDataItemRemoved: function DV_onDataItemRemoved(aDataItem) + { DownloadsCommon.log("A download data item was removed."); - let itemIndex = this._downloads.indexOf(download); - this._downloads.splice(itemIndex, 1); + let itemIndex = this._dataItems.indexOf(aDataItem); + this._dataItems.splice(itemIndex, 1); if (itemIndex < this.kItemCountLimit) { // The item to remove is visible in the panel. - this._removeViewItem(download); - if (this._downloads.length >= this.kItemCountLimit) { + this._removeViewItem(aDataItem); + if (this._dataItems.length >= this.kItemCountLimit) { // Reinsert the next item into the panel. - this._addViewItem(this._downloads[this.kItemCountLimit - 1], false); + this._addViewItem(this._dataItems[this.kItemCountLimit - 1], false); } } @@ -828,29 +873,43 @@ const DownloadsView = { }, /** - * Associates each richlistitem for a download with its corresponding - * DownloadsViewItemController object. + * Returns the view item associated with the provided data item for this view. + * + * @param aDataItem + * DownloadsDataItem object for which the view item is requested. + * + * @return Object that can be used to notify item status events. */ - _controllersForElements: new Map(), - - controllerForElement(element) { - return this._controllersForElements.get(element); + getViewItem: function DV_getViewItem(aDataItem) + { + // If the item is visible, just return it, otherwise return a mock object + // that doesn't react to notifications. + if (aDataItem.downloadGuid in this._viewItems) { + return this._viewItems[aDataItem.downloadGuid]; + } + return this._invisibleViewItem; }, + /** + * Mock DownloadsDataItem object that doesn't react to notifications. + */ + _invisibleViewItem: Object.freeze({ + onStateChange: function () { }, + onProgressChange: function () { } + }), + /** * Creates a new view item associated with the specified data item, and adds * it to the top or the bottom of the list. */ - _addViewItem(download, aNewest) + _addViewItem: function DV_addViewItem(aDataItem, aNewest) { DownloadsCommon.log("Adding a new DownloadsViewItem to the downloads list.", "aNewest =", aNewest); let element = document.createElement("richlistitem"); - let viewItem = new DownloadsViewItem(download, element); - this._visibleViewItems.set(download, viewItem); - let viewItemController = new DownloadsViewItemController(download); - this._controllersForElements.set(element, viewItemController); + let viewItem = new DownloadsViewItem(aDataItem, element); + this._viewItems[aDataItem.downloadGuid] = viewItem; if (aNewest) { this.richListBox.insertBefore(element, this.richListBox.firstChild); } else { @@ -861,17 +920,17 @@ const DownloadsView = { /** * Removes the view item associated with the specified data item. */ - _removeViewItem(download) { + _removeViewItem: function DV_removeViewItem(aDataItem) + { DownloadsCommon.log("Removing a DownloadsViewItem from the downloads list."); - let element = this._visibleViewItems.get(download).element; + let element = this.getViewItem(aDataItem)._element; let previousSelectedIndex = this.richListBox.selectedIndex; this.richListBox.removeChild(element); if (previousSelectedIndex != -1) { this.richListBox.selectedIndex = Math.min(previousSelectedIndex, this.richListBox.itemCount - 1); } - this._visibleViewItems.delete(download); - this._controllersForElements.delete(element); + delete this._viewItems[aDataItem.downloadGuid]; }, ////////////////////////////////////////////////////////////////////////////// @@ -887,15 +946,17 @@ const DownloadsView = { * @param aCommand * The command to be performed. */ - onDownloadCommand(aEvent, aCommand) { + onDownloadCommand: function DV_onDownloadCommand(aEvent, aCommand) + { let target = aEvent.target; while (target.nodeName != "richlistitem") { target = target.parentNode; } - DownloadsView.controllerForElement(target).doCommand(aCommand); + new DownloadsViewItemController(target).doCommand(aCommand); }, - onDownloadClick(aEvent) { + onDownloadClick: function DV_onDownloadClick(aEvent) + { // Handle primary clicks only, and exclude the action button. if (aEvent.button == 0 && !aEvent.originalTarget.hasAttribute("oncommand")) { @@ -906,7 +967,8 @@ const DownloadsView = { /** * Handles keypress events on a download item. */ - onDownloadKeyPress(aEvent) { + onDownloadKeyPress: function DV_onDownloadKeyPress(aEvent) + { // Pressing the key on buttons should not invoke the action because the // event has already been handled by the button itself. if (aEvent.originalTarget.hasAttribute("command") || @@ -919,21 +981,23 @@ const DownloadsView = { return; } - if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) { + if (aEvent.keyCode == KeyEvent.DOM_VK_ENTER || + aEvent.keyCode == KeyEvent.DOM_VK_RETURN) { goDoCommand("downloadsCmd_doDefault"); } }, + /** * Mouse listeners to handle selection on hover. */ - onDownloadMouseOver(aEvent) { - if (aEvent.originalTarget.parentNode == this.richListBox) { + onDownloadMouseOver: function DV_onDownloadMouseOver(aEvent) + { + if (aEvent.originalTarget.parentNode == this.richListBox) this.richListBox.selectedItem = aEvent.originalTarget; - } }, - - onDownloadMouseOut(aEvent) { + onDownloadMouseOut: function DV_onDownloadMouseOut(aEvent) + { if (aEvent.originalTarget.parentNode == this.richListBox) { // If the destination element is outside of the richlistitem, clear the // selection. @@ -941,13 +1005,13 @@ const DownloadsView = { while (element && element != aEvent.originalTarget) { element = element.parentNode; } - if (!element) { + if (!element) this.richListBox.selectedIndex = -1; - } } }, - onDownloadContextMenu(aEvent) { + onDownloadContextMenu: function DV_onDownloadContextMenu(aEvent) + { let element = this.richListBox.selectedItem; if (!element) { return; @@ -958,33 +1022,31 @@ const DownloadsView = { // Set the state attribute so that only the appropriate items are displayed. let contextMenu = document.getElementById("downloadsContextMenu"); contextMenu.setAttribute("state", element.getAttribute("state")); - contextMenu.classList.toggle("temporary-block", - element.classList.contains("temporary-block")); }, - onDownloadDragStart(aEvent) { + onDownloadDragStart: function DV_onDownloadDragStart(aEvent) + { let element = this.richListBox.selectedItem; if (!element) { return; } - // We must check for existence synchronously because this is a DOM event. - let file = new FileUtils.File(DownloadsView.controllerForElement(element) - .download.target.path); - if (!file.exists()) { + let controller = new DownloadsViewItemController(element); + let localFile = controller.dataItem.localFile; + if (!localFile.exists()) { return; } let dataTransfer = aEvent.dataTransfer; - dataTransfer.mozSetDataAt("application/x-moz-file", file, 0); + dataTransfer.mozSetDataAt("application/x-moz-file", localFile, 0); dataTransfer.effectAllowed = "copyMove"; - let spec = NetUtil.newURI(file).spec; - dataTransfer.setData("text/uri-list", spec); - dataTransfer.setData("text/plain", spec); + var url = Services.io.newFileURI(localFile).spec; + dataTransfer.setData("text/uri-list", url); + dataTransfer.setData("text/plain", url); dataTransfer.addElement(element); aEvent.stopPropagation(); - }, + } } //////////////////////////////////////////////////////////////////////////////// @@ -994,43 +1056,259 @@ const DownloadsView = { * Builds and updates a single item in the downloads list widget, responding to * changes in the download state and real-time data. * - * @param download - * Download object to be associated with the view item. + * @param aDataItem + * DownloadsDataItem to be associated with the view item. * @param aElement * XUL element corresponding to the single download item in the view. */ -function DownloadsViewItem(download, aElement) { - this.download = download; - this.element = aElement; - this.element._shell = this; - - this.element.setAttribute("type", "download"); - this.element.classList.add("download-state"); +function DownloadsViewItem(aDataItem, aElement) +{ + this._element = aElement; + this.dataItem = aDataItem; + + this.lastEstimatedSecondsLeft = Infinity; + + // Set the URI that represents the correct icon for the target file. As soon + // as bug 239948 comment 12 is handled, the "file" property will be always a + // file URL rather than a file name. At that point we should remove the "//" + // (double slash) from the icon URI specification (see test_moz_icon_uri.js). + this.image = "moz-icon://" + this.dataItem.file + "?size=32"; + + let s = DownloadsCommon.strings; + let [displayHost, fullHost] = + DownloadUtils.getURIHost(this.dataItem.referrer || this.dataItem.uri); + + let attributes = { + "type": "download", + "class": "download-state", + "id": "downloadsItem_" + this.dataItem.downloadGuid, + "downloadGuid": this.dataItem.downloadGuid, + "state": this.dataItem.state, + "progress": this.dataItem.inProgress ? this.dataItem.percentComplete : 100, + "displayName": this.dataItem.target, + "extendedDisplayName": s.statusSeparator(this.dataItem.target, displayHost), + "extendedDisplayNameTip": s.statusSeparator(this.dataItem.target, fullHost), + "image": this.image + }; + + for (let attributeName in attributes) { + this._element.setAttribute(attributeName, attributes[attributeName]); + } - this._updateState(); + // Initialize more complex attributes. + this._updateProgress(); + this._updateStatusLine(); + this.verifyTargetExists(); } DownloadsViewItem.prototype = { - __proto__: DownloadsViewUI.DownloadElementShell.prototype, + /** + * The DownloadDataItem associated with this view item. + */ + dataItem: null, /** * The XUL element corresponding to the associated richlistbox item. */ _element: null, - onStateChanged() { - this.element.setAttribute("image", this.image); - this.element.setAttribute("state", - DownloadsCommon.stateOfDownload(this.download)); + /** + * The inner XUL element for the progress bar, or null if not available. + */ + _progressElement: null, + + ////////////////////////////////////////////////////////////////////////////// + //// Callback functions from DownloadsData + + /** + * Called when the download state might have changed. Sometimes the state of + * the download might be the same as before, if the data layer received + * multiple events for the same download. + */ + onStateChange: function DVI_onStateChange(aOldState) + { + // If a download just finished successfully, it means that the target file + // now exists and we can extract its specific icon. To ensure that the icon + // is reloaded, we must change the URI used by the XUL image element, for + // example by adding a query parameter. Since this URI has a "moz-icon" + // scheme, this only works if we add one of the parameters explicitly + // supported by the nsIMozIconURI interface. + if (aOldState != Ci.nsIDownloadManager.DOWNLOAD_FINISHED && + aOldState != this.dataItem.state) { + this._element.setAttribute("image", this.image + "&state=normal"); + + // We assume the existence of the target of a download that just completed + // successfully, without checking the condition in the background. If the + // panel is already open, this will take effect immediately. If the panel + // is opened later, a new background existence check will be performed. + this._element.setAttribute("exists", "true"); + } + + // Update the user interface after switching states. + this._element.setAttribute("state", this.dataItem.state); + this._updateProgress(); + this._updateStatusLine(); }, - onChanged() { - // This cannot be placed within onStateChanged because - // when a download goes from hasBlockedData to !hasBlockedData - // it will still remain in the same state. - this.element.classList.toggle("temporary-block", - !!this.download.hasBlockedData); + /** + * Called when the download progress has changed. + */ + onProgressChange: function DVI_onProgressChange() { this._updateProgress(); + this._updateStatusLine(); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Functions for updating the user interface + + /** + * Updates the progress bar. + */ + _updateProgress: function DVI_updateProgress() { + if (this.dataItem.starting) { + // Before the download starts, the progress meter has its initial value. + this._element.setAttribute("progressmode", "normal"); + this._element.setAttribute("progress", "0"); + } else if (this.dataItem.state == Ci.nsIDownloadManager.DOWNLOAD_SCANNING || + this.dataItem.percentComplete == -1) { + // We might not know the progress of a running download, and we don't know + // the remaining time during the malware scanning phase. + this._element.setAttribute("progressmode", "undetermined"); + } else { + // This is a running download of which we know the progress. + this._element.setAttribute("progressmode", "normal"); + this._element.setAttribute("progress", this.dataItem.percentComplete); + } + + // Find the progress element as soon as the download binding is accessible. + if (!this._progressElement) { + this._progressElement = + document.getAnonymousElementByAttribute(this._element, "anonid", + "progressmeter"); + } + + // Dispatch the ValueChange event for accessibility, if possible. + if (this._progressElement) { + let event = document.createEvent("Events"); + event.initEvent("ValueChange", true, true); + this._progressElement.dispatchEvent(event); + } + }, + + /** + * Updates the main status line, including bytes transferred, bytes total, + * download rate, and time remaining. + */ + _updateStatusLine: function DVI_updateStatusLine() { + const nsIDM = Ci.nsIDownloadManager; + + let status = ""; + let statusTip = ""; + + if (this.dataItem.paused) { + let transfer = DownloadUtils.getTransferTotal(this.dataItem.currBytes, + this.dataItem.maxBytes); + + // We use the same XUL label to display both the state and the amount + // transferred, for example "Paused - 1.1 MB". + status = DownloadsCommon.strings.statusSeparatorBeforeNumber( + DownloadsCommon.strings.statePaused, + transfer); + } else if (this.dataItem.state == nsIDM.DOWNLOAD_DOWNLOADING) { + // We don't show the rate for each download in order to reduce clutter. + // The remaining time per download is likely enough information for the + // panel. + [status] = + DownloadUtils.getDownloadStatusNoRate(this.dataItem.currBytes, + this.dataItem.maxBytes, + this.dataItem.speed, + this.lastEstimatedSecondsLeft); + + // We are, however, OK with displaying the rate in the tooltip. + let newEstimatedSecondsLeft; + [statusTip, newEstimatedSecondsLeft] = + DownloadUtils.getDownloadStatus(this.dataItem.currBytes, + this.dataItem.maxBytes, + this.dataItem.speed, + this.lastEstimatedSecondsLeft); + this.lastEstimatedSecondsLeft = newEstimatedSecondsLeft; + } else if (this.dataItem.starting) { + status = DownloadsCommon.strings.stateStarting; + } else if (this.dataItem.state == nsIDM.DOWNLOAD_SCANNING) { + status = DownloadsCommon.strings.stateScanning; + } else if (!this.dataItem.inProgress) { + let stateLabel = function () { + let s = DownloadsCommon.strings; + switch (this.dataItem.state) { + case nsIDM.DOWNLOAD_FAILED: return s.stateFailed; + case nsIDM.DOWNLOAD_CANCELED: return s.stateCanceled; + case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: return s.stateBlockedParentalControls; + case nsIDM.DOWNLOAD_BLOCKED_POLICY: return s.stateBlockedPolicy; + case nsIDM.DOWNLOAD_DIRTY: return s.stateDirty; + case nsIDM.DOWNLOAD_FINISHED: return this._fileSizeText; + } + return null; + }.apply(this); + + let [displayHost, fullHost] = + DownloadUtils.getURIHost(this.dataItem.referrer || this.dataItem.uri); + + let end = new Date(this.dataItem.endTime); + let [displayDate, fullDate] = DownloadUtils.getReadableDates(end); + + // We use the same XUL label to display the state, the host name, and the + // end time, for example "Canceled - 222.net - 11:15" or "1.1 MB - + // website2.com - Yesterday". We show the full host and the complete date + // in the tooltip. + let firstPart = DownloadsCommon.strings.statusSeparator(stateLabel, + displayHost); + status = DownloadsCommon.strings.statusSeparator(firstPart, displayDate); + statusTip = DownloadsCommon.strings.statusSeparator(fullHost, fullDate); + } + + this._element.setAttribute("status", status); + this._element.setAttribute("statusTip", statusTip || status); + }, + + /** + * Localized string representing the total size of completed downloads, for + * example "1.5 MB" or "Unknown size". + */ + get _fileSizeText() + { + // Display the file size, but show "Unknown" for negative sizes. + let fileSize = this.dataItem.maxBytes; + if (fileSize < 0) { + return DownloadsCommon.strings.sizeUnknown; + } + let [size, unit] = DownloadUtils.convertByteUnits(fileSize); + return DownloadsCommon.strings.sizeWithUnits(size, unit); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Functions called by the panel + + /** + * Starts checking whether the target file of a finished download is still + * available on disk, and sets an attribute that controls how the item is + * presented visually. + * + * The existence check is executed on a background thread. + */ + verifyTargetExists: function DVI_verifyTargetExists() { + // We don't need to check if the download is not finished successfully. + if (!this.dataItem.openable) { + return; + } + + OS.File.exists(this.dataItem.localFile.path).then( + function DVI_RTE_onSuccess(aExists) { + if (aExists) { + this._element.setAttribute("exists", "true"); + } else { + this._element.removeAttribute("exists"); + } + }.bind(this), Cu.reportError); }, }; @@ -1046,18 +1324,21 @@ const DownloadsViewController = { ////////////////////////////////////////////////////////////////////////////// //// Initialization and termination - initialize() { + initialize: function DVC_initialize() + { window.controllers.insertControllerAt(0, this); }, - terminate() { + terminate: function DVC_terminate() + { window.controllers.removeController(this); }, ////////////////////////////////////////////////////////////////////////////// //// nsIController - supportsCommand(aCommand) { + supportsCommand: function DVC_supportsCommand(aCommand) + { // Firstly, determine if this is a command that we can handle. if (!(aCommand in this.commands) && !(aCommand in DownloadsViewItemController.prototype.commands)) { @@ -1073,7 +1354,8 @@ const DownloadsViewController = { return !!element; }, - isCommandEnabled(aCommand) { + isCommandEnabled: function DVC_isCommandEnabled(aCommand) + { // Handle commands that are not selection-specific. if (aCommand == "downloadsCmd_clearList") { return DownloadsCommon.getData(window).canRemoveFinished; @@ -1081,11 +1363,12 @@ const DownloadsViewController = { // Other commands are selection-specific. let element = DownloadsView.richListBox.selectedItem; - return element && DownloadsView.controllerForElement(element) - .isCommandEnabled(aCommand); + return element && + new DownloadsViewItemController(element).isCommandEnabled(aCommand); }, - doCommand(aCommand) { + doCommand: function DVC_doCommand(aCommand) + { // If this command is not selection-specific, execute it. if (aCommand in this.commands) { this.commands[aCommand].apply(this); @@ -1096,16 +1379,17 @@ const DownloadsViewController = { let element = DownloadsView.richListBox.selectedItem; if (element) { // The doCommand function also checks if the command is enabled. - DownloadsView.controllerForElement(element).doCommand(aCommand); + new DownloadsViewItemController(element).doCommand(aCommand); } }, - onEvent() {}, + onEvent: function () { }, ////////////////////////////////////////////////////////////////////////////// //// Other functions - updateCommands() { + updateCommands: function DVC_updateCommands() + { Object.keys(this.commands).forEach(goUpdateCommand); Object.keys(DownloadsViewItemController.prototype.commands) .forEach(goUpdateCommand); @@ -1119,7 +1403,8 @@ const DownloadsViewController = { * the currently selected item in the list. */ commands: { - downloadsCmd_clearList() { + downloadsCmd_clearList: function DVC_downloadsCmd_clearList() + { DownloadsCommon.getData(window).removeFinished(); } } @@ -1132,53 +1417,47 @@ const DownloadsViewController = { * Handles all the user interaction events, in particular the "commands", * related to a single item in the downloads list widgets. */ -function DownloadsViewItemController(download) { - this.download = download; +function DownloadsViewItemController(aElement) { + let downloadGuid = aElement.getAttribute("downloadGuid"); + this.dataItem = DownloadsCommon.getData(window).dataItems[downloadGuid]; } DownloadsViewItemController.prototype = { - isCommandEnabled(aCommand) { + ////////////////////////////////////////////////////////////////////////////// + //// Command dispatching + + /** + * The DownloadDataItem controlled by this object. + */ + dataItem: null, + + isCommandEnabled: function DVIC_isCommandEnabled(aCommand) + { switch (aCommand) { case "downloadsCmd_open": { - if (!this.download.succeeded) { - return false; - } - - let file = new FileUtils.File(this.download.target.path); - return file.exists(); + return this.dataItem.openable && this.dataItem.localFile.exists(); } case "downloadsCmd_show": { - let file = new FileUtils.File(this.download.target.path); - if (file.exists()) { - return true; - } - - if (!this.download.target.partFilePath) { - return false; - } - - let partFile = new FileUtils.File(this.download.target.partFilePath); - return partFile.exists(); + return this.dataItem.localFile.exists() || + this.dataItem.partFile.exists(); } case "downloadsCmd_pauseResume": - return this.download.hasPartialData && !this.download.error; + return this.dataItem.inProgress && this.dataItem.resumable; case "downloadsCmd_retry": - return this.download.canceled || this.download.error; + return this.dataItem.canRetry; case "downloadsCmd_openReferrer": - return !!this.download.source.referrer; + return !!this.dataItem.referrer; case "cmd_delete": case "downloadsCmd_cancel": case "downloadsCmd_copyLocation": case "downloadsCmd_doDefault": return true; - case "downloadsCmd_unblock": - case "downloadsCmd_confirmBlock": - return this.download.hasBlockedData; } return false; }, - doCommand(aCommand) { + doCommand: function DVIC_doCommand(aCommand) + { if (this.isCommandEnabled(aCommand)) { this.commands[aCommand].apply(this); } @@ -1193,34 +1472,23 @@ DownloadsViewItemController.prototype = { * In commands, the "this" identifier points to the controller item. */ commands: { - cmd_delete() { - DownloadsCommon.removeAndFinalizeDownload(this.download); - PlacesUtils.bhistory.removePage( - NetUtil.newURI(this.download.source.url)); - }, - - downloadsCmd_cancel() { - this.download.cancel().catch(() => {}); - this.download.removePartialData().catch(Cu.reportError); + cmd_delete: function DVIC_cmd_delete() + { + Downloads.getList(Downloads.ALL) + .then(list => list.remove(this.dataItem._download)) + .then(() => this.dataItem._download.finalize(true)) + .catch(Cu.reportError); + PlacesUtils.bhistory.removePage(NetUtil.newURI(this.dataItem.uri)); }, - downloadsCmd_unblock() { - DownloadsPanel.hidePanel(); - DownloadsCommon.confirmUnblockDownload(DownloadsCommon.BLOCK_VERDICT_MALWARE, - window).then((confirmed) => { - if (confirmed) { - return this.download.unblock(); - } - }).catch(Cu.reportError); + downloadsCmd_cancel: function DVIC_downloadsCmd_cancel() + { + this.dataItem.cancel(); }, - downloadsCmd_confirmBlock() { - this.download.confirmBlock().catch(Cu.reportError); - }, - - downloadsCmd_open() { - this.download.launch().catch(Cu.reportError); - + downloadsCmd_open: function DVIC_downloadsCmd_open() + { + this.dataItem.openLocalFile(window); // We explicitly close the panel here to give the user the feedback that // their click has been received, and we're handling the action. // Otherwise, we'd have to wait for the file-type handler to execute @@ -1229,9 +1497,9 @@ DownloadsViewItemController.prototype = { DownloadsPanel.hidePanel(); }, - downloadsCmd_show() { - let file = new FileUtils.File(this.download.target.path); - DownloadsCommon.showDownloadedFile(file); + downloadsCmd_show: function DVIC_downloadsCmd_show() + { + this.dataItem.showLocalFile(); // We explicitly close the panel here to give the user the feedback that // their click has been received, and we're handling the action. @@ -1241,34 +1509,35 @@ DownloadsViewItemController.prototype = { DownloadsPanel.hidePanel(); }, - downloadsCmd_pauseResume() { - if (this.download.stopped) { - this.download.start(); - } else { - this.download.cancel(); - } + downloadsCmd_pauseResume: function DVIC_downloadsCmd_pauseResume() + { + this.dataItem.togglePauseResume(); }, - downloadsCmd_retry() { - this.download.start().catch(() => {}); + downloadsCmd_retry: function DVIC_downloadsCmd_retry() + { + this.dataItem.retry(); }, - downloadsCmd_openReferrer() { - openURL(this.download.source.referrer); + downloadsCmd_openReferrer: function DVIC_downloadsCmd_openReferrer() + { + openURL(this.dataItem.referrer); }, - downloadsCmd_copyLocation() { + downloadsCmd_copyLocation: function DVIC_downloadsCmd_copyLocation() + { let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"] .getService(Ci.nsIClipboardHelper); - clipboard.copyString(this.download.source.url, document); + clipboard.copyString(this.dataItem.uri, document); }, - downloadsCmd_doDefault() { + downloadsCmd_doDefault: function DVIC_downloadsCmd_doDefault() + { const nsIDM = Ci.nsIDownloadManager; // Determine the default command for the current item. let defaultCommand = function () { - switch (DownloadsCommon.stateOfDownload(this.download)) { + switch (this.dataItem.state) { case nsIDM.DOWNLOAD_NOTSTARTED: return "downloadsCmd_cancel"; case nsIDM.DOWNLOAD_FINISHED: return "downloadsCmd_open"; case nsIDM.DOWNLOAD_FAILED: return "downloadsCmd_retry"; @@ -1282,11 +1551,10 @@ DownloadsViewItemController.prototype = { } return ""; }.apply(this); - if (defaultCommand && this.isCommandEnabled(defaultCommand)) { + if (defaultCommand && this.isCommandEnabled(defaultCommand)) this.doCommand(defaultCommand); - } - }, - }, + } + } }; @@ -1306,7 +1574,8 @@ const DownloadsSummary = { * @param aActive * Set to true to activate the summary. */ - set active(aActive) { + set active(aActive) + { if (aActive == this._active || !this._summaryNode) { return this._active; } @@ -1333,7 +1602,8 @@ const DownloadsSummary = { * @param aShowingProgress * True if we should show the progress bar. */ - set showingProgress(aShowingProgress) { + set showingProgress(aShowingProgress) + { if (aShowingProgress) { this._summaryNode.setAttribute("inprogress", "true"); } else { @@ -1350,7 +1620,8 @@ const DownloadsSummary = { * A value between 0 and 100 to represent the progress of the * summarized downloads. */ - set percentComplete(aValue) { + set percentComplete(aValue) + { if (this._progressNode) { this._progressNode.setAttribute("value", aValue); } @@ -1364,7 +1635,8 @@ const DownloadsSummary = { * A string representing the description of the summarized * downloads. */ - set description(aValue) { + set description(aValue) + { if (this._descriptionNode) { this._descriptionNode.setAttribute("value", aValue); this._descriptionNode.setAttribute("tooltiptext", aValue); @@ -1380,7 +1652,8 @@ const DownloadsSummary = { * A string representing the details of the summarized * downloads. */ - set details(aValue) { + set details(aValue) + { if (this._detailsNode) { this._detailsNode.setAttribute("value", aValue); this._detailsNode.setAttribute("tooltiptext", aValue); @@ -1391,7 +1664,8 @@ const DownloadsSummary = { /** * Focuses the root element of the summary. */ - focus() { + focus: function() + { if (this._summaryNode) { this._summaryNode.focus(); } @@ -1403,8 +1677,10 @@ const DownloadsSummary = { * @param aEvent * The keydown event being handled. */ - onKeyDown(aEvent) { + onKeyDown: function DS_onKeyDown(aEvent) + { if (aEvent.charCode == " ".charCodeAt(0) || + aEvent.keyCode == KeyEvent.DOM_VK_ENTER || aEvent.keyCode == KeyEvent.DOM_VK_RETURN) { DownloadsPanel.showDownloadsHistory(); } @@ -1416,14 +1692,16 @@ const DownloadsSummary = { * @param aEvent * The click event being handled. */ - onClick(aEvent) { + onClick: function DS_onClick(aEvent) + { DownloadsPanel.showDownloadsHistory(); }, /** * Element corresponding to the root of the downloads summary. */ - get _summaryNode() { + get _summaryNode() + { let node = document.getElementById("downloadsSummary"); if (!node) { return null; @@ -1435,7 +1713,8 @@ const DownloadsSummary = { /** * Element corresponding to the progress bar in the downloads summary. */ - get _progressNode() { + get _progressNode() + { let node = document.getElementById("downloadsSummaryProgress"); if (!node) { return null; @@ -1448,7 +1727,8 @@ const DownloadsSummary = { * Element corresponding to the main description of the downloads * summary. */ - get _descriptionNode() { + get _descriptionNode() + { let node = document.getElementById("downloadsSummaryDescription"); if (!node) { return null; @@ -1461,7 +1741,8 @@ const DownloadsSummary = { * Element corresponding to the secondary description of the downloads * summary. */ - get _detailsNode() { + get _detailsNode() + { let node = document.getElementById("downloadsSummaryDetails"); if (!node) { return null; @@ -1485,7 +1766,8 @@ const DownloadsFooter = { * is visible, focus it. If not, focus the "Show All Downloads" * button. */ - focus() { + focus: function DF_focus() + { if (this._showingSummary) { DownloadsSummary.focus(); } else { @@ -1499,7 +1781,8 @@ const DownloadsFooter = { * Sets whether or not the Downloads Summary should be displayed in the * footer. If not, the "Show All Downloads" button is shown instead. */ - set showingSummary(aValue) { + set showingSummary(aValue) + { if (this._footerNode) { if (aValue) { this._footerNode.setAttribute("showingsummary", "true"); @@ -1514,7 +1797,8 @@ const DownloadsFooter = { /** * Element corresponding to the footer of the downloads panel. */ - get _footerNode() { + get _footerNode() + { let node = document.getElementById("downloadsFooter"); if (!node) { return null; diff --git a/browser-omni/chrome/kmeleon/content/browser/downloads/downloadsOverlay.xul b/browser-omni/chrome/kmeleon/content/browser/downloads/downloadsOverlay.xul index 5771248f..e205754f 100644 --- a/browser-omni/chrome/kmeleon/content/browser/downloads/downloadsOverlay.xul +++ b/browser-omni/chrome/kmeleon/content/browser/downloads/downloadsOverlay.xul @@ -16,10 +16,6 @@ oncommand="goDoCommand('downloadsCmd_pauseResume')"/> - - + + + %browserDTD; + + %downloadsDTD; ]> - - - - - - - - - - - + + + + + + + + + + + + + +