diff options
Diffstat (limited to 'browser/components/downloads/DownloadsViewUI.jsm')
-rw-r--r-- | browser/components/downloads/DownloadsViewUI.jsm | 395 |
1 files changed, 395 insertions, 0 deletions
diff --git a/browser/components/downloads/DownloadsViewUI.jsm b/browser/components/downloads/DownloadsViewUI.jsm new file mode 100644 index 000000000..3b0e4c1b7 --- /dev/null +++ b/browser/components/downloads/DownloadsViewUI.jsm @@ -0,0 +1,395 @@ +/* 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, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This module is imported by code that uses the "download.xml" binding, and + * provides prototypes for objects that handle input and display information. + */ + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "DownloadsViewUI", +]; + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", + "resource://gre/modules/Downloads.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", + "resource://gre/modules/DownloadUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", + "resource:///modules/DownloadsCommon.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); + +this.DownloadsViewUI = { + /** + * Returns true if the given string is the name of a command that can be + * handled by the Downloads user interface, including standard commands. + */ + isCommandName(name) { + return name.startsWith("cmd_") || name.startsWith("downloadsCmd_"); + }, +}; + +/** + * A download element shell is responsible for handling the commands and the + * displayed data for a single element that uses the "download.xml" binding. + * + * The information to display is obtained through the associated Download object + * from the JavaScript API for downloads, and commands are executed using a + * combination of Download methods and DownloadsCommon.jsm helper functions. + * + * Specialized versions of this shell must be defined, and they are required to + * implement the "download" property or getter. Currently these objects are the + * HistoryDownloadElementShell and the DownloadsViewItem for the panel. The + * history view may use a HistoryDownload object in place of a Download object. + */ +this.DownloadsViewUI.DownloadElementShell = function () {} + +this.DownloadsViewUI.DownloadElementShell.prototype = { + /** + * The richlistitem for the download, initialized by the derived object. + */ + element: null, + + /** + * URI string for the file type icon displayed in the download element. + */ + get image() { + if (!this.download.target.path) { + // Old history downloads may not have a target path. + return "moz-icon://.unknown?size=32"; + } + + // When a download that was previously in progress finishes successfully, it + // means that the target file now exists and we can extract its specific + // icon, for example from a Windows executable. To ensure that the icon is + // reloaded, however, we must change the URI used by the XUL image element, + // for example by adding a query parameter. This only works if we add one of + // the parameters explicitly supported by the nsIMozIconURI interface. + return "moz-icon://" + this.download.target.path + "?size=32" + + (this.download.succeeded ? "&state=normal" : ""); + }, + + /** + * The user-facing label for the download. This is normally the leaf name of + * the download target file. In case this is a very old history download for + * which the target file is unknown, the download source URI is displayed. + */ + get displayName() { + if (!this.download.target.path) { + return this.download.source.url; + } + return OS.Path.basename(this.download.target.path); + }, + + /** + * The progress element for the download, or undefined in case the XBL binding + * has not been applied yet. + */ + get _progressElement() { + if (!this.__progressElement) { + // If the element is not available now, we will try again the next time. + this.__progressElement = + this.element.ownerDocument.getAnonymousElementByAttribute( + this.element, "anonid", + "progressmeter"); + } + return this.__progressElement; + }, + + /** + * Processes a major state change in the user interface, then proceeds with + * the normal progress update. This function is not called for every progress + * update in order to improve performance. + */ + _updateState() { + this.element.setAttribute("displayName", this.displayName); + this.element.setAttribute("image", this.image); + this.element.setAttribute("state", + DownloadsCommon.stateOfDownload(this.download)); + + if (!this.download.succeeded && this.download.error && + this.download.error.becauseBlockedByReputationCheck) { + this.element.setAttribute("verdict", + this.download.error.reputationCheckVerdict); + } else { + this.element.removeAttribute("verdict"); + } + + // Since state changed, reset the time left estimation. + this.lastEstimatedSecondsLeft = Infinity; + + this._updateProgress(); + }, + + /** + * Updates the elements that change regularly for in-progress downloads, + * namely the progress bar and the status line. + */ + _updateProgress() { + if (this.download.succeeded) { + // We only need to add or remove this attribute for succeeded downloads. + if (this.download.target.exists) { + this.element.setAttribute("exists", "true"); + } else { + this.element.removeAttribute("exists"); + } + } + + // When a block is confirmed, the removal of blocked data will not trigger a + // state change for the download, so this class must be updated here. + this.element.classList.toggle("temporary-block", + !!this.download.hasBlockedData); + + // The progress bar is only displayed for in-progress downloads. + if (this.download.hasProgress) { + this.element.setAttribute("progressmode", "normal"); + this.element.setAttribute("progress", this.download.progress); + } else { + this.element.setAttribute("progressmode", "undetermined"); + } + + if (this.download.stopped && this.download.canceled && + this.download.hasPartialData) { + this.element.setAttribute("progresspaused", "true"); + } else { + this.element.removeAttribute("progresspaused"); + } + + // Dispatch the ValueChange event for accessibility, if possible. + if (this._progressElement) { + let event = this.element.ownerDocument.createEvent("Events"); + event.initEvent("ValueChange", true, true); + this._progressElement.dispatchEvent(event); + } + + let status = this.statusTextAndTip; + this.element.setAttribute("status", status.text); + this.element.setAttribute("statusTip", status.tip); + }, + + lastEstimatedSecondsLeft: Infinity, + + /** + * Returns the text for the status line and the associated tooltip. These are + * returned by a single property because they are computed together. The + * result may be overridden by derived objects. + */ + get statusTextAndTip() { + return this.rawStatusTextAndTip; + }, + + /** + * Derived objects may call this to get the status text. + */ + get rawStatusTextAndTip() { + let s = DownloadsCommon.strings; + + let text = ""; + let tip = ""; + + if (!this.download.stopped) { + let totalBytes = this.download.hasProgress ? this.download.totalBytes + : -1; + // By default, extended status information including the individual + // download rate is displayed in the tooltip. The history view overrides + // the getter and displays the datails in the main area instead. + [text] = DownloadUtils.getDownloadStatusNoRate( + this.download.currentBytes, + totalBytes, + this.download.speed, + this.lastEstimatedSecondsLeft); + let newEstimatedSecondsLeft; + [tip, newEstimatedSecondsLeft] = DownloadUtils.getDownloadStatus( + this.download.currentBytes, + totalBytes, + this.download.speed, + this.lastEstimatedSecondsLeft); + this.lastEstimatedSecondsLeft = newEstimatedSecondsLeft; + } else if (this.download.canceled && this.download.hasPartialData) { + let totalBytes = this.download.hasProgress ? this.download.totalBytes + : -1; + let transfer = DownloadUtils.getTransferTotal(this.download.currentBytes, + totalBytes); + + // We use the same XUL label to display both the state and the amount + // transferred, for example "Paused - 1.1 MB". + text = s.statusSeparatorBeforeNumber(s.statePaused, transfer); + } else if (!this.download.succeeded && !this.download.canceled && + !this.download.error) { + text = s.stateStarting; + } else { + let stateLabel; + + if (this.download.succeeded) { + // For completed downloads, show the file size (e.g. "1.5 MB"). + if (this.download.target.size !== undefined) { + let [size, unit] = + DownloadUtils.convertByteUnits(this.download.target.size); + stateLabel = s.sizeWithUnits(size, unit); + } else { + // History downloads may not have a size defined. + stateLabel = s.sizeUnknown; + } + } else if (this.download.canceled) { + stateLabel = s.stateCanceled; + } else if (this.download.error.becauseBlockedByParentalControls) { + stateLabel = s.stateBlockedParentalControls; + } else if (this.download.error.becauseBlockedByReputationCheck) { + stateLabel = this.rawBlockedTitleAndDetails[0]; + } else { + stateLabel = s.stateFailed; + } + + let referrer = this.download.source.referrer || this.download.source.url; + let [displayHost, fullHost] = DownloadUtils.getURIHost(referrer); + + let date = new Date(this.download.endTime); + let [displayDate, fullDate] = DownloadUtils.getReadableDates(date); + + let firstPart = s.statusSeparator(stateLabel, displayHost); + text = s.statusSeparator(firstPart, displayDate); + tip = s.statusSeparator(fullHost, fullDate); + } + + return { text, tip: tip || text }; + }, + + /** + * Returns [title, [details1, details2]] for blocked downloads. + */ + get rawBlockedTitleAndDetails() { + let s = DownloadsCommon.strings; + if (!this.download.error || + !this.download.error.becauseBlockedByReputationCheck) { + return [null, null]; + } + switch (this.download.error.reputationCheckVerdict) { + case Downloads.Error.BLOCK_VERDICT_UNCOMMON: + return [s.blockedUncommon2, [s.unblockTypeUncommon2, s.unblockTip2]]; + case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED: + return [s.blockedPotentiallyUnwanted, + [s.unblockTypePotentiallyUnwanted2, s.unblockTip2]]; + case Downloads.Error.BLOCK_VERDICT_MALWARE: + return [s.blockedMalware, [s.unblockTypeMalware, s.unblockTip2]]; + } + throw new Error("Unexpected reputationCheckVerdict: " + + this.download.error.reputationCheckVerdict); + // return anyway to avoid a JS strict warning. + return [null, null]; + }, + + /** + * Shows the appropriate unblock dialog based on the verdict, and executes the + * action selected by the user in the dialog, which may involve unblocking, + * opening or removing the file. + * + * @param window + * The window to which the dialog should be anchored. + * @param dialogType + * Can be "unblock", "chooseUnblock", or "chooseOpen". + */ + confirmUnblock(window, dialogType) { + DownloadsCommon.confirmUnblockDownload({ + verdict: this.download.error.reputationCheckVerdict, + window, + dialogType, + }).then(action => { + if (action == "open") { + return this.unblockAndOpenDownload(); + } else if (action == "unblock") { + return this.download.unblock(); + } else if (action == "confirmBlock") { + return this.download.confirmBlock(); + } + }).catch(Cu.reportError); + }, + + /** + * Unblocks the downloaded file and opens it. + * + * @return A promise that's resolved after the file has been opened. + */ + unblockAndOpenDownload() { + return this.download.unblock().then(() => this.downloadsCmd_open()); + }, + + /** + * Returns the name of the default command to use for the current state of the + * download, when there is a double click or another default interaction. If + * there is no default command for the current state, returns an empty string. + * The commands are implemented as functions on this object or derived ones. + */ + get currentDefaultCommandName() { + switch (DownloadsCommon.stateOfDownload(this.download)) { + case Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED: + return "downloadsCmd_cancel"; + case Ci.nsIDownloadManager.DOWNLOAD_FAILED: + case Ci.nsIDownloadManager.DOWNLOAD_CANCELED: + return "downloadsCmd_retry"; + case Ci.nsIDownloadManager.DOWNLOAD_PAUSED: + return "downloadsCmd_pauseResume"; + case Ci.nsIDownloadManager.DOWNLOAD_FINISHED: + return "downloadsCmd_open"; + case Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL: + return "downloadsCmd_openReferrer"; + case Ci.nsIDownloadManager.DOWNLOAD_DIRTY: + return "downloadsCmd_showBlockedInfo"; + } + return ""; + }, + + /** + * Returns true if the specified command can be invoked on the current item. + * The commands are implemented as functions on this object or derived ones. + * + * @param aCommand + * Name of the command to check, for example "downloadsCmd_retry". + */ + isCommandEnabled(aCommand) { + switch (aCommand) { + case "downloadsCmd_retry": + return this.download.canceled || this.download.error; + case "downloadsCmd_pauseResume": + return this.download.hasPartialData && !this.download.error; + case "downloadsCmd_openReferrer": + return !!this.download.source.referrer; + case "downloadsCmd_confirmBlock": + case "downloadsCmd_chooseUnblock": + case "downloadsCmd_chooseOpen": + case "downloadsCmd_unblock": + case "downloadsCmd_unblockAndOpen": + return this.download.hasBlockedData; + } + return false; + }, + + downloadsCmd_cancel() { + // This is the correct way to avoid race conditions when cancelling. + this.download.cancel().catch(() => {}); + this.download.removePartialData().catch(Cu.reportError); + }, + + downloadsCmd_retry() { + // Errors when retrying are already reported as download failures. + this.download.start().catch(() => {}); + }, + + downloadsCmd_pauseResume() { + if (this.download.stopped) { + this.download.start(); + } else { + this.download.cancel(); + } + }, + + downloadsCmd_confirmBlock() { + this.download.confirmBlock().catch(Cu.reportError); + }, +}; |