diff options
Diffstat (limited to 'browser/components/downloads')
16 files changed, 0 insertions, 6754 deletions
diff --git a/browser/components/downloads/DownloadsCommon.jsm b/browser/components/downloads/DownloadsCommon.jsm deleted file mode 100644 index 90f14f2d8..000000000 --- a/browser/components/downloads/DownloadsCommon.jsm +++ /dev/null @@ -1,1559 +0,0 @@ -/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ -/* 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/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = [ - "DownloadsCommon", -]; - -/** - * Handles the Downloads panel shared methods and data access. - * - * This file includes the following constructors and global objects: - * - * DownloadsCommon - * This object is exposed directly to the consumers of this JavaScript module, - * and provides shared methods for all the instances of the user interface. - * - * DownloadsData - * Retrieves the list of past and completed downloads from the underlying - * Downloads API data, and provides asynchronous notifications allowing - * to build a consistent view of the available data. - * - * DownloadsIndicatorData - * This object registers itself with DownloadsData as a view, and transforms the - * notifications it receives into overall status data, that is then broadcast to - * the registered download status indicators. - */ - -//////////////////////////////////////////////////////////////////////////////// -//// Globals - -const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", - "resource://gre/modules/NetUtil.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", - "resource://gre/modules/PluralForm.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", - "resource://gre/modules/AppConstants.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Downloads", - "resource://gre/modules/Downloads.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper", - "resource://gre/modules/DownloadUIHelper.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", - "resource://gre/modules/DownloadUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", - "resource://gre/modules/FileUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "OS", - "resource://gre/modules/osfile.jsm") -XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", - "resource://gre/modules/PlacesUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", - "resource://gre/modules/PrivateBrowsingUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", - "resource:///modules/RecentWindow.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Promise", - "resource://gre/modules/Promise.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Task", - "resource://gre/modules/Task.jsm"); - -XPCOMUtils.defineLazyGetter(this, "DownloadsLogger", () => { - let { ConsoleAPI } = Cu.import("resource://gre/modules/Console.jsm", {}); - let consoleOptions = { - maxLogLevelPref: "browser.download.loglevel", - prefix: "Downloads" - }; - return new ConsoleAPI(consoleOptions); -}); - -const nsIDM = Ci.nsIDownloadManager; - -const kDownloadsStringBundleUrl = - "chrome://browser/locale/downloads/downloads.properties"; - -const kDownloadsStringsRequiringFormatting = { - sizeWithUnits: true, - shortTimeLeftSeconds: true, - shortTimeLeftMinutes: true, - shortTimeLeftHours: true, - shortTimeLeftDays: true, - statusSeparator: true, - statusSeparatorBeforeNumber: true, - fileExecutableSecurityWarning: true -}; - -const kDownloadsStringsRequiringPluralForm = { - otherDownloads2: true -}; - -const kPartialDownloadSuffix = ".part"; - -const kPrefBranch = Services.prefs.getBranch("browser.download."); - -var PrefObserver = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, - Ci.nsISupportsWeakReference]), - getPref(name) { - try { - switch (typeof this.prefs[name]) { - case "boolean": - return kPrefBranch.getBoolPref(name); - } - } catch (ex) { } - return this.prefs[name]; - }, - observe(aSubject, aTopic, aData) { - if (this.prefs.hasOwnProperty(aData)) { - delete this[aData]; - return this[aData] = this.getPref(aData); - } - }, - register(prefs) { - this.prefs = prefs; - kPrefBranch.addObserver("", this, true); - for (let key in prefs) { - let name = key; - XPCOMUtils.defineLazyGetter(this, name, function () { - return PrefObserver.getPref(name); - }); - } - }, -}; - -PrefObserver.register({ - // prefName: defaultValue - animateNotifications: true, - showPanelDropmarker: true, -}); - - -//////////////////////////////////////////////////////////////////////////////// -//// DownloadsCommon - -/** - * This object is exposed directly to the consumers of this JavaScript module, - * and provides shared methods for all the instances of the user interface. - */ -this.DownloadsCommon = { - ATTENTION_NONE: "", - ATTENTION_SUCCESS: "success", - ATTENTION_WARNING: "warning", - ATTENTION_SEVERE: "severe", - - /** - * Returns an object whose keys are the string names from the downloads string - * bundle, and whose values are either the translated strings or functions - * returning formatted strings. - */ - get strings() { - let strings = {}; - let sb = Services.strings.createBundle(kDownloadsStringBundleUrl); - let enumerator = sb.getSimpleEnumeration(); - while (enumerator.hasMoreElements()) { - let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); - let stringName = string.key; - if (stringName in kDownloadsStringsRequiringFormatting) { - strings[stringName] = function () { - // Convert "arguments" to a real array before calling into XPCOM. - return sb.formatStringFromName(stringName, - Array.slice(arguments, 0), - arguments.length); - }; - } else if (stringName in kDownloadsStringsRequiringPluralForm) { - strings[stringName] = function (aCount) { - // Convert "arguments" to a real array before calling into XPCOM. - let formattedString = sb.formatStringFromName(stringName, - Array.slice(arguments, 0), - arguments.length); - return PluralForm.get(aCount, formattedString); - }; - } else { - strings[stringName] = string.value; - } - } - delete this.strings; - return this.strings = strings; - }, - - /** - * Generates a very short string representing the given time left. - * - * @param aSeconds - * Value to be formatted. It represents the number of seconds, it must - * be positive but does not need to be an integer. - * - * @return Formatted string, for example "30s" or "2h". The returned value is - * maximum three characters long, at least in English. - */ - formatTimeLeft(aSeconds) { - // Decide what text to show for the time - let seconds = Math.round(aSeconds); - if (!seconds) { - return ""; - } else if (seconds <= 30) { - return DownloadsCommon.strings["shortTimeLeftSeconds"](seconds); - } - let minutes = Math.round(aSeconds / 60); - if (minutes < 60) { - return DownloadsCommon.strings["shortTimeLeftMinutes"](minutes); - } - let hours = Math.round(minutes / 60); - if (hours < 48) { // two days - return DownloadsCommon.strings["shortTimeLeftHours"](hours); - } - let days = Math.round(hours / 24); - return DownloadsCommon.strings["shortTimeLeftDays"](Math.min(days, 99)); - }, - - /** - * Indicates whether we should show visual notification on the indicator - * when a download event is triggered. - */ - get animateNotifications() { - return PrefObserver.animateNotifications; - }, - - /** - * Indicates whether we should show the dropmarker in the Downloads Panel. - */ - get showPanelDropmarker() { - return PrefObserver.showPanelDropmarker; - }, - - /** - * Get access to one of the DownloadsData or PrivateDownloadsData objects, - * depending on the privacy status of the window in question. - * - * @param aWindow - * The browser window which owns the download button. - */ - getData(aWindow) { - if (PrivateBrowsingUtils.isContentWindowPrivate(aWindow)) { - return PrivateDownloadsData; - } else { - return DownloadsData; - } - }, - - /** - * Initializes the Downloads back-end and starts receiving events for both the - * private and non-private downloads data objects. - */ - initializeAllDataLinks() { - DownloadsData.initializeDataLink(); - PrivateDownloadsData.initializeDataLink(); - }, - - /** - * Get access to one of the DownloadsIndicatorData or - * PrivateDownloadsIndicatorData objects, depending on the privacy status of - * the window in question. - */ - getIndicatorData(aWindow) { - if (PrivateBrowsingUtils.isContentWindowPrivate(aWindow)) { - return PrivateDownloadsIndicatorData; - } else { - return DownloadsIndicatorData; - } - }, - - /** - * Returns a reference to the DownloadsSummaryData singleton - creating one - * in the process if one hasn't been instantiated yet. - * - * @param aWindow - * The browser window which owns the download button. - * @param aNumToExclude - * The number of items on the top of the downloads list to exclude - * from the summary. - */ - getSummary(aWindow, aNumToExclude) { - if (PrivateBrowsingUtils.isContentWindowPrivate(aWindow)) { - if (this._privateSummary) { - return this._privateSummary; - } - return this._privateSummary = new DownloadsSummaryData(true, aNumToExclude); - } else { - if (this._summary) { - return this._summary; - } - return this._summary = new DownloadsSummaryData(false, aNumToExclude); - } - }, - _summary: null, - _privateSummary: null, - - /** - * Returns the legacy state integer value for the provided Download object. - */ - stateOfDownload(download) { - // Collapse state using the correct priority. - if (!download.stopped) { - return nsIDM.DOWNLOAD_DOWNLOADING; - } - if (download.succeeded) { - return nsIDM.DOWNLOAD_FINISHED; - } - if (download.error) { - if (download.error.becauseBlockedByParentalControls) { - return nsIDM.DOWNLOAD_BLOCKED_PARENTAL; - } - if (download.error.becauseBlockedByReputationCheck) { - return nsIDM.DOWNLOAD_DIRTY; - } - return nsIDM.DOWNLOAD_FAILED; - } - if (download.canceled) { - if (download.hasPartialData) { - return nsIDM.DOWNLOAD_PAUSED; - } - return nsIDM.DOWNLOAD_CANCELED; - } - return nsIDM.DOWNLOAD_NOTSTARTED; - }, - - /** - * Helper function required because the Downloads Panel and the Downloads View - * don't share the controller yet. - */ - removeAndFinalizeDownload(download) { - Downloads.getList(Downloads.ALL) - .then(list => list.remove(download)) - .then(() => download.finalize(true)) - .catch(Cu.reportError); - }, - - /** - * Given an iterable collection of Download objects, generates and returns - * statistics about that collection. - * - * @param downloads An iterable collection of Download objects. - * - * @return Object whose properties are the generated statistics. Currently, - * we return the following properties: - * - * numActive : The total number of downloads. - * numPaused : The total number of paused downloads. - * numDownloading : The total number of downloads being downloaded. - * totalSize : The total size of all downloads once completed. - * totalTransferred: The total amount of transferred data for these - * downloads. - * slowestSpeed : The slowest download rate. - * rawTimeLeft : The estimated time left for the downloads to - * complete. - * percentComplete : The percentage of bytes successfully downloaded. - */ - summarizeDownloads(downloads) { - let summary = { - numActive: 0, - numPaused: 0, - numDownloading: 0, - totalSize: 0, - totalTransferred: 0, - // slowestSpeed is Infinity so that we can use Math.min to - // find the slowest speed. We'll set this to 0 afterwards if - // it's still at Infinity by the time we're done iterating all - // download. - slowestSpeed: Infinity, - rawTimeLeft: -1, - percentComplete: -1 - } - - for (let download of downloads) { - summary.numActive++; - - if (!download.stopped) { - summary.numDownloading++; - if (download.hasProgress && download.speed > 0) { - let sizeLeft = download.totalBytes - download.currentBytes; - summary.rawTimeLeft = Math.max(summary.rawTimeLeft, - sizeLeft / download.speed); - summary.slowestSpeed = Math.min(summary.slowestSpeed, - download.speed); - } - } else if (download.canceled && download.hasPartialData) { - summary.numPaused++; - } - - // Only add to total values if we actually know the download size. - if (download.succeeded) { - summary.totalSize += download.target.size; - summary.totalTransferred += download.target.size; - } else if (download.hasProgress) { - summary.totalSize += download.totalBytes; - summary.totalTransferred += download.currentBytes; - } - } - - if (summary.totalSize != 0) { - summary.percentComplete = (summary.totalTransferred / - summary.totalSize) * 100; - } - - if (summary.slowestSpeed == Infinity) { - summary.slowestSpeed = 0; - } - - return summary; - }, - - /** - * If necessary, smooths the estimated number of seconds remaining for one - * or more downloads to complete. - * - * @param aSeconds - * Current raw estimate on number of seconds left for one or more - * downloads. This is a floating point value to help get sub-second - * accuracy for current and future estimates. - */ - smoothSeconds(aSeconds, aLastSeconds) { - // We apply an algorithm similar to the DownloadUtils.getTimeLeft function, - // though tailored to a single time estimation for all downloads. We never - // apply something if the new value is less than half the previous value. - let shouldApplySmoothing = aLastSeconds >= 0 && - aSeconds > aLastSeconds / 2; - if (shouldApplySmoothing) { - // Apply hysteresis to favor downward over upward swings. Trust only 30% - // of the new value if lower, and 10% if higher (exponential smoothing). - let diff = aSeconds - aLastSeconds; - aSeconds = aLastSeconds + (diff < 0 ? .3 : .1) * diff; - - // If the new time is similar, reuse something close to the last time - // left, but subtract a little to provide forward progress. - diff = aSeconds - aLastSeconds; - let diffPercent = diff / aLastSeconds * 100; - if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) { - aSeconds = aLastSeconds - (diff < 0 ? .4 : .2); - } - } - - // In the last few seconds of downloading, we are always subtracting and - // never adding to the time left. Ensure that we never fall below one - // second left until all downloads are actually finished. - return aLastSeconds = Math.max(aSeconds, 1); - }, - - /** - * Opens a downloaded file. - * - * @param aFile - * the downloaded file to be opened. - * @param aMimeInfo - * the mime type info object. May be null. - * @param aOwnerWindow - * the window with which this action is associated. - */ - openDownloadedFile(aFile, aMimeInfo, aOwnerWindow) { - if (!(aFile instanceof Ci.nsIFile)) { - throw new Error("aFile must be a nsIFile object"); - } - if (aMimeInfo && !(aMimeInfo instanceof Ci.nsIMIMEInfo)) { - throw new Error("Invalid value passed for aMimeInfo"); - } - if (!(aOwnerWindow instanceof Ci.nsIDOMWindow)) { - throw new Error("aOwnerWindow must be a dom-window object"); - } - - let isWindowsExe = AppConstants.platform == "win" && - aFile.leafName.toLowerCase().endsWith(".exe"); - - let promiseShouldLaunch; - // Don't prompt on Windows for .exe since there will be a native prompt. - if (aFile.isExecutable() && !isWindowsExe) { - // We get a prompter for the provided window here, even though anchoring - // to the most recently active window should work as well. - promiseShouldLaunch = - DownloadUIHelper.getPrompter(aOwnerWindow) - .confirmLaunchExecutable(aFile.path); - } else { - promiseShouldLaunch = Promise.resolve(true); - } - - promiseShouldLaunch.then(shouldLaunch => { - if (!shouldLaunch) { - return; - } - - // Actually open the file. - try { - if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) { - aMimeInfo.launchWithFile(aFile); - return; - } - } catch (ex) { } - - // If either we don't have the mime info, or the preferred action failed, - // attempt to launch the file directly. - try { - aFile.launch(); - } catch (ex) { - // If launch fails, try sending it through the system's external "file:" - // URL handler. - Cc["@mozilla.org/uriloader/external-protocol-service;1"] - .getService(Ci.nsIExternalProtocolService) - .loadUrl(NetUtil.newURI(aFile)); - } - }).then(null, Cu.reportError); - }, - - /** - * Show a downloaded file in the system file manager. - * - * @param aFile - * a downloaded file. - */ - showDownloadedFile(aFile) { - if (!(aFile instanceof Ci.nsIFile)) { - throw new Error("aFile must be a nsIFile object"); - } - try { - // Show the directory containing the file and select the file. - aFile.reveal(); - } catch (ex) { - // If reveal fails for some reason (e.g., it's not implemented on unix - // or the file doesn't exist), try using the parent if we have it. - let parent = aFile.parent; - if (parent) { - this.showDirectory(parent); - } - } - }, - - /** - * Show the specified folder in the system file manager. - * - * @param aDirectory - * a directory to be opened with system file manager. - */ - showDirectory(aDirectory) { - if (!(aDirectory instanceof Ci.nsIFile)) { - throw new Error("aDirectory must be a nsIFile object"); - } - try { - aDirectory.launch(); - } catch (ex) { - // If launch fails (probably because it's not implemented), let - // the OS handler try to open the directory. - Cc["@mozilla.org/uriloader/external-protocol-service;1"] - .getService(Ci.nsIExternalProtocolService) - .loadUrl(NetUtil.newURI(aDirectory)); - } - }, - - /** - * Displays an alert message box which asks the user if they want to - * unblock the downloaded file or not. - * - * @param options - * An object with the following properties: - * { - * verdict: - * The detailed reason why the download was blocked, according to - * the "Downloads.Error.BLOCK_VERDICT_" constants. If an unknown - * reason is specified, "Downloads.Error.BLOCK_VERDICT_MALWARE" is - * assumed. - * window: - * The window with which this action is associated. - * dialogType: - * String that determines which actions are available: - * - "unblock" to offer just "unblock". - * - "chooseUnblock" to offer "unblock" and "confirmBlock". - * - "chooseOpen" to offer "open" and "confirmBlock". - * } - * - * @return {Promise} - * @resolves String representing the action that should be executed: - * - "open" to allow the download and open the file. - * - "unblock" to allow the download without opening the file. - * - "confirmBlock" to delete the blocked data permanently. - * - "cancel" to do nothing and cancel the operation. - */ - confirmUnblockDownload: Task.async(function* ({ verdict, window, - dialogType }) { - let s = DownloadsCommon.strings; - - // All the dialogs have an action button and a cancel button, while only - // some of them have an additonal button to remove the file. The cancel - // button must always be the one at BUTTON_POS_1 because this is the value - // returned by confirmEx when using ESC or closing the dialog (bug 345067). - let title = s.unblockHeaderUnblock; - let firstButtonText = s.unblockButtonUnblock; - let firstButtonAction = "unblock"; - let buttonFlags = - (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) + - (Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1); - - switch (dialogType) { - case "unblock": - // Use only the unblock action. The default is to cancel. - buttonFlags += Ci.nsIPrompt.BUTTON_POS_1_DEFAULT; - break; - case "chooseUnblock": - // Use the unblock and remove file actions. The default is remove file. - buttonFlags += - (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) + - Ci.nsIPrompt.BUTTON_POS_2_DEFAULT; - break; - case "chooseOpen": - // Use the unblock and open file actions. The default is open file. - title = s.unblockHeaderOpen; - firstButtonText = s.unblockButtonOpen; - firstButtonAction = "open"; - buttonFlags += - (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) + - Ci.nsIPrompt.BUTTON_POS_0_DEFAULT; - break; - default: - Cu.reportError("Unexpected dialog type: " + dialogType); - return "cancel"; - } - - let message; - switch (verdict) { - case Downloads.Error.BLOCK_VERDICT_UNCOMMON: - message = s.unblockTypeUncommon2; - break; - case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED: - message = s.unblockTypePotentiallyUnwanted2; - break; - default: // Assume Downloads.Error.BLOCK_VERDICT_MALWARE - message = s.unblockTypeMalware; - break; - } - message += "\n\n" + s.unblockTip2; - - Services.ww.registerNotification(function onOpen(subj, topic) { - if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) { - // Make sure to listen for "DOMContentLoaded" because it is fired - // before the "load" event. - subj.addEventListener("DOMContentLoaded", function onLoad() { - subj.removeEventListener("DOMContentLoaded", onLoad); - if (subj.document.documentURI == - "chrome://global/content/commonDialog.xul") { - Services.ww.unregisterNotification(onOpen); - let dialog = subj.document.getElementById("commonDialog"); - if (dialog) { - // Change the dialog to use a warning icon. - dialog.classList.add("alert-dialog"); - } - } - }); - } - }); - - let rv = Services.prompt.confirmEx(window, title, message, buttonFlags, - firstButtonText, null, - s.unblockButtonConfirmBlock, null, {}); - return [firstButtonAction, "cancel", "confirmBlock"][rv]; - }), -}; - -XPCOMUtils.defineLazyGetter(this.DownloadsCommon, "log", () => { - return DownloadsLogger.log.bind(DownloadsLogger); -}); -XPCOMUtils.defineLazyGetter(this.DownloadsCommon, "error", () => { - return DownloadsLogger.error.bind(DownloadsLogger); -}); - -/** - * Returns true if we are executing on Windows Vista or a later version. - */ -XPCOMUtils.defineLazyGetter(DownloadsCommon, "isWinVistaOrHigher", function () { - let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; - if (os != "WINNT") { - return false; - } - let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); - return parseFloat(sysInfo.getProperty("version")) >= 6; -}); - -//////////////////////////////////////////////////////////////////////////////// -//// DownloadsData - -/** - * Retrieves the list of past and completed downloads from the underlying - * Download Manager data, and provides asynchronous notifications allowing to - * build a consistent view of the available data. - * - * This object responds to real-time changes in the underlying Download Manager - * data. For example, the deletion of one or more downloads is notified through - * the nsIObserver interface, while any state or progress change is notified - * through the nsIDownloadProgressListener interface. - * - * Note that using this object does not automatically start the Download Manager - * service. Consumers will see an empty list of downloads until the service is - * actually started. This is useful to display a neutral progress indicator in - * the main browser window until the autostart timeout elapses. - * - * Note that DownloadsData and PrivateDownloadsData are two equivalent singleton - * objects, one accessing non-private downloads, and the other accessing private - * ones. - */ -function DownloadsDataCtor(aPrivate) { - this._isPrivate = aPrivate; - - // Contains all the available Download objects and their integer state. - this.oldDownloadStates = new Map(); - - // Array of view objects that should be notified when the available download - // data changes. - this._views = []; -} - -DownloadsDataCtor.prototype = { - /** - * Starts receiving events for current downloads. - */ - initializeDataLink() { - if (!this._dataLinkInitialized) { - let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE - : Downloads.PUBLIC); - promiseList.then(list => list.addView(this)).then(null, Cu.reportError); - this._dataLinkInitialized = true; - } - }, - _dataLinkInitialized: false, - - /** - * Iterator for all the available Download objects. This is empty until the - * data has been loaded using the JavaScript API for downloads. - */ - get downloads() { - return this.oldDownloadStates.keys(); - }, - - /** - * True if there are finished downloads that can be removed from the list. - */ - get canRemoveFinished() { - for (let download of this.downloads) { - // Stopped, paused, and failed downloads with partial data are removed. - if (download.stopped && !(download.canceled && download.hasPartialData)) { - return true; - } - } - return false; - }, - - /** - * Asks the back-end to remove finished downloads from the list. - */ - removeFinished() { - let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE - : Downloads.PUBLIC); - promiseList.then(list => list.removeFinished()) - .then(null, Cu.reportError); - let indicatorData = this._isPrivate ? PrivateDownloadsIndicatorData - : DownloadsIndicatorData; - indicatorData.attention = DownloadsCommon.ATTENTION_NONE; - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Integration with the asynchronous Downloads back-end - - onDownloadAdded(download) { - // Download objects do not store the end time of downloads, as the Downloads - // API does not need to persist this information for all platforms. Once a - // download terminates on a Desktop browser, it becomes a history download, - // for which the end time is stored differently, as a Places annotation. - download.endTime = Date.now(); - - this.oldDownloadStates.set(download, - DownloadsCommon.stateOfDownload(download)); - - for (let view of this._views) { - view.onDownloadAdded(download, true); - } - }, - - onDownloadChanged(download) { - let oldState = this.oldDownloadStates.get(download); - let newState = DownloadsCommon.stateOfDownload(download); - this.oldDownloadStates.set(download, newState); - - if (oldState != newState) { - if (download.succeeded || - (download.canceled && !download.hasPartialData) || - download.error) { - // Store the end time that may be displayed by the views. - download.endTime = Date.now(); - - // This state transition code should actually be located in a Downloads - // API module (bug 941009). Moreover, the fact that state is stored as - // annotations should be ideally hidden behind methods of - // nsIDownloadHistory (bug 830415). - if (!this._isPrivate) { - try { - let downloadMetaData = { - state: DownloadsCommon.stateOfDownload(download), - endTime: download.endTime, - }; - if (download.succeeded) { - downloadMetaData.fileSize = download.target.size; - } - if (download.error && download.error.reputationCheckVerdict) { - downloadMetaData.reputationCheckVerdict = - download.error.reputationCheckVerdict; - } - - PlacesUtils.annotations.setPageAnnotation( - NetUtil.newURI(download.source.url), - "downloads/metaData", - JSON.stringify(downloadMetaData), 0, - PlacesUtils.annotations.EXPIRE_WITH_HISTORY); - } catch (ex) { - Cu.reportError(ex); - } - } - } - - for (let view of this._views) { - try { - view.onDownloadStateChanged(download); - } catch (ex) { - Cu.reportError(ex); - } - } - - if (download.succeeded || - (download.error && download.error.becauseBlocked)) { - this._notifyDownloadEvent("finish"); - } - } - - if (!download.newDownloadNotified) { - download.newDownloadNotified = true; - this._notifyDownloadEvent("start"); - } - - for (let view of this._views) { - view.onDownloadChanged(download); - } - }, - - onDownloadRemoved(download) { - this.oldDownloadStates.delete(download); - - for (let view of this._views) { - view.onDownloadRemoved(download); - } - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Registration of views - - /** - * Adds an object to be notified when the available download data changes. - * The specified object is initialized with the currently available downloads. - * - * @param aView - * DownloadsView object to be added. This reference must be passed to - * removeView before termination. - */ - addView(aView) { - this._views.push(aView); - this._updateView(aView); - }, - - /** - * Removes an object previously added using addView. - * - * @param aView - * DownloadsView object to be removed. - */ - removeView(aView) { - let index = this._views.indexOf(aView); - if (index != -1) { - this._views.splice(index, 1); - } - }, - - /** - * Ensures that the currently loaded data is added to the specified view. - * - * @param aView - * DownloadsView object to be initialized. - */ - _updateView(aView) { - // Indicate to the view that a batch loading operation is in progress. - aView.onDataLoadStarting(); - - // Sort backwards by start time, ensuring that the most recent - // downloads are added first regardless of their state. - let downloadsArray = [...this.downloads]; - downloadsArray.sort((a, b) => b.startTime - a.startTime); - downloadsArray.forEach(download => aView.onDownloadAdded(download, false)); - - // Notify the view that all data is available. - aView.onDataLoadCompleted(); - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Notifications sent to the most recent browser window only - - /** - * Set to true after the first download causes the downloads panel to be - * displayed. - */ - get panelHasShownBefore() { - try { - return Services.prefs.getBoolPref("browser.download.panel.shown"); - } catch (ex) { } - return false; - }, - - set panelHasShownBefore(aValue) { - Services.prefs.setBoolPref("browser.download.panel.shown", aValue); - return aValue; - }, - - /** - * Displays a new or finished download notification in the most recent browser - * window, if one is currently available with the required privacy type. - * - * @param aType - * Set to "start" for new downloads, "finish" for completed downloads. - */ - _notifyDownloadEvent(aType) { - DownloadsCommon.log("Attempting to notify that a new download has started or finished."); - - // Show the panel in the most recent browser window, if present. - let browserWin = RecentWindow.getMostRecentBrowserWindow({ private: this._isPrivate }); - if (!browserWin) { - return; - } - - if (this.panelHasShownBefore) { - // For new downloads after the first one, don't show the panel - // automatically, but provide a visible notification in the topmost - // browser window, if the status indicator is already visible. - DownloadsCommon.log("Showing new download notification."); - browserWin.DownloadsIndicatorView.showEventNotification(aType); - return; - } - this.panelHasShownBefore = true; - browserWin.DownloadsPanel.showPanel(); - } -}; - -XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsData", function() { - return new DownloadsDataCtor(true); -}); - -XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() { - return new DownloadsDataCtor(false); -}); - -//////////////////////////////////////////////////////////////////////////////// -//// DownloadsViewPrototype - -/** - * A prototype for an object that registers itself with DownloadsData as soon - * as a view is registered with it. - */ -const DownloadsViewPrototype = { - ////////////////////////////////////////////////////////////////////////////// - //// Registration of views - - /** - * Array of view objects that should be notified when the available status - * data changes. - * - * SUBCLASSES MUST OVERRIDE THIS PROPERTY. - */ - _views: null, - - /** - * Determines whether this view object is over the private or non-private - * downloads. - * - * SUBCLASSES MUST OVERRIDE THIS PROPERTY. - */ - _isPrivate: false, - - /** - * Adds an object to be notified when the available status data changes. - * The specified object is initialized with the currently available status. - * - * @param aView - * View object to be added. This reference must be - * passed to removeView before termination. - */ - addView(aView) { - // Start receiving events when the first of our views is registered. - if (this._views.length == 0) { - if (this._isPrivate) { - PrivateDownloadsData.addView(this); - } else { - DownloadsData.addView(this); - } - } - - this._views.push(aView); - this.refreshView(aView); - }, - - /** - * Updates the properties of an object previously added using addView. - * - * @param aView - * View object to be updated. - */ - refreshView(aView) { - // Update immediately even if we are still loading data asynchronously. - // Subclasses must provide these two functions! - this._refreshProperties(); - this._updateView(aView); - }, - - /** - * Removes an object previously added using addView. - * - * @param aView - * View object to be removed. - */ - removeView(aView) { - let index = this._views.indexOf(aView); - if (index != -1) { - this._views.splice(index, 1); - } - - // Stop receiving events when the last of our views is unregistered. - if (this._views.length == 0) { - if (this._isPrivate) { - PrivateDownloadsData.removeView(this); - } else { - DownloadsData.removeView(this); - } - } - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Callback functions from DownloadsData - - /** - * Indicates whether we are still loading downloads data asynchronously. - */ - _loading: false, - - /** - * Called before multiple downloads are about to be loaded. - */ - onDataLoadStarting() { - this._loading = true; - }, - - /** - * Called after data loading finished. - */ - onDataLoadCompleted() { - this._loading = false; - }, - - /** - * Called when a new download data item is available, either during the - * asynchronous data load or when a new download is started. - * - * @param download - * Download object that was just added. - * @param newest - * 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 - * started. When false, indicates that the item is the least recent - * with regard to the items that have been already added. The latter - * generally happens during the asynchronous data load. - * - * @note Subclasses should override this. - */ - onDownloadAdded(download, newest) { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; - }, - - /** - * Called when the overall state of a Download has changed. In particular, - * this is called only once when the download succeeds or is blocked - * permanently, and is never called if only the current progress changed. - * - * The onDownloadChanged notification will always be sent afterwards. - * - * @note Subclasses should override this. - */ - onDownloadStateChanged(download) { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; - }, - - /** - * Called every time any state property of a Download may have changed, - * including progress properties. - * - * Note that progress notification changes are throttled at the Downloads.jsm - * API level, and there is no throttling mechanism in the front-end. - * - * @note Subclasses should override this. - */ - onDownloadChanged(download) { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; - }, - - /** - * 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. - * - * @note Subclasses should override this. - */ - onDownloadRemoved(download) { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; - }, - - /** - * Private function used to refresh the internal properties being sent to - * each registered view. - * - * @note Subclasses should override this. - */ - _refreshProperties() { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; - }, - - /** - * Private function used to refresh an individual view. - * - * @note Subclasses should override this. - */ - _updateView() { - throw Components.results.NS_ERROR_NOT_IMPLEMENTED; - }, -}; - -//////////////////////////////////////////////////////////////////////////////// -//// DownloadsIndicatorData - -/** - * This object registers itself with DownloadsData as a view, and transforms the - * notifications it receives into overall status data, that is then broadcast to - * the registered download status indicators. - * - * Note that using this object does not automatically start the Download Manager - * service. Consumers will see an empty list of downloads until the service is - * actually started. This is useful to display a neutral progress indicator in - * the main browser window until the autostart timeout elapses. - */ -function DownloadsIndicatorDataCtor(aPrivate) { - this._isPrivate = aPrivate; - this._views = []; -} -DownloadsIndicatorDataCtor.prototype = { - __proto__: DownloadsViewPrototype, - - /** - * Removes an object previously added using addView. - * - * @param aView - * DownloadsIndicatorView object to be removed. - */ - removeView(aView) { - DownloadsViewPrototype.removeView.call(this, aView); - - if (this._views.length == 0) { - this._itemCount = 0; - } - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Callback functions from DownloadsData - - onDataLoadCompleted() { - DownloadsViewPrototype.onDataLoadCompleted.call(this); - this._updateViews(); - }, - - onDownloadAdded(download, newest) { - this._itemCount++; - this._updateViews(); - }, - - onDownloadStateChanged(download) { - if (!download.succeeded && download.error && download.error.reputationCheckVerdict) { - switch (download.error.reputationCheckVerdict) { - case Downloads.Error.BLOCK_VERDICT_UNCOMMON: // fall-through - case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED: - // Existing higher level attention indication trumps ATTENTION_WARNING. - if (this._attention != DownloadsCommon.ATTENTION_SEVERE) { - this.attention = DownloadsCommon.ATTENTION_WARNING; - } - break; - case Downloads.Error.BLOCK_VERDICT_MALWARE: - this.attention = DownloadsCommon.ATTENTION_SEVERE; - break; - default: - this.attention = DownloadsCommon.ATTENTION_SEVERE; - Cu.reportError("Unknown reputation verdict: " + - download.error.reputationCheckVerdict); - } - } else if (download.succeeded) { - // Existing higher level attention indication trumps ATTENTION_SUCCESS. - if (this._attention != DownloadsCommon.ATTENTION_SEVERE && - this._attention != DownloadsCommon.ATTENTION_WARNING) { - this.attention = DownloadsCommon.ATTENTION_SUCCESS; - } - } else if (download.error) { - // Existing higher level attention indication trumps ATTENTION_WARNING. - if (this._attention != DownloadsCommon.ATTENTION_SEVERE) { - this.attention = DownloadsCommon.ATTENTION_WARNING; - } - } - - // Since the state of a download changed, reset the estimated time left. - this._lastRawTimeLeft = -1; - this._lastTimeLeft = -1; - }, - - onDownloadChanged(download) { - this._updateViews(); - }, - - onDownloadRemoved(download) { - this._itemCount--; - this._updateViews(); - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Propagation of properties to our views - - // The following properties are updated by _refreshProperties and are then - // propagated to the views. See _refreshProperties for details. - _hasDownloads: false, - _counter: "", - _percentComplete: -1, - _paused: false, - - /** - * Indicates whether the download indicators should be highlighted. - */ - set attention(aValue) { - this._attention = aValue; - this._updateViews(); - return aValue; - }, - _attention: DownloadsCommon.ATTENTION_NONE, - - /** - * Indicates whether the user is interacting with downloads, thus the - * attention indication should not be shown even if requested. - */ - set attentionSuppressed(aValue) { - this._attentionSuppressed = aValue; - this._attention = DownloadsCommon.ATTENTION_NONE; - this._updateViews(); - return aValue; - }, - _attentionSuppressed: false, - - /** - * Computes aggregate values and propagates the changes to our views. - */ - _updateViews() { - // Do not update the status indicators during batch loads of download items. - if (this._loading) { - return; - } - - this._refreshProperties(); - this._views.forEach(this._updateView, this); - }, - - /** - * Updates the specified view with the current aggregate values. - * - * @param aView - * DownloadsIndicatorView object to be updated. - */ - _updateView(aView) { - aView.hasDownloads = this._hasDownloads; - aView.counter = this._counter; - aView.percentComplete = this._percentComplete; - aView.paused = this._paused; - aView.attention = this._attentionSuppressed ? DownloadsCommon.ATTENTION_NONE - : this._attention; - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Property updating based on current download status - - /** - * Number of download items that are available to be displayed. - */ - _itemCount: 0, - - /** - * Floating point value indicating the last number of seconds estimated until - * the longest download will finish. We need to store this value so that we - * don't continuously apply smoothing if the actual download state has not - * changed. This is set to -1 if the previous value is unknown. - */ - _lastRawTimeLeft: -1, - - /** - * Last number of seconds estimated until all in-progress downloads with a - * known size and speed will finish. This value is stored to allow smoothing - * in case of small variations. This is set to -1 if the previous value is - * unknown. - */ - _lastTimeLeft: -1, - - /** - * A generator function for the Download objects this summary is currently - * interested in. This generator is passed off to summarizeDownloads in order - * to generate statistics about the downloads we care about - in this case, - * it's all active downloads. - */ - * _activeDownloads() { - let downloads = this._isPrivate ? PrivateDownloadsData.downloads - : DownloadsData.downloads; - for (let download of downloads) { - if (!download.stopped || (download.canceled && download.hasPartialData)) { - yield download; - } - } - }, - - /** - * Computes aggregate values based on the current state of downloads. - */ - _refreshProperties() { - let summary = - DownloadsCommon.summarizeDownloads(this._activeDownloads()); - - // Determine if the indicator should be shown or get attention. - this._hasDownloads = (this._itemCount > 0); - - // If all downloads are paused, show the progress indicator as paused. - this._paused = summary.numActive > 0 && - summary.numActive == summary.numPaused; - - this._percentComplete = summary.percentComplete; - - // Display the estimated time left, if present. - if (summary.rawTimeLeft == -1) { - // There are no downloads with a known time left. - this._lastRawTimeLeft = -1; - this._lastTimeLeft = -1; - this._counter = ""; - } else { - // Compute the new time left only if state actually changed. - if (this._lastRawTimeLeft != summary.rawTimeLeft) { - this._lastRawTimeLeft = summary.rawTimeLeft; - this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft, - this._lastTimeLeft); - } - this._counter = DownloadsCommon.formatTimeLeft(this._lastTimeLeft); - } - } -}; - -XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsIndicatorData", function() { - return new DownloadsIndicatorDataCtor(true); -}); - -XPCOMUtils.defineLazyGetter(this, "DownloadsIndicatorData", function() { - return new DownloadsIndicatorDataCtor(false); -}); - -//////////////////////////////////////////////////////////////////////////////// -//// DownloadsSummaryData - -/** - * DownloadsSummaryData is a view for DownloadsData that produces a summary - * of all downloads after a certain exclusion point aNumToExclude. For example, - * if there were 5 downloads in progress, and a DownloadsSummaryData was - * constructed with aNumToExclude equal to 3, then that DownloadsSummaryData - * would produce a summary of the last 2 downloads. - * - * @param aIsPrivate - * True if the browser window which owns the download button is a private - * window. - * @param aNumToExclude - * The number of items to exclude from the summary, starting from the - * top of the list. - */ -function DownloadsSummaryData(aIsPrivate, aNumToExclude) { - this._numToExclude = aNumToExclude; - // Since we can have multiple instances of DownloadsSummaryData, we - // override these values from the prototype so that each instance can be - // completely separated from one another. - this._loading = false; - - this._downloads = []; - - // Floating point value indicating the last number of seconds estimated until - // the longest download will finish. We need to store this value so that we - // don't continuously apply smoothing if the actual download state has not - // changed. This is set to -1 if the previous value is unknown. - this._lastRawTimeLeft = -1; - - // Last number of seconds estimated until all in-progress downloads with a - // known size and speed will finish. This value is stored to allow smoothing - // in case of small variations. This is set to -1 if the previous value is - // unknown. - this._lastTimeLeft = -1; - - // The following properties are updated by _refreshProperties and are then - // propagated to the views. - this._showingProgress = false; - this._details = ""; - this._description = ""; - this._numActive = 0; - this._percentComplete = -1; - - this._isPrivate = aIsPrivate; - this._views = []; -} - -DownloadsSummaryData.prototype = { - __proto__: DownloadsViewPrototype, - - /** - * Removes an object previously added using addView. - * - * @param aView - * DownloadsSummary view to be removed. - */ - removeView(aView) { - DownloadsViewPrototype.removeView.call(this, aView); - - if (this._views.length == 0) { - // Clear out our collection of Download objects. If we ever have - // another view registered with us, this will get re-populated. - this._downloads = []; - } - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Callback functions from DownloadsData - see the documentation in - //// DownloadsViewPrototype for more information on what these functions - //// are used for. - - onDataLoadCompleted() { - DownloadsViewPrototype.onDataLoadCompleted.call(this); - this._updateViews(); - }, - - onDownloadAdded(download, newest) { - if (newest) { - this._downloads.unshift(download); - } else { - this._downloads.push(download); - } - - this._updateViews(); - }, - - onDownloadStateChanged() { - // Since the state of a download changed, reset the estimated time left. - this._lastRawTimeLeft = -1; - this._lastTimeLeft = -1; - }, - - onDownloadChanged() { - this._updateViews(); - }, - - onDownloadRemoved(download) { - let itemIndex = this._downloads.indexOf(download); - this._downloads.splice(itemIndex, 1); - this._updateViews(); - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Propagation of properties to our views - - /** - * Computes aggregate values and propagates the changes to our views. - */ - _updateViews() { - // Do not update the status indicators during batch loads of download items. - if (this._loading) { - return; - } - - this._refreshProperties(); - this._views.forEach(this._updateView, this); - }, - - /** - * Updates the specified view with the current aggregate values. - * - * @param aView - * DownloadsIndicatorView object to be updated. - */ - _updateView(aView) { - aView.showingProgress = this._showingProgress; - aView.percentComplete = this._percentComplete; - aView.description = this._description; - aView.details = this._details; - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Property updating based on current download status - - /** - * A generator function for the Download objects this summary is currently - * interested in. This generator is passed off to summarizeDownloads in order - * to generate statistics about the downloads we care about - in this case, - * it's the downloads in this._downloads after the first few to exclude, - * which was set when constructing this DownloadsSummaryData instance. - */ - * _downloadsForSummary() { - if (this._downloads.length > 0) { - for (let i = this._numToExclude; i < this._downloads.length; ++i) { - yield this._downloads[i]; - } - } - }, - - /** - * Computes aggregate values based on the current state of downloads. - */ - _refreshProperties() { - // Pre-load summary with default values. - let summary = - DownloadsCommon.summarizeDownloads(this._downloadsForSummary()); - - this._description = DownloadsCommon.strings - .otherDownloads2(summary.numActive); - this._percentComplete = summary.percentComplete; - - // If all downloads are paused, show the progress indicator as paused. - this._showingProgress = summary.numDownloading > 0 || - summary.numPaused > 0; - - // Display the estimated time left, if present. - if (summary.rawTimeLeft == -1) { - // There are no downloads with a known time left. - this._lastRawTimeLeft = -1; - this._lastTimeLeft = -1; - this._details = ""; - } else { - // Compute the new time left only if state actually changed. - if (this._lastRawTimeLeft != summary.rawTimeLeft) { - this._lastRawTimeLeft = summary.rawTimeLeft; - this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft, - this._lastTimeLeft); - } - [this._details] = DownloadUtils.getDownloadStatusNoRate( - summary.totalTransferred, summary.totalSize, summary.slowestSpeed, - this._lastTimeLeft); - } - }, -} diff --git a/browser/components/downloads/DownloadsTaskbar.jsm b/browser/components/downloads/DownloadsTaskbar.jsm deleted file mode 100644 index cf915abb5..000000000 --- a/browser/components/downloads/DownloadsTaskbar.jsm +++ /dev/null @@ -1,177 +0,0 @@ -/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ -/* 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/. */ - -/** - * Handles the download progress indicator in the taskbar. - */ - -"use strict"; - -this.EXPORTED_SYMBOLS = [ - "DownloadsTaskbar", -]; - -//////////////////////////////////////////////////////////////////////////////// -//// Globals - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; -const Cr = Components.results; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "Downloads", - "resource://gre/modules/Downloads.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", - "resource:///modules/RecentWindow.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); - -XPCOMUtils.defineLazyGetter(this, "gWinTaskbar", function () { - if (!("@mozilla.org/windows-taskbar;1" in Cc)) { - return null; - } - let winTaskbar = Cc["@mozilla.org/windows-taskbar;1"] - .getService(Ci.nsIWinTaskbar); - return winTaskbar.available && winTaskbar; -}); - -XPCOMUtils.defineLazyGetter(this, "gMacTaskbarProgress", function () { - return ("@mozilla.org/widget/macdocksupport;1" in Cc) && - Cc["@mozilla.org/widget/macdocksupport;1"] - .getService(Ci.nsITaskbarProgress); -}); - -//////////////////////////////////////////////////////////////////////////////// -//// DownloadsTaskbar - -/** - * Handles the download progress indicator in the taskbar. - */ -this.DownloadsTaskbar = { - /** - * Underlying DownloadSummary providing the aggregate download information, or - * null if the indicator has never been initialized. - */ - _summary: null, - - /** - * nsITaskbarProgress object to which download information is dispatched. - * This can be null if the indicator has never been initialized or if the - * indicator is currently hidden on Windows. - */ - _taskbarProgress: null, - - /** - * This method is called after a new browser window is opened, and ensures - * that the download progress indicator is displayed in the taskbar. - * - * On Windows, the indicator is attached to the first browser window that - * calls this method. When the window is closed, the indicator is moved to - * another browser window, if available, in no particular order. When there - * are no browser windows visible, the indicator is hidden. - * - * On Mac OS X, the indicator is initialized globally when this method is - * called for the first time. Subsequent calls have no effect. - * - * @param aBrowserWindow - * nsIDOMWindow object of the newly opened browser window to which the - * indicator may be attached. - */ - registerIndicator(aBrowserWindow) { - if (!this._taskbarProgress) { - if (gMacTaskbarProgress) { - // On Mac OS X, we have to register the global indicator only once. - this._taskbarProgress = gMacTaskbarProgress; - // Free the XPCOM reference on shutdown, to prevent detecting a leak. - Services.obs.addObserver(() => { - this._taskbarProgress = null; - gMacTaskbarProgress = null; - }, "quit-application-granted", false); - } else if (gWinTaskbar) { - // On Windows, the indicator is currently hidden because we have no - // previous browser window, thus we should attach the indicator now. - this._attachIndicator(aBrowserWindow); - } else { - // The taskbar indicator is not available on this platform. - return; - } - } - - // Ensure that the DownloadSummary object will be created asynchronously. - if (!this._summary) { - Downloads.getSummary(Downloads.ALL).then(summary => { - // In case the method is re-entered, we simply ignore redundant - // invocations of the callback, instead of keeping separate state. - if (this._summary) { - return; - } - this._summary = summary; - return this._summary.addView(this); - }).then(null, Cu.reportError); - } - }, - - /** - * On Windows, attaches the taskbar indicator to the specified browser window. - */ - _attachIndicator(aWindow) { - // Activate the indicator on the specified window. - let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner - .QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIXULWindow).docShell; - this._taskbarProgress = gWinTaskbar.getTaskbarProgress(docShell); - - // If the DownloadSummary object has already been created, we should update - // the state of the new indicator, otherwise it will be updated as soon as - // the DownloadSummary view is registered. - if (this._summary) { - this.onSummaryChanged(); - } - - aWindow.addEventListener("unload", () => { - // Locate another browser window, excluding the one being closed. - let browserWindow = RecentWindow.getMostRecentBrowserWindow(); - if (browserWindow) { - // Move the progress indicator to the other browser window. - this._attachIndicator(browserWindow); - } else { - // The last browser window has been closed. We remove the reference to - // the taskbar progress object so that the indicator will be registered - // again on the next browser window that is opened. - this._taskbarProgress = null; - } - }, false); - }, - - ////////////////////////////////////////////////////////////////////////////// - //// DownloadSummary view - - onSummaryChanged() { - // If the last browser window has been closed, we have no indicator any more. - if (!this._taskbarProgress) { - return; - } - - if (this._summary.allHaveStopped || this._summary.progressTotalBytes == 0) { - this._taskbarProgress.setProgressState( - Ci.nsITaskbarProgress.STATE_NO_PROGRESS, 0, 0); - } else { - // For a brief moment before completion, some download components may - // report more transferred bytes than the total number of bytes. Thus, - // ensure that we never break the expectations of the progress indicator. - let progressCurrentBytes = Math.min(this._summary.progressTotalBytes, - this._summary.progressCurrentBytes); - this._taskbarProgress.setProgressState( - Ci.nsITaskbarProgress.STATE_NORMAL, - progressCurrentBytes, - this._summary.progressTotalBytes); - } - }, -}; diff --git a/browser/components/downloads/DownloadsViewUI.jsm b/browser/components/downloads/DownloadsViewUI.jsm deleted file mode 100644 index 3b0e4c1b7..000000000 --- a/browser/components/downloads/DownloadsViewUI.jsm +++ /dev/null @@ -1,395 +0,0 @@ -/* 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); - }, -}; diff --git a/browser/components/downloads/content/allDownloadsViewOverlay.js b/browser/components/downloads/content/allDownloadsViewOverlay.js deleted file mode 100644 index 58078cd08..000000000 --- a/browser/components/downloads/content/allDownloadsViewOverlay.js +++ /dev/null @@ -1,1439 +0,0 @@ -/* 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/. */ - -var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -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"); -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"); - -const nsIDM = Ci.nsIDownloadManager; - -const DESTINATION_FILE_URI_ANNO = "downloads/destinationFileURI"; -const DOWNLOAD_META_DATA_ANNO = "downloads/metaData"; - -/** - * Represents a download from the browser history. It implements part of the - * interface of the Download object. - * - * @param aPlacesNode - * The Places node from which the history download should be initialized. - */ -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; -} - -HistoryDownload.prototype = { - /** - * Pushes information from Places metadata into this object. - */ - 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; - } - - if ("state" in metaData) { - this.succeeded = metaData.state == nsIDM.DOWNLOAD_FINISHED; - this.canceled = metaData.state == nsIDM.DOWNLOAD_CANCELED || - metaData.state == nsIDM.DOWNLOAD_PAUSED; - this.endTime = metaData.endTime; - - // Recreate partial error information from the state saved in history. - if (metaData.state == nsIDM.DOWNLOAD_FAILED) { - this.error = { message: "History download failed." }; - } else if (metaData.state == nsIDM.DOWNLOAD_BLOCKED_PARENTAL) { - this.error = { becauseBlockedByParentalControls: true }; - } else if (metaData.state == nsIDM.DOWNLOAD_DIRTY) { - this.error = { - becauseBlockedByReputationCheck: true, - reputationCheckVerdict: metaData.reputationCheckVerdict || "", - }; - } else { - this.error = null; - } - - // 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.target.exists = false; - this.target.size = undefined; - } - }, - - /** - * History downloads are never in progress. - */ - stopped: true, - - /** - * No percentage indication is shown for history downloads. - */ - hasProgress: false, - - /** - * 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 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; - - // 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); - - return Promise.resolve(); - }, - - /** - * 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; - } - }), -}; - -/** - * 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; - - this.element.classList.add("download"); - this.element.classList.add("download-state"); - - if (aSessionDownload) { - this.sessionDownload = aSessionDownload; - } - if (aHistoryDownload) { - this.historyDownload = aHistoryDownload; - } -} - -HistoryDownloadElementShell.prototype = { - __proto__: DownloadsViewUI.DownloadElementShell.prototype, - - /** - * 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(); - } - }, - get active() { - return !!this._active; - }, - - /** - * Overrides the base getter to return the Download or HistoryDownload object - * for displaying information and executing commands in the user interface. - */ - get download() { - return this._sessionDownload || this._historyDownload; - }, - - _sessionDownload: null, - get sessionDownload() { - return 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"); - } - - this._sessionDownload = aValue; - - this.ensureActive(); - this._updateUI(); - } - return aValue; - }, - - _historyDownload: null, - get historyDownload() { - return 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"); - } - - this._historyDownload = aValue; - - // 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(); - } - } - return aValue; - }, - - _updateUI() { - // There is nothing to do if the item has always been invisible. - if (!this.active) { - return; - } - - // Since the state changed, we may need to check the target file again. - this._targetFileChecked = false; - - this._updateState(); - }, - - get statusTextAndTip() { - let status = this.rawStatusTextAndTip; - - // 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; - } - status.tip = ""; - - return status; - }, - - onStateChanged() { - this._updateState(); - - if (this.element.selected) { - goUpdateDownloadCommands(); - } else { - // If a state change occurs in an item that is not currently selected, - // this is the only command that may be affected. - 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(); - }, - - isCommandEnabled(aCommand) { - // The only valid command for inactive elements is 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": - // 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; - } - } - - // This property is false if the download did not succeed. - return this.download.target.exists; - case "cmd_delete": - // We don't want in-progress downloads to be removed accidentally. - return this.download.stopped; - case "downloadsCmd_cancel": - return !!this._sessionDownload; - } - return DownloadsViewUI.DownloadElementShell.prototype - .isCommandEnabled.call(this, aCommand); - }, - - doCommand(aCommand) { - if (DownloadsViewUI.isCommandName(aCommand)) { - this[aCommand](); - } - }, - - downloadsCmd_open() { - let file = new FileUtils.File(this.download.target.path); - DownloadsCommon.openDownloadedFile(file, null, window); - }, - - downloadsCmd_show() { - let file = new FileUtils.File(this.download.target.path); - DownloadsCommon.showDownloadedFile(file); - }, - - downloadsCmd_openReferrer() { - openURL(this.download.source.referrer); - }, - - cmd_delete() { - if (this._sessionDownload) { - DownloadsCommon.removeAndFinalizeDownload(this.download); - } - if (this._historyDownload) { - let uri = NetUtil.newURI(this.download.source.url); - PlacesUtils.bhistory.removePage(uri); - } - }, - - downloadsCmd_unblock() { - this.confirmUnblock(window, "unblock"); - }, - - downloadsCmd_chooseUnblock() { - this.confirmUnblock(window, "chooseUnblock"); - }, - - downloadsCmd_chooseOpen() { - this.confirmUnblock(window, "chooseOpen"); - }, - - // 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) { - return true; - } - aTerm = aTerm.toLowerCase(); - return this.displayName.toLowerCase().includes(aTerm) || - this.download.source.url.toLowerCase().includes(aTerm); - }, - - // Handles return keypress on the element (the keypress listener is - // set in the DownloadsPlacesView object). - doDefaultCommand() { - let command = this.currentDefaultCommandName; - 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. - */ - 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(); - }), -}; - -/** - * Relays commands from the download.xml binding to the selected items. - */ -const DownloadsView = { - onDownloadCommand(event, command) { - goDoCommand(command); - }, - - onDownloadClick() {}, -}; - -/** - * A Downloads Places View is a places view designed to show a places query - * for history downloads alongside the session downloads. - * - * As we don't use the places controller, some methods implemented by other - * places views are not implemented by this view. - * - * A richlistitem in this view can represent either a past download or a session - * download, or both. Session downloads are shown first in the view, and as long - * as they exist they "collapses" their history "counterpart" (So we don't show two - * items for every download). - */ -function DownloadsPlacesView(aRichListBox, aActive = true) { - this._richlistbox = aRichListBox; - this._richlistbox._placesView = this; - window.controllers.insertControllerAt(0, this); - - // Map download URLs to download element shells regardless of their type - this._downloadElementsShellsForURI = new Map(); - - // Map download data items to their element shells. - this._viewItemsForDownloads = new WeakMap(); - - // Points to the last session download element. We keep track of this - // in order to keep all session downloads above past downloads. - this._lastSessionDownloadElement = null; - - this._searchTerm = ""; - - this._active = aActive; - - // Register as a downloads view. The places data will be initialized by - // the places setter. - this._initiallySelectedElement = null; - this._downloadsData = DownloadsCommon.getData(window.opener || window); - this._downloadsData.addView(this); - - // Get the Download button out of the attention state since we're about to - // view all downloads. - DownloadsCommon.getIndicatorData(window).attention = DownloadsCommon.ATTENTION_NONE; - - // Make sure to unregister the view if the window is closed. - window.addEventListener("unload", () => { - window.controllers.removeController(this); - this._downloadsData.removeView(this); - this.result = null; - }, true); - // Resizing the window may change items visibility. - window.addEventListener("resize", () => { - this._ensureVisibleElementsAreActive(); - }, true); -} - -DownloadsPlacesView.prototype = { - get associatedElement() { - return this._richlistbox; - }, - - get active() { - return this._active; - }, - set active(val) { - this._active = val; - if (this._active) - this._ensureVisibleElementsAreActive(); - 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) {} - } - - // 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); - } - 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; - }, - - /** - * Given a data item for a session download, or a places node for a past - * download, updates the view as necessary. - * 1. If the given data is a places node, we check whether there are any - * elements for the same download url. If there are, then we just reset - * their places node. Otherwise we add a new download element. - * 2. If the given data is a data item, we first check if there's a history - * download in the list that is not associated with a data item. If we - * found one, we use it for the data item as well and reposition it - * 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 [optional] aPlacesNode - * The Places node for a history download, or null for session downloads. - * @param [optional] aNewest - * @see onDownloadAdded. 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), - * a document fragment may be passed to which the new elements would - * 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; - 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 - // need to create one. - let shouldCreateShell = shellsForURI.size == 0; - - // However, if we do have shells for this download uri, there are - // few options: - // 1) There's only one shell and it's for a history download (it has - // no data item). In this case, we update this shell and move it - // if necessary - // 2) There are multiple shells, indicating multiple downloads for - // the same download uri are running. In this case we create - // another shell for the download (so we have one shell for each data - // item). - // - // Note: If a cancelled session download is already in the list, and the - // download is retried, onDownloadAdded 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)) { - // 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) { - shouldCreateShell = false; - shell.sessionDownload = sessionDownload; - newOrUpdatedShell = shell; - this._viewItemsForDownloads.set(sessionDownload, 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; - 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. - 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 (newOrUpdatedShell) { - if (aNewest) { - this._richlistbox.insertBefore(newOrUpdatedShell.element, - this._richlistbox.firstChild); - if (!this._lastSessionDownloadElement) { - this._lastSessionDownloadElement = newOrUpdatedShell.element; - } - // Some operations like retrying an history download move an element to - // 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) { - let before = this._lastSessionDownloadElement ? - this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild; - this._richlistbox.insertBefore(newOrUpdatedShell.element, before); - this._lastSessionDownloadElement = newOrUpdatedShell.element; - } else { - let appendTo = aDocumentFragment || this._richlistbox; - appendTo.appendChild(newOrUpdatedShell.element); - } - - if (this.searchTerm) { - newOrUpdatedShell.element.hidden = - !newOrUpdatedShell.element._shell.matchesSearchTerm(this.searchTerm); - } - } - - // If aDocumentFragment is defined this is a batch change, so it's up to - // the caller to append the fragment and activate the visible shells. - if (!aDocumentFragment) { - this._ensureVisibleElementsAreActive(); - goUpdateCommand("downloadsCmd_clearDownloads"); - } - }, - - _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) && - this._richlistbox.selectedItems && - this._richlistbox.selectedItems.length == 1 && - this._richlistbox.selectedItems[0] == aElement) { - this._richlistbox.selectItem(aElement.nextSibling || - aElement.previousSibling); - } - - if (this._lastSessionDownloadElement == aElement) { - this._lastSessionDownloadElement = aElement.previousSibling; - } - - this._richlistbox.removeItemFromSelection(aElement); - this._richlistbox.removeChild(aElement); - this._ensureVisibleElementsAreActive(); - goUpdateCommand("downloadsCmd_clearDownloads"); - }, - - _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 { - this._removeElement(shell.element); - shellsForURI.delete(shell); - if (shellsForURI.size == 0) - this._downloadElementsShellsForURI.delete(downloadURI); - } - } - } - }, - - _removeSessionDownloadFromView(download) { - let shells = this._downloadElementsShellsForURI - .get(download.source.url); - 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)) { - 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) { - 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; - // Move it below the session-download items; - if (this._lastSessionDownloadElement == shell.element) { - this._lastSessionDownloadElement = shell.element.previousSibling; - } else { - let before = this._lastSessionDownloadElement ? - this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild; - this._richlistbox.insertBefore(shell.element, before); - } - } - }, - - _ensureVisibleElementsAreActive() { - if (!this.active || this._ensureVisibleTimer || - !this._richlistbox.firstChild) { - return; - } - - this._ensureVisibleTimer = setTimeout(() => { - delete this._ensureVisibleTimer; - if (!this._richlistbox.firstChild) { - return; - } - - let rlbRect = this._richlistbox.getBoundingClientRect(); - let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - let nodes = winUtils.nodesFromRect(rlbRect.left, rlbRect.top, - 0, rlbRect.width, rlbRect.height, 0, - true, false); - // nodesFromRect returns nodes in z-index order, and for the same z-index - // sorts them in inverted DOM order, thus starting from the one that would - // be on top. - let firstVisibleNode, lastVisibleNode; - for (let node of nodes) { - if (node.localName === "richlistitem" && node._shell) { - node._shell.ensureActive(); - // The first visible node is the last match. - firstVisibleNode = node; - // While the last visible node is the first match. - if (!lastVisibleNode) { - lastVisibleNode = node; - } - } - } - - // Also activate the first invisible nodes in both boundaries (that is, - // above and below the visible area) to ensure proper keyboard navigation - // in both directions. - let nodeBelowVisibleArea = lastVisibleNode && lastVisibleNode.nextSibling; - if (nodeBelowVisibleArea && nodeBelowVisibleArea._shell) { - nodeBelowVisibleArea._shell.ensureActive(); - } - - let nodeAboveVisibleArea = firstVisibleNode && - firstVisibleNode.previousSibling; - if (nodeAboveVisibleArea && nodeAboveVisibleArea._shell) { - nodeAboveVisibleArea._shell.ensureActive(); - } - }, 10); - }, - - _place: "", - get place() { - return this._place; - }, - set place(val) { - // Don't reload everything if we don't have to. - if (this._place == val) { - // XXXmano: places.js relies on this behavior (see Bug 822203). - this.searchTerm = ""; - return val; - } - - this._place = val; - - let history = PlacesUtils.history; - let queries = { }, options = { }; - history.queryStringToQueries(val, queries, { }, options); - if (!queries.value.length) { - queries.value = [history.getNewQuery()]; - } - - let result = history.executeQueries(queries.value, queries.value.length, - options.value); - result.addObserver(this, false); - return val; - }, - - _result: null, - get result() { - return this._result; - }, - set result(val) { - if (this._result == val) { - return val; - } - - if (this._result) { - this._result.removeObserver(this); - this._resultNode.containerOpen = false; - } - - if (val) { - this._result = val; - this._resultNode = val.root; - this._resultNode.containerOpen = true; - this._ensureInitialSelection(); - } else { - delete this._resultNode; - delete this._result; - } - - return val; - }, - - get selectedNodes() { - return [for (element of this._richlistbox.selectedItems) - if (element._placesNode) - element._placesNode]; - }, - - get selectedNode() { - let selectedNodes = this.selectedNodes; - return selectedNodes.length == 1 ? selectedNodes[0] : null; - }, - - get hasSelection() { - return this.selectedNodes.length > 0; - }, - - containerStateChanged(aNode, aOldState, aNewState) { - this.invalidateContainer(aNode) - }, - - invalidateContainer(aContainer) { - if (aContainer != this._resultNode) { - throw new Error("Unexpected container node"); - } - if (!aContainer.containerOpen) { - throw new Error("Root container for the downloads query cannot be closed"); - } - - let suppressOnSelect = this._richlistbox.suppressOnSelect; - this._richlistbox.suppressOnSelect = true; - try { - // Remove the invalidated history downloads from the list and unset the - // places node for data downloads. - // 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); - } - } - } finally { - this._richlistbox.suppressOnSelect = suppressOnSelect; - } - - if (aContainer.childCount > 0) { - let elementsToAppendFragment = document.createDocumentFragment(); - for (let i = 0; i < aContainer.childCount; i++) { - try { - this._addDownloadData(null, aContainer.getChild(i), false, - elementsToAppendFragment); - } catch (ex) { - Cu.reportError(ex); - } - } - - // _addDownloadData may not add new elements if there were already - // data items in place. - if (elementsToAppendFragment.firstChild) { - this._appendDownloadsFragment(elementsToAppendFragment); - this._ensureVisibleElementsAreActive(); - } - } - - goUpdateDownloadCommands(); - }, - - _appendDownloadsFragment(aDOMFragment) { - // Workaround multiple reflows hang by removing the richlistbox - // and adding it back when we're done. - - // Hack for bug 836283: reset xbl fields to their old values after the - // binding is reattached to avoid breaking the selection state - let xblFields = new Map(); - for (let key of Object.getOwnPropertyNames(this._richlistbox)) { - let value = this._richlistbox[key]; - xblFields.set(key, value); - } - - let parentNode = this._richlistbox.parentNode; - let nextSibling = this._richlistbox.nextSibling; - parentNode.removeChild(this._richlistbox); - this._richlistbox.appendChild(aDOMFragment); - parentNode.insertBefore(this._richlistbox, nextSibling); - - for (let [key, value] of xblFields) { - this._richlistbox[key] = value; - } - }, - - nodeInserted(aParent, aPlacesNode) { - this._addDownloadData(null, aPlacesNode); - }, - - nodeRemoved(aParent, aPlacesNode, aOldIndex) { - this._removeHistoryDownloadFromView(aPlacesNode); - }, - - nodeAnnotationChanged() {}, - nodeIconChanged() {}, - nodeTitleChanged() {}, - nodeKeywordChanged() {}, - nodeDateAddedChanged() {}, - nodeLastModifiedChanged() {}, - nodeHistoryDetailsChanged() {}, - nodeTagsChanged() {}, - sortingChanged() {}, - nodeMoved() {}, - nodeURIChanged() {}, - batching() {}, - - get controller() { - return this._richlistbox.controller; - }, - - get searchTerm() { - return this._searchTerm; - }, - set searchTerm(aValue) { - if (this._searchTerm != aValue) { - for (let element of this._richlistbox.childNodes) { - element.hidden = !element._shell.matchesSearchTerm(aValue); - } - this._ensureVisibleElementsAreActive(); - } - return this._searchTerm = aValue; - }, - - /** - * When the view loads, we want to select the first item. - * However, because session downloads, for which the data is loaded - * asynchronously, always come first in the list, and because the list - * may (or may not) already contain history downloads at that point, it - * turns out that by the time we can select the first item, the user may - * have already started using the view. - * To make things even more complicated, in other cases, the places data - * may be loaded after the session downloads data. Thus we cannot rely on - * the order in which the data comes in. - * We work around this by attempting to select the first element twice, - * once after the places data is loaded and once when the session downloads - * 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() { - // Either they're both null, or the selection has not changed in between. - if (this._richlistbox.selectedItem == this._initiallySelectedElement) { - let firstDownloadElement = this._richlistbox.firstChild; - if (firstDownloadElement != this._initiallySelectedElement) { - // We may be called before _ensureVisibleElementsAreActive, - // or before the download binding is attached. Therefore, ensure the - // 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(() => { - this._richlistbox.selectedItem = firstDownloadElement; - this._richlistbox.currentItem = firstDownloadElement; - this._initiallySelectedElement = firstDownloadElement; - }, Ci.nsIThread.DISPATCH_NORMAL); - } - } - }, - - onDataLoadStarting() {}, - onDataLoadCompleted() { - this._ensureInitialSelection(); - }, - - onDownloadAdded(download, newest) { - this._addDownloadData(download, null, newest); - }, - - onDownloadStateChanged(download) { - this._viewItemsForDownloads.get(download).onStateChanged(); - }, - - onDownloadChanged(download) { - this._viewItemsForDownloads.get(download).onChanged(); - }, - - onDownloadRemoved(download) { - this._removeSessionDownloadFromView(download); - }, - - // nsIController - supportsCommand(aCommand) { - // Firstly, determine if this is a command that we can handle. - if (!DownloadsViewUI.isCommandName(aCommand)) { - return false; - } - if (!(aCommand in this) && - !(aCommand in HistoryDownloadElementShell.prototype)) { - return false; - } - // If this function returns true, other controllers won't get a chance to - // process the command even if isCommandEnabled returns false, so it's - // important to check if the list is focused here to handle common commands - // like copy and paste correctly. The clear downloads command, instead, is - // specific to the downloads list but can be invoked from the toolbar, so we - // can just return true unconditionally. - return aCommand == "downloadsCmd_clearDownloads" || - document.activeElement == this._richlistbox; - }, - - // nsIController - isCommandEnabled(aCommand) { - switch (aCommand) { - case "cmd_copy": - return this._richlistbox.selectedItems.length > 0; - case "cmd_selectAll": - return true; - case "cmd_paste": - return this._canDownloadClipboardURL(); - case "downloadsCmd_clearDownloads": - return this._canClearDownloads(); - default: - return Array.every(this._richlistbox.selectedItems, - element => element._shell.isCommandEnabled(aCommand)); - } - }, - - _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)) { - return true; - } - } - return false; - }, - - _copySelectedDownloadsToClipboard() { - let urls = [for (element of this._richlistbox.selectedItems) - element._shell.download.source.url]; - - Cc["@mozilla.org/widget/clipboardhelper;1"] - .getService(Ci.nsIClipboardHelper) - .copyString(urls.join("\n")); - }, - - _getURLFromClipboardData() { - let trans = Cc["@mozilla.org/widget/transferable;1"]. - createInstance(Ci.nsITransferable); - trans.init(null); - - let flavors = ["text/x-moz-url", "text/unicode"]; - flavors.forEach(trans.addDataFlavor); - - Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard); - - // Getting the data or creating the nsIURI might fail. - try { - let data = {}; - trans.getAnyTransferData({}, data, {}); - let [url, name] = data.value.QueryInterface(Ci.nsISupportsString) - .data.split("\n"); - if (url) { - return [NetUtil.newURI(url, null, null).spec, name]; - } - } catch (ex) {} - - return ["", ""]; - }, - - _canDownloadClipboardURL() { - let [url, name] = this._getURLFromClipboardData(); - return url != ""; - }, - - _downloadURLFromClipboard() { - let [url, name] = this._getURLFromClipboardData(); - let browserWin = RecentWindow.getMostRecentBrowserWindow(); - let initiatingDoc = browserWin ? browserWin.document : document; - DownloadURL(url, name, initiatingDoc); - }, - - // nsIController - doCommand(aCommand) { - // Commands may be invoked with keyboard shortcuts even if disabled. - if (!this.isCommandEnabled(aCommand)) { - return; - } - - // If this command is not selection-specific, execute it. - if (aCommand in this) { - this[aCommand](); - return; - } - - // Cloning the nodelist into an array to get a frozen list of selected items. - // Otherwise, the selectedItems nodelist is live and doCommand may alter the - // selection while we are trying to do one particular action, like removing - // items from history. - let selectedElements = [...this._richlistbox.selectedItems]; - for (let element of selectedElements) { - element._shell.doCommand(aCommand); - } - }, - - // nsIController - onEvent() {}, - - cmd_copy() { - this._copySelectedDownloadsToClipboard(); - }, - - cmd_selectAll() { - this._richlistbox.selectAll(); - }, - - cmd_paste() { - this._downloadURLFromClipboard(); - }, - - downloadsCmd_clearDownloads() { - this._downloadsData.removeFinished(); - if (this.result) { - Cc["@mozilla.org/browser/download-history;1"] - .getService(Ci.nsIDownloadHistory) - .removeAllDownloads(); - } - // There may be no selection or focus change as a result - // of these change, and we want the command updated immediately. - goUpdateCommand("downloadsCmd_clearDownloads"); - }, - - onContextMenu(aEvent) { - let element = this._richlistbox.selectedItem; - 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"); - } - - return true; - }, - - 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 - // supported when a single item is selected. To be consistent, do the - // same here. - if (selectedElements.length == 1) { - let element = selectedElements[0]; - 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")) { - element._shell.doCommand("downloadsCmd_pauseResume"); - } - } - } - }, - - onDoubleClick(aEvent) { - if (aEvent.button != 0) { - return; - } - - let selectedElements = this._richlistbox.selectedItems; - if (selectedElements.length != 1) { - return; - } - - let element = selectedElements[0]; - if (element._shell) { - element._shell.doDefaultCommand(); - } - }, - - onScroll() { - this._ensureVisibleElementsAreActive(); - }, - - onSelect() { - goUpdateDownloadCommands(); - - let selectedElements = this._richlistbox.selectedItems; - for (let elt of selectedElements) { - if (elt._shell) { - elt._shell.onSelect(); - } - } - }, - - 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) { - return; - } - - let targetPath = selectedItem._shell.download.target.path; - if (!targetPath) { - return; - } - - // We must check for existence synchronously because this is a DOM event. - let file = new FileUtils.File(targetPath); - if (!file.exists()) { - return; - } - - let dt = aEvent.dataTransfer; - dt.mozSetDataAt("application/x-moz-file", file, 0); - let url = Services.io.newFileURI(file).spec; - dt.setData("text/uri-list", url); - dt.setData("text/plain", url); - dt.effectAllowed = "copyMove"; - dt.addElement(selectedItem); - }, - - onDragOver(aEvent) { - let types = aEvent.dataTransfer.types; - if (types.includes("text/uri-list") || - types.includes("text/x-moz-url") || - types.includes("text/plain")) { - aEvent.preventDefault(); - } - }, - - 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)) { - return; - } - - let links = Services.droppedLinkHandler.dropLinks(aEvent); - if (!links.length) - return; - let browserWin = RecentWindow.getMostRecentBrowserWindow(); - let initiatingDoc = browserWin ? browserWin.document : document; - for (let link of links) { - if (link.url.startsWith("about:")) - continue; - DownloadURL(link.url, link.name, initiatingDoc); - } - }, -}; - -for (let methodName of ["load", "applyFilter", "selectNode", "selectItems"]) { - DownloadsPlacesView.prototype[methodName] = function () { - throw new Error("|" + methodName + - "| is not implemented by the downloads view."); - } -} - -function goUpdateDownloadCommands() { - function updateCommandsForObject(object) { - for (let name in object) { - if (DownloadsViewUI.isCommandName(name)) { - goUpdateCommand(name); - } - } - } - updateCommandsForObject(DownloadsPlacesView.prototype); - updateCommandsForObject(HistoryDownloadElementShell.prototype); -} diff --git a/browser/components/downloads/content/allDownloadsViewOverlay.xul b/browser/components/downloads/content/allDownloadsViewOverlay.xul deleted file mode 100644 index cb8c699bf..000000000 --- a/browser/components/downloads/content/allDownloadsViewOverlay.xul +++ /dev/null @@ -1,131 +0,0 @@ -<?xml version="1.0"?> - -# 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/. - -<?xml-stylesheet href="chrome://browser/content/downloads/downloads.css"?> -<?xml-stylesheet href="chrome://browser/skin/downloads/allDownloadsViewOverlay.css"?> - -<!DOCTYPE overlay [ -<!ENTITY % downloadsDTD SYSTEM "chrome://browser/locale/downloads/downloads.dtd"> -%downloadsDTD; -]> - -<!-- This overlay provides a downloads view that lists both session downloads, - using the DownloadsView API, and history downloads, using places queries. - The view also implements a command controller and a context menu for - managing the downloads list. In order to use this view: - 1. Apply this overlay to your window. - 2. Insert in all the overlay entry-points, namely: - <richlistbox id="downloadsRichListBox"/> - <commandset id="downloadCommands"/> - <menupopup id="downloadsContextMenu"/> - 3. Make sure your window has the editMenuOverlay overlay applied, - because the view implements cmd_copy and cmd_delete. - 4. Make sure your window has the globalOverlay.js script loaded. - 5. To initialize the view - let view = new DownloadsPlacesView(document.getElementById("downloadsRichListBox")); - // This is what the Places Library uses. It could be tweaked a bit as long as the - // transition-type is set correctly - view.place = "place:transition=7&sort=4"; ---> -<overlay id="downloadsViewOverlay" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> - - <script type="application/javascript" - src="chrome://browser/content/downloads/allDownloadsViewOverlay.js"/> - <script type="application/javascript" - src="chrome://global/content/contentAreaUtils.js"/> - - <richlistbox flex="1" - seltype="multiple" - id="downloadsRichListBox" context="downloadsContextMenu" - onscroll="return this._placesView.onScroll();" - onkeypress="return this._placesView.onKeyPress(event);" - ondblclick="return this._placesView.onDoubleClick(event);" - oncontextmenu="return this._placesView.onContextMenu(event);" - ondragstart="this._placesView.onDragStart(event);" - ondragover="this._placesView.onDragOver(event);" - ondrop="this._placesView.onDrop(event);" - onfocus="goUpdateDownloadCommands();" - onselect="this._placesView.onSelect();" - onblur="goUpdateDownloadCommands();"/> - - <commandset id="downloadCommands" - commandupdater="true" - events="focus,select,contextmenu" - oncommandupdate="goUpdateDownloadCommands();"> - <command id="downloadsCmd_pauseResume" - oncommand="goDoCommand('downloadsCmd_pauseResume')"/> - <command id="downloadsCmd_cancel" - oncommand="goDoCommand('downloadsCmd_cancel')"/> - <command id="downloadsCmd_unblock" - oncommand="goDoCommand('downloadsCmd_unblock')"/> - <command id="downloadsCmd_chooseUnblock" - oncommand="goDoCommand('downloadsCmd_chooseUnblock')"/> - <command id="downloadsCmd_chooseOpen" - oncommand="goDoCommand('downloadsCmd_chooseOpen')"/> - <command id="downloadsCmd_confirmBlock" - oncommand="goDoCommand('downloadsCmd_confirmBlock')"/> - <command id="downloadsCmd_open" - oncommand="goDoCommand('downloadsCmd_open')"/> - <command id="downloadsCmd_show" - oncommand="goDoCommand('downloadsCmd_show')"/> - <command id="downloadsCmd_retry" - oncommand="goDoCommand('downloadsCmd_retry')"/> - <command id="downloadsCmd_openReferrer" - oncommand="goDoCommand('downloadsCmd_openReferrer')"/> - <command id="downloadsCmd_clearDownloads" - oncommand="goDoCommand('downloadsCmd_clearDownloads')"/> - </commandset> - - <menupopup id="downloadsContextMenu" class="download-state"> - <menuitem command="downloadsCmd_pauseResume" - class="downloadPauseMenuItem" - label="&cmd.pause.label;" - accesskey="&cmd.pause.accesskey;"/> - <menuitem command="downloadsCmd_pauseResume" - class="downloadResumeMenuItem" - label="&cmd.resume.label;" - accesskey="&cmd.resume.accesskey;"/> - <menuitem command="downloadsCmd_cancel" - class="downloadCancelMenuItem" - label="&cmd.cancel.label;" - accesskey="&cmd.cancel.accesskey;"/> - <menuitem command="downloadsCmd_unblock" - class="downloadUnblockMenuItem" - label="&cmd.unblock2.label;" - accesskey="&cmd.unblock2.accesskey;"/> - <menuitem command="cmd_delete" - class="downloadRemoveFromHistoryMenuItem" - label="&cmd.removeFromHistory.label;" - accesskey="&cmd.removeFromHistory.accesskey;"/> - <menuitem command="downloadsCmd_show" - class="downloadShowMenuItem" -#ifdef XP_MACOSX - label="&cmd.showMac.label;" - accesskey="&cmd.showMac.accesskey;" -#else - label="&cmd.show.label;" - accesskey="&cmd.show.accesskey;" -#endif - /> - - <menuseparator class="downloadCommandsSeparator"/> - - <menuitem command="downloadsCmd_openReferrer" - label="&cmd.goToDownloadPage.label;" - accesskey="&cmd.goToDownloadPage.accesskey;"/> - <menuitem command="cmd_copy" - label="&cmd.copyDownloadLink.label;" - accesskey="&cmd.copyDownloadLink.accesskey;"/> - - <menuseparator/> - - <menuitem command="downloadsCmd_clearDownloads" - label="&cmd.clearDownloads.label;" - accesskey="&cmd.clearDownloads.accesskey;"/> - </menupopup> -</overlay> diff --git a/browser/components/downloads/content/contentAreaDownloadsView.css b/browser/components/downloads/content/contentAreaDownloadsView.css deleted file mode 100644 index abaae1f7b..000000000 --- a/browser/components/downloads/content/contentAreaDownloadsView.css +++ /dev/null @@ -1,11 +0,0 @@ -/* 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/. */ - -#downloadsListEmptyDescription { - display: none; -} - -#downloadsRichListBox:empty + #downloadsListEmptyDescription { - display: -moz-box; -} diff --git a/browser/components/downloads/content/contentAreaDownloadsView.js b/browser/components/downloads/content/contentAreaDownloadsView.js deleted file mode 100644 index 6e4f18599..000000000 --- a/browser/components/downloads/content/contentAreaDownloadsView.js +++ /dev/null @@ -1,17 +0,0 @@ -/* 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/. */ - -Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); - -var ContentAreaDownloadsView = { - init() { - let view = new DownloadsPlacesView(document.getElementById("downloadsRichListBox")); - // Do not display the Places downloads in private windows - if (!PrivateBrowsingUtils.isContentWindowPrivate(window)) { - view.place = "place:transition=7&sort=4"; - } - // Set focus to Downloads list once it is created - document.getElementById("downloadsRichListBox").focus(); - }, -}; diff --git a/browser/components/downloads/content/contentAreaDownloadsView.xul b/browser/components/downloads/content/contentAreaDownloadsView.xul deleted file mode 100644 index 91c986656..000000000 --- a/browser/components/downloads/content/contentAreaDownloadsView.xul +++ /dev/null @@ -1,46 +0,0 @@ -<?xml version="1.0"?> - -# 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/. - -<?xml-stylesheet href="chrome://global/skin/"?> -<?xml-stylesheet href="chrome://browser/content/downloads/contentAreaDownloadsView.css"?> -<?xml-stylesheet href="chrome://browser/skin/downloads/contentAreaDownloadsView.css"?> - -<?xul-overlay href="chrome://browser/content/downloads/allDownloadsViewOverlay.xul"?> - -<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> - -<!DOCTYPE window [ -<!ENTITY % downloadsDTD SYSTEM "chrome://browser/locale/downloads/downloads.dtd"> -%downloadsDTD; -]> - -<window id="contentAreaDownloadsView" - xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - title="&downloads.title;" - onload="ContentAreaDownloadsView.init();"> - - <script type="application/javascript" - src="chrome://global/content/globalOverlay.js"/> - <script type="application/javascript" - src="chrome://browser/content/downloads/contentAreaDownloadsView.js"/> - - <commandset id="editMenuCommands"/> - - <keyset id="editMenuKeys"> -#ifdef XP_MACOSX - <key id="key_delete2" keycode="VK_BACK" command="cmd_delete"/> -#endif - </keyset> - - <stack flex="1"> - <richlistbox id="downloadsRichListBox"/> - <description id="downloadsListEmptyDescription" - value="&downloadsListEmpty.label;" - mousethrough="always"/> - </stack> - <commandset id="downloadCommands"/> - <menupopup id="downloadsContextMenu"/> -</window> diff --git a/browser/components/downloads/content/download.xml b/browser/components/downloads/content/download.xml deleted file mode 100644 index 29a1530fd..000000000 --- a/browser/components/downloads/content/download.xml +++ /dev/null @@ -1,99 +0,0 @@ -<?xml version="1.0"?> -<!-- -*- Mode: HTML; 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, - - You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<!DOCTYPE bindings SYSTEM "chrome://browser/locale/downloads/downloads.dtd"> - -<bindings id="downloadBindings" - xmlns="http://www.mozilla.org/xbl" - xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - xmlns:xbl="http://www.mozilla.org/xbl"> - - <binding id="download" - extends="chrome://global/content/bindings/richlistbox.xml#richlistitem"> - <content orient="horizontal" - onclick="DownloadsView.onDownloadClick(event);"> - <xul:hbox class="downloadMainArea" - flex="1" - align="center"> - <xul:stack> - <xul:image class="downloadTypeIcon" - validate="always" - xbl:inherits="src=image"/> - <xul:image class="downloadBlockedBadge" /> - </xul:stack> - <xul:vbox pack="center" - flex="1" - class="downloadContainer" - style="width: &downloadDetails.width;"> - <!-- We're letting localizers put a min-width in here primarily - because of the downloads summary at the bottom of the list of - download items. An element in the summary has the same min-width - on a description, and we don't want the panel to change size if the - summary isn't being displayed, so we ensure that items share the - same minimum width. - --> - <xul:description class="downloadTarget" - crop="center" - style="min-width: &downloadsSummary.minWidth2;" - xbl:inherits="value=displayName,tooltiptext=displayName"/> - <xul:progressmeter anonid="progressmeter" - class="downloadProgress" - min="0" - max="100" - xbl:inherits="mode=progressmode,value=progress,paused=progresspaused"/> - <xul:description class="downloadDetails" - crop="end" - xbl:inherits="value=status,tooltiptext=statusTip"/> - </xul:vbox> - </xul:hbox> - <xul:toolbarseparator /> - <xul:stack class="downloadButtonArea"> - <xul:button class="downloadButton downloadCancel downloadIconCancel" - tooltiptext="&cmd.cancel.label;" - oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_cancel');"/> - <xul:button class="downloadButton downloadRetry downloadIconRetry" - tooltiptext="&cmd.retry.label;" - oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_retry');"/> - <xul:button class="downloadButton downloadShow downloadIconShow" -#ifdef XP_MACOSX - tooltiptext="&cmd.showMac.label;" -#else - tooltiptext="&cmd.show.label;" -#endif - oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_show');"/> - <xul:button class="downloadButton downloadConfirmBlock downloadIconCancel" - tooltiptext="&cmd.removeFile.label;" - oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_confirmBlock');"/> - <xul:button class="downloadButton downloadChooseUnblock downloadIconShow" - tooltiptext="&cmd.chooseUnblock.label;" - oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_chooseUnblock');"/> - <xul:button class="downloadButton downloadChooseOpen downloadIconShow" - tooltiptext="&cmd.chooseOpen.label;" - oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_chooseOpen');"/> - <xul:button class="downloadButton downloadShowBlockedInfo" - tooltiptext="&cmd.chooseUnblock.label;" - oncommand="DownloadsView.onDownloadCommand(event, 'downloadsCmd_showBlockedInfo');"/> - </xul:stack> - </content> - </binding> - - <binding id="download-toolbarbutton" - extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton-badged"> - <content> - <xul:stack class="toolbarbutton-badge-stack"> - <children /> - <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,consumeanchor"/> - <xul:label class="toolbarbutton-badge" xbl:inherits="value=badge" top="0" end="0" crop="none"/> - </xul:stack> - <xul:label class="toolbarbutton-text" crop="right" flex="1" - xbl:inherits="value=label,accesskey,crop,wrap"/> - <xul:label class="toolbarbutton-multiline-text" flex="1" - xbl:inherits="xbl:text=label,accesskey,wrap"/> - </content> - </binding> -</bindings> diff --git a/browser/components/downloads/content/downloads.css b/browser/components/downloads/content/downloads.css deleted file mode 100644 index dd47c6f91..000000000 --- a/browser/components/downloads/content/downloads.css +++ /dev/null @@ -1,267 +0,0 @@ -/* 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/. */ - -/*** Downloads Panel ***/ - -richlistitem[type="download"] { - -moz-binding: url('chrome://browser/content/downloads/download.xml#download'); -} - -richlistitem[type="download"]:not([selected]) button { - /* Only focus buttons in the selected item. */ - -moz-user-focus: none; -} - -.downloadsHideDropmarker > #downloadsFooterButtonsSplitter, -.downloadsHideDropmarker > #downloadsFooterDropmarker { - display: none; -} - -richlistitem[type="download"].download-state[state="1"]:not([exists]) > .downloadButtonArea, -richlistitem[type="download"].download-state[state="1"]:not([exists]) > toolbarseparator { - display: none; -} - -#downloadsSummary:not([inprogress]) > vbox > #downloadsSummaryProgress, -#downloadsSummary:not([inprogress]) > vbox > #downloadsSummaryDetails, -#downloadsFooter:not([showingsummary]) #downloadsSummary { - display: none; -} - -#downloadsFooter[showingdropdown] > stack > #downloadsSummary, -#downloadsFooter[showingsummary] > stack:hover > #downloadsSummary, -#downloadsFooter[showingsummary]:not([showingdropdown]) > stack:not(:hover) > #downloadsFooterButtons { - /* If we used "visibility: hidden;" then the mouseenter event of - #downloadsHistory wouldn't be triggered immediately, and the hover styling - of the button would not apply until the mouse is moved again. - - "-moz-user-focus: ignore;" prevents the elements with "opacity: 0;" from - being focused with the keyboard. */ - opacity: 0; - -moz-user-focus: ignore; -} - -/*** Downloads View ***/ - -/** - * The downloads richlistbox may list thousands of items, and it turns out - * XBL binding attachment, and even more so detachment, is a performance hog. - * This hack makes sure we don't apply any binding to inactive items (inactive - * items are history downloads that haven't been in the visible area). - * We can do this because the richlistbox implementation does not interact - * much with the richlistitem binding. However, this may turn out to have - * some side effects (see bug 828111 for the details). - * - * We might be able to do away with this workaround once bug 653881 is fixed. - */ -richlistitem.download { - -moz-binding: none; -} - -richlistitem.download[active] { - -moz-binding: url("chrome://browser/content/downloads/download.xml#download"); -} - -richlistitem.download button { - /* These buttons should never get focus, as that would "disable" - the downloads view controller (it's only used when the richlistbox - is focused). */ - -moz-user-focus: none; -} - -/*** Visibility of controls inside download items ***/ -.download-state:not(:-moz-any([state="6"], /* Blocked (parental) */ - [state="8"], /* Blocked (dirty) */ - [state="9"]) /* Blocked (policy) */) - .downloadBlockedBadge, - -.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */ - [state="5"], /* Starting (queued) */ - [state="0"], /* Downloading */ - [state="4"], /* Paused */ - [state="7"]) /* Scanning */) - .downloadProgress, - -.download-state:not( [state="0"] /* Downloading */) - .downloadPauseMenuItem, - -.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, - -.download-state:not(:-moz-any([state="1"], /* Finished */ - [state="2"], /* Failed */ - [state="3"], /* Canceled */ - [state="6"], /* Blocked (parental) */ - [state="8"], /* Blocked (dirty) */ - [state="9"]) /* Blocked (policy) */) - .downloadRemoveFromHistoryMenuItem, - -.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */ - [state="0"], /* Downloading */ - [state="1"], /* Finished */ - [state="4"], /* Paused */ - [state="5"]) /* Starting (queued) */) - .downloadShowMenuItem, - -.download-state[state="7"] .downloadCommandsSeparator - -{ - display: none; -} - -/*** Visibility of download buttons ***/ - -.download-state:not(:-moz-any([state="-1"],/* Starting (initial) */ - [state="5"], /* Starting (queued) */ - [state="0"], /* Downloading */ - [state="4"]) /* Paused */) - .downloadCancel, - -/* Blocked (dirty) downloads that have not been confirmed and - have temporary data, for the Malware case. */ -.download-state:not( [state="8"] /* Blocked (dirty) */) - .downloadConfirmBlock, -.download-state[state="8"]:not(.temporary-block) - .downloadConfirmBlock, -.download-state[state="8"].temporary-block:not([verdict="Malware"]) - .downloadConfirmBlock, - -/* Blocked (dirty) downloads that have not been confirmed and - have temporary data, for the Potentially Unwanted case. */ -.download-state:not( [state="8"] /* Blocked (dirty) */) - .downloadChooseUnblock, -.download-state[state="8"]:not(.temporary-block) - .downloadChooseUnblock, -.download-state[state="8"].temporary-block:not([verdict="PotentiallyUnwanted"]) - .downloadChooseUnblock, - -/* Blocked (dirty) downloads that have not been confirmed and - have temporary data, for the Uncommon case. */ -.download-state:not( [state="8"] /* Blocked (dirty) */) - .downloadChooseOpen, -.download-state[state="8"]:not(.temporary-block) - .downloadChooseOpen, -.download-state[state="8"].temporary-block:not([verdict="Uncommon"]) - .downloadChooseOpen, - -.download-state:not(:-moz-any([state="2"], /* Failed */ - [state="3"]) /* Canceled */) - .downloadRetry, - -.download-state:not( [state="1"] /* Finished */) - .downloadShow, - -.download-state:-moz-any( [state="6"], /* Blocked (parental) */ - [state="7"], /* Scanning */ - [state="9"]) /* Blocked (policy) */ - > toolbarseparator, - -/* The "show blocked info" button is shown only in the downloads panel. */ -.downloadShowBlockedInfo -{ - display: none; -} - -/*** Downloads panel ***/ - -#downloadsPanel[hasdownloads] #emptyDownloads, -#downloadsPanel:not([hasdownloads]) #downloadsListBox { - display: none; -} - -/*** Downloads panel multiview (main view and blocked-downloads subview) ***/ - -/* Hide all the usual buttons. */ -#downloadsPanel-mainView .download-state[state="8"] .downloadCancel, -#downloadsPanel-mainView .download-state[state="8"] .downloadConfirmBlock, -#downloadsPanel-mainView .download-state[state="8"] .downloadChooseUnblock, -#downloadsPanel-mainView .download-state[state="8"] .downloadChooseOpen, -#downloadsPanel-mainView .download-state[state="8"] .downloadRetry, -#downloadsPanel-mainView .download-state[state="8"] .downloadShow { - display: none; -} - -/* Make the panel wide enough to show the download list items without improperly - truncating them. */ -#downloadsPanel-multiView > .panel-viewcontainer, -#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack, -#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack > .panel-mainview { - max-width: unset; -} - -/* Show the "show blocked info" button. */ -#downloadsPanel-mainView .download-state[state="8"] .downloadShowBlockedInfo { - display: inline; -} - -/** When the main view is showing... **/ - -/* The subview should be off to the right and not visible at all. */ -#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype=main] > .panel-subviews { - transform: translateX(101%); - transition: transform var(--panelui-subview-transition-duration); -} - -#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype=main] > .panel-subviews:-moz-locale-dir(rtl) { - transform: translateX(-101%); -} - -/** When the subview is showing... **/ - -/* Hide the buttons of all downloads except the one that triggered the - subview. */ -#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] .download-state:not([showingsubview]) .downloadButton { - visibility: hidden; -} - -/* For the download that triggered the subview, move its button farther to the - right by removing padding so that a minimum amount of the main view's right - edge needs to be shown. */ -#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] .download-state[showingsubview] { - padding: 0; -} - -/* The main view should slide to the left and its right edge should remain - visible. */ -#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype=subview] > .panel-mainview { - transform: translateX(calc(-100% + 38px)); - transition: transform var(--panelui-subview-transition-duration); -} - -#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype=subview] > .panel-mainview:-moz-locale-dir(rtl) { - transform: translateX(calc(100% - 38px)); -} - -/* The subview should leave the right edge of the main view uncovered. */ -#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack > .panel-subviews { - /* Use a margin instead of a transform like above so that the subview's width - isn't wider than the panel. */ - -moz-margin-start: 38px !important; -} - -/* Prevent keyboard interaction in the main view by preventing all elements in - the main view from being focused... */ -#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] > .panel-mainview #downloadsListBox, -#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] > .panel-mainview richlistitem, -#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] > .panel-mainview .downloadButton, -#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] > .panel-mainview .downloadsPanelFooterButton, -#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] > .panel-mainview #downloadsSummary { - -moz-user-focus: ignore; -} -/* ... except for the downloadShowBlockedInfo button in the blocked download. - Selecting it with the keyboard should show the main view again. */ -#downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] .download-state[showingsubview] .downloadShowBlockedInfo { - -moz-user-focus: normal; -} diff --git a/browser/components/downloads/content/downloads.js b/browser/components/downloads/content/downloads.js deleted file mode 100644 index 57397c815..000000000 --- a/browser/components/downloads/content/downloads.js +++ /dev/null @@ -1,1732 +0,0 @@ -/* -*- indent-tabs-mode: nil; js-indent-level: 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, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/** - * Handles the Downloads panel user interface for each browser window. - * - * This file includes the following constructors and global objects: - * - * DownloadsPanel - * Main entry point for the downloads panel interface. - * - * DownloadsOverlayLoader - * Allows loading the downloads panel and the status indicator interfaces on - * demand, to improve startup performance. - * - * DownloadsView - * Builds and updates the downloads list widget, responding to changes in the - * download state and real-time data. In addition, handles part of the user - * interaction events raised by the downloads list widget. - * - * DownloadsViewItem - * Builds and updates a single item in the downloads list widget, responding to - * changes in the download state and real-time data, and handles the user - * interaction events related to a single item in the downloads list widgets. - * - * DownloadsViewController - * Handles part of the user interaction events raised by the downloads list - * widget, in particular the "commands" that apply to multiple items, and - * dispatches the commands that apply to individual items. - */ - -/** - * A few words on focus and focusrings - * - * We do quite a few hacks in the Downloads Panel for focusrings. In fact, we - * basically suppress most if not all XUL-level focusrings, and style/draw - * them ourselves (using :focus instead of -moz-focusring). There are a few - * reasons for this: - * - * 1) Richlists on OSX don't have focusrings; instead, they are shown as - * selected. This makes for some ambiguity when we have a focused/selected - * item in the list, and the mouse is hovering a completed download (which - * highlights). - * 2) Windows doesn't show focusrings until after the first time that tab is - * pressed (and by then you're focusing the second item in the panel). - * 3) Richlistbox sets -moz-focusring even when we select it with a mouse. - * - * In general, the desired behaviour is to focus the first item after pressing - * tab/down, and show that focus with a ring. Then, if the mouse moves over - * the panel, to hide that focus ring; essentially resetting us to the state - * before pressing the key. - * - * We end up capturing the tab/down key events, and preventing their default - * behaviour. We then set a "keyfocus" attribute on the panel, which allows - * us to draw a ring around the currently focused element. If the panel is - * closed or the mouse moves over the panel, we remove the attribute. - */ - -"use strict"; - -var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.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, "PlacesUtils", - "resource://gre/modules/PlacesUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); - -//////////////////////////////////////////////////////////////////////////////// -//// DownloadsPanel - -/** - * Main entry point for the downloads panel interface. - */ -const DownloadsPanel = { - ////////////////////////////////////////////////////////////////////////////// - //// Initialization and termination - - /** - * Internal state of the downloads panel, based on one of the kState - * constants. This is not the same state as the XUL panel element. - */ - _state: 0, - - /** The panel is not linked to downloads data yet. */ - get kStateUninitialized() { - return 0; - }, - /** This object is linked to data, but the panel is invisible. */ - get kStateHidden() { - return 1; - }, - /** The panel will be shown as soon as possible. */ - get kStateWaitingData() { - return 2; - }, - /** The panel is almost shown - we're just waiting to get a handle on the - anchor. */ - get kStateWaitingAnchor() { - return 3; - }, - /** The panel is open. */ - get kStateShown() { - return 4; - }, - - /** - * Location of the panel overlay. - */ - get kDownloadsOverlay() { - return "chrome://browser/content/downloads/downloadsOverlay.xul"; - }, - - /** - * Starts loading the download data in background, without opening the panel. - * Use showPanel instead to load the data and open the panel at the same time. - * - * @param aCallback - * Called when initialization is complete. - */ - initialize(aCallback) { - DownloadsCommon.log("Attempting to initialize DownloadsPanel for a window."); - if (this._state != this.kStateUninitialized) { - DownloadsCommon.log("DownloadsPanel is already initialized."); - DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay, - aCallback); - return; - } - this._state = this.kStateHidden; - - 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. - 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, () => { - DownloadsViewController.initialize(); - DownloadsCommon.log("Attaching DownloadsView..."); - DownloadsCommon.getData(window).addView(DownloadsView); - DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit) - .addView(DownloadsSummary); - DownloadsCommon.log("DownloadsView attached - the panel for this window", - "should now see download items come in."); - DownloadsPanel._attachEventListeners(); - DownloadsCommon.log("DownloadsPanel initialized."); - aCallback(); - }); - }, - - /** - * Closes the downloads panel and frees the internal resources related to the - * downloads. The downloads panel can be reopened later, even after this - * function has been called. - */ - terminate() { - DownloadsCommon.log("Attempting to terminate DownloadsPanel for a window."); - if (this._state == this.kStateUninitialized) { - DownloadsCommon.log("DownloadsPanel was never initialized. Nothing to do."); - return; - } - - window.removeEventListener("unload", this.onWindowUnload, false); - - // Ensure that the panel is closed before shutting down. - this.hidePanel(); - - DownloadsViewController.terminate(); - DownloadsCommon.getData(window).removeView(DownloadsView); - DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit) - .removeView(DownloadsSummary); - this._unattachEventListeners(); - - this._state = this.kStateUninitialized; - - DownloadsSummary.active = false; - DownloadsCommon.log("DownloadsPanel terminated."); - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Panel interface - - /** - * Main panel element in the browser window, or null if the panel overlay - * hasn't been loaded yet. - */ - get panel() { - // If the downloads panel overlay hasn't loaded yet, just return null - // without resetting this.panel. - let downloadsPanel = document.getElementById("downloadsPanel"); - if (!downloadsPanel) - return null; - - delete this.panel; - return this.panel = downloadsPanel; - }, - - /** - * Starts opening the downloads panel interface, anchored to the downloads - * button of the browser window. The list of downloads to display is - * initialized the first time this method is called, and the panel is shown - * only when data is ready. - */ - showPanel() { - DownloadsCommon.log("Opening the downloads panel."); - - if (this.isPanelShowing) { - DownloadsCommon.log("Panel is already showing - focusing instead."); - this._focusPanel(); - return; - } - - this.initialize(() => { - let downloadsFooterButtons = - document.getElementById("downloadsFooterButtons"); - if (DownloadsCommon.showPanelDropmarker) { - downloadsFooterButtons.classList.remove("downloadsHideDropmarker"); - } else { - downloadsFooterButtons.classList.add("downloadsHideDropmarker"); - } - - // 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); - }); - - DownloadsCommon.log("Waiting for the downloads panel to appear."); - this._state = this.kStateWaitingData; - }, - - /** - * Hides the downloads panel, if visible, but keeps the internal state so that - * the panel can be reopened quickly if required. - */ - hidePanel() { - DownloadsCommon.log("Closing the downloads panel."); - - if (!this.isPanelShowing) { - DownloadsCommon.log("Downloads panel is not showing - nothing to do."); - return; - } - - this.panel.hidePopup(); - - // Ensure that we allow the panel to be reopened. Note that, if the popup - // was open, then the onPopupHidden event handler has already updated the - // current state, otherwise we must update the state ourselves. - this._state = this.kStateHidden; - DownloadsCommon.log("Downloads panel is now closed."); - }, - - /** - * Indicates whether the panel is shown or will be shown. - */ - get isPanelShowing() { - return this._state == this.kStateWaitingData || - this._state == this.kStateWaitingAnchor || - this._state == this.kStateShown; - }, - - /** - * Returns whether the user has started keyboard navigation. - */ - get keyFocusing() { - return this.panel.hasAttribute("keyfocus"); - }, - - /** - * Set to true if the user has started keyboard navigation, and we should be - * showing focusrings in the panel. Also adds a mousemove event handler to - * the panel which disables keyFocusing. - */ - set keyFocusing(aValue) { - if (aValue) { - this.panel.setAttribute("keyfocus", "true"); - this.panel.addEventListener("mousemove", this); - } else { - this.panel.removeAttribute("keyfocus"); - this.panel.removeEventListener("mousemove", this); - } - return aValue; - }, - - /** - * Handles the mousemove event for the panel, which disables focusring - * visualization. - */ - handleEvent(aEvent) { - switch (aEvent.type) { - case "mousemove": - this.keyFocusing = false; - break; - case "keydown": - return this._onKeyDown(aEvent); - case "keypress": - return this._onKeyPress(aEvent); - case "popupshown": - if (this.setHeightToFitOnShow) { - this.setHeightToFitOnShow = false; - this.setHeightToFit(); - } - break; - } - }, - - setHeightToFit() { - if (this._state == this.kStateShown) { - DownloadsBlockedSubview.view.setHeightToFit(); - } else { - this.setHeightToFitOnShow = true; - } - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Callback functions from DownloadsView - - /** - * Called after data loading finished. - */ - onViewLoadCompleted() { - this._openPopupIfDataReady(); - }, - - ////////////////////////////////////////////////////////////////////////////// - //// User interface event functions - - onWindowUnload() { - // This function is registered as an event listener, we can't use "this". - DownloadsPanel.terminate(); - }, - - onPopupShown(aEvent) { - // Ignore events raised by nested popups. - if (aEvent.target != aEvent.currentTarget) { - return; - } - - DownloadsCommon.log("Downloads panel has shown."); - this._state = this.kStateShown; - - // Since at most one popup is open at any given time, we can set globally. - DownloadsCommon.getIndicatorData(window).attentionSuppressed = true; - - // Ensure that the first item is selected when the panel is focused. - if (DownloadsView.richListBox.itemCount > 0 && - DownloadsView.richListBox.selectedIndex == -1) { - DownloadsView.richListBox.selectedIndex = 0; - } - - this._focusPanel(); - }, - - onPopupHidden(aEvent) { - // Ignore events raised by nested popups. - if (aEvent.target != aEvent.currentTarget) { - return; - } - - DownloadsCommon.log("Downloads panel has hidden."); - - // Removes the keyfocus attribute so that we stop handling keyboard - // navigation. - this.keyFocusing = false; - - // Since at most one popup is open at any given time, we can set globally. - DownloadsCommon.getIndicatorData(window).attentionSuppressed = false; - - // Allow the anchor to be hidden. - DownloadsButton.releaseAnchor(); - - // Allow the panel to be reopened. - this._state = this.kStateHidden; - }, - - onFooterPopupShowing(aEvent) { - let itemClearList = document.getElementById("downloadsDropdownItemClearList"); - if (DownloadsCommon.getData(window).canRemoveFinished) { - itemClearList.removeAttribute("hidden"); - } else { - itemClearList.setAttribute("hidden", "true"); - } - DownloadsViewController.updateCommands(); - - document.getElementById("downloadsFooter") - .setAttribute("showingdropdown", true); - }, - - onFooterPopupHidden(aEvent) { - document.getElementById("downloadsFooter") - .removeAttribute("showingdropdown"); - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Related operations - - /** - * Shows or focuses the user interface dedicated to downloads history. - */ - 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. - this.hidePanel(); - - BrowserDownloadsUI(); - }, - - openDownloadsFolder() { - Downloads.getPreferredDownloadsDirectory().then(downloadsPath => { - DownloadsCommon.showDirectory(new FileUtils.File(downloadsPath)); - }).catch(Cu.reportError); - this.hidePanel(); - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Internal functions - - /** - * Attach event listeners to a panel element. These listeners should be - * removed in _unattachEventListeners. This is called automatically after the - * panel has successfully loaded. - */ - _attachEventListeners() { - // Handle keydown to support accel-V. - this.panel.addEventListener("keydown", this, false); - // Handle keypress to be able to preventDefault() events before they reach - // the richlistbox, for keyboard navigation. - this.panel.addEventListener("keypress", this, false); - // Handle height adjustment on show. - this.panel.addEventListener("popupshown", this, false); - }, - - /** - * Unattach event listeners that were added in _attachEventListeners. This - * is called automatically on panel termination. - */ - _unattachEventListeners() { - this.panel.removeEventListener("keydown", this, false); - this.panel.removeEventListener("keypress", this, false); - this.panel.removeEventListener("popupshown", this, false); - }, - - _onKeyPress(aEvent) { - // Handle unmodified keys only. - if (aEvent.altKey || aEvent.ctrlKey || aEvent.shiftKey || aEvent.metaKey) { - return; - } - - let richListBox = DownloadsView.richListBox; - - // If the user has pressed the tab, up, or down cursor key, start keyboard - // navigation, thus enabling focusrings in the panel. Keyboard navigation - // is automatically disabled if the user moves the mouse on the panel, or - // if the panel is closed. - if ((aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_TAB || - aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP || - aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) && - !this.keyFocusing) { - 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) { - DownloadsView.richListBox.selectedIndex = 0; - } - aEvent.preventDefault(); - return; - } - - if (aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) { - // If the last element in the list is selected, or the footer is already - // focused, focus the footer. - if (richListBox.selectedItem === richListBox.lastChild || - document.activeElement.parentNode.id === "downloadsFooter") { - DownloadsFooter.focus(); - aEvent.preventDefault(); - return; - } - } - - // Pass keypress events to the richlistbox view when it's focused. - if (document.activeElement === richListBox) { - DownloadsView.onDownloadKeyPress(aEvent); - } - }, - - /** - * Keydown listener that listens for the keys to start key focusing, as well - * as the the accel-V "paste" event, which initiates a file download if the - * pasted item can be resolved to a URI. - */ - _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 && - document.activeElement.parentNode.id === "downloadsFooter" && - DownloadsView.richListBox.firstChild) { - DownloadsView.richListBox.focus(); - DownloadsView.richListBox.selectedItem = DownloadsView.richListBox.lastChild; - aEvent.preventDefault(); - return; - } - - let pasting = aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_V && - aEvent.getModifierState("Accel"); - - if (!pasting) { - return; - } - - DownloadsCommon.log("Received a paste event."); - - let trans = Cc["@mozilla.org/widget/transferable;1"] - .createInstance(Ci.nsITransferable); - trans.init(null); - let flavors = ["text/x-moz-url", "text/unicode"]; - flavors.forEach(trans.addDataFlavor); - Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard); - // Getting the data or creating the nsIURI might fail - try { - let data = {}; - trans.getAnyTransferData({}, data, {}); - let [url, name] = data.value - .QueryInterface(Ci.nsISupportsString) - .data - .split("\n"); - if (!url) { - return; - } - - let uri = NetUtil.newURI(url); - DownloadsCommon.log("Pasted URL seems valid. Starting download."); - DownloadURL(uri.spec, name, document); - } catch (ex) {} - }, - - /** - * Move focus to the main element in the downloads panel, unless another - * element in the panel is already focused. - */ - _focusPanel() { - // We may be invoked while the panel is still waiting to be shown. - if (this._state != this.kStateShown) { - return; - } - - let element = document.commandDispatcher.focusedElement; - while (element && element != this.panel) { - element = element.parentNode; - } - if (!element) { - if (DownloadsView.richListBox.itemCount > 0) { - DownloadsView.richListBox.focus(); - } else { - DownloadsFooter.focus(); - } - } - }, - - /** - * Opens the downloads panel when data is ready to be displayed. - */ - _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) { - return; - } - - this._state = this.kStateWaitingAnchor; - - // 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 => { - // 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) { - 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 - // close the panel any more. To prevent this, check if the window is - // minimized and in that case force the panel to the closed state. - if (window.windowState == Ci.nsIDOMChromeWindow.STATE_MINIMIZED) { - DownloadsButton.releaseAnchor(); - this._state = this.kStateHidden; - 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); - } - - DownloadsCommon.log("Opening downloads panel popup."); - this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, null); - }); - }, -}; - -XPCOMUtils.defineConstant(this, "DownloadsPanel", DownloadsPanel); - -//////////////////////////////////////////////////////////////////////////////// -//// DownloadsOverlayLoader - -/** - * Allows loading the downloads panel and the status indicator interfaces on - * demand, to improve startup performance. - */ -const DownloadsOverlayLoader = { - /** - * We cannot load two overlays at the same time, thus we use a queue of - * pending load requests. - */ - _loadRequests: [], - - /** - * True while we are waiting for an overlay to be loaded. - */ - _overlayLoading: false, - - /** - * This object has a key for each overlay URI that is already loaded. - */ - _loadedOverlays: {}, - - /** - * Loads the specified overlay and invokes the given callback when finished. - * - * @param aOverlay - * String containing the URI of the overlay to load in the current - * window. If this overlay has already been loaded using this - * function, then the overlay is not loaded again. - * @param aCallback - * Invoked when loading is completed. If the overlay is already - * loaded, the function is called immediately. - */ - ensureOverlayLoaded(aOverlay, aCallback) { - // The overlay is already loaded, invoke the callback immediately. - if (aOverlay in this._loadedOverlays) { - aCallback(); - return; - } - - // The callback will be invoked when loading is finished. - this._loadRequests.push({ overlay: aOverlay, callback: aCallback }); - if (this._overlayLoading) { - return; - } - - this._overlayLoading = true; - DownloadsCommon.log("Loading overlay ", aOverlay); - document.loadOverlay(aOverlay, () => { - this._overlayLoading = false; - this._loadedOverlays[aOverlay] = true; - - this.processPendingRequests(); - }); - }, - - /** - * Re-processes all the currently pending requests, invoking the callbacks - * 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() { - // 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; - for (let i = 0; i < currentLength; i++) { - let request = this._loadRequests.shift(); - - // We must call ensureOverlayLoaded again for each request, to check if - // the associated callback can be invoked now, or if we must still wait - // for the associated overlay to load. - this.ensureOverlayLoaded(request.overlay, request.callback); - } - }, -}; - -XPCOMUtils.defineConstant(this, "DownloadsOverlayLoader", DownloadsOverlayLoader); - -//////////////////////////////////////////////////////////////////////////////// -//// DownloadsView - -/** - * Builds and updates the downloads list widget, responding to changes in the - * download state and real-time data. In addition, handles part of the user - * interaction events raised by the downloads list widget. - */ -const DownloadsView = { - ////////////////////////////////////////////////////////////////////////////// - //// Functions handling download items in the list - - /** - * Maximum number of items shown by the list at any given time. - */ - kItemCountLimit: 5, - - /** - * Indicates whether there is an open contextMenu for a download item. - */ - contextMenuOpen: false, - - /** - * Indicates whether there is a DownloadsBlockedSubview open. - */ - subViewOpen: false, - - /** - * Indicates whether we are still loading downloads data asynchronously. - */ - 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. - */ - _downloads: [], - - /** - * 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. - */ - _visibleViewItems: new Map(), - - /** - * Called when the number of items in the list changes. - */ - _itemCountChanged() { - DownloadsCommon.log("The downloads item count has changed - we are tracking", - this._downloads.length, "downloads in total."); - let count = this._downloads.length; - let hiddenCount = count - this.kItemCountLimit; - - if (count > 0) { - DownloadsCommon.log("Setting the panel's hasdownloads attribute to true."); - DownloadsPanel.panel.setAttribute("hasdownloads", "true"); - } else { - DownloadsCommon.log("Removing the panel's hasdownloads attribute."); - DownloadsPanel.panel.removeAttribute("hasdownloads"); - } - - // If we've got some hidden downloads, we should activate the - // DownloadsSummary. The DownloadsSummary will determine whether or not - // it's appropriate to actually display the summary. - DownloadsSummary.active = hiddenCount > 0; - }, - - /** - * Element corresponding to the list of downloads. - */ - get richListBox() { - delete this.richListBox; - return this.richListBox = document.getElementById("downloadsListBox"); - }, - - /** - * Element corresponding to the button for showing more downloads. - */ - get downloadsHistory() { - delete this.downloadsHistory; - return this.downloadsHistory = document.getElementById("downloadsHistory"); - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Callback functions from DownloadsData - - /** - * Called before multiple downloads are about to be loaded. - */ - onDataLoadStarting() { - DownloadsCommon.log("onDataLoadStarting called for DownloadsView."); - this.loading = true; - }, - - /** - * Called after data loading finished. - */ - onDataLoadCompleted() { - DownloadsCommon.log("onDataLoadCompleted called for DownloadsView."); - - this.loading = false; - - // We suppressed item count change notifications during the batch load, at - // this point we should just call the function once. - this._itemCountChanged(); - - // Notify the panel that all the initially available downloads have been - // loaded. This ensures that the interface is visible, if still required. - DownloadsPanel.onViewLoadCompleted(); - }, - - /** - * 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 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 - * started. When false, indicates that the item is the least recent - * and should be appended. The latter generally happens during the - * asynchronous data load. - */ - onDownloadAdded(download, aNewest) { - DownloadsCommon.log("A new download data item was added - aNewest =", - aNewest); - - if (aNewest) { - this._downloads.unshift(download); - } else { - this._downloads.push(download); - } - - let itemsNowOverflow = this._downloads.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); - } - 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]); - } - - // For better performance during batch loads, don't update the count for - // every item, because the interface won't be visible until load finishes. - if (!this.loading) { - this._itemCountChanged(); - } - }, - - 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. - */ - onDownloadRemoved(download) { - DownloadsCommon.log("A download data item was removed."); - - let itemIndex = this._downloads.indexOf(download); - this._downloads.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) { - // Reinsert the next item into the panel. - this._addViewItem(this._downloads[this.kItemCountLimit - 1], false); - } - } - - this._itemCountChanged(); - - // Adjust the panel height if we removed items. - DownloadsPanel.setHeightToFit(); - }, - - /** - * Associates each richlistitem for a download with its corresponding - * DownloadsViewItem object. - */ - _itemsForElements: new Map(), - - itemForElement(element) { - return this._itemsForElements.get(element); - }, - - /** - * 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) - { - 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); - this._itemsForElements.set(element, viewItem); - if (aNewest) { - this.richListBox.insertBefore(element, this.richListBox.firstChild); - } else { - this.richListBox.appendChild(element); - } - }, - - /** - * Removes the view item associated with the specified data item. - */ - _removeViewItem(download) { - DownloadsCommon.log("Removing a DownloadsViewItem from the downloads list."); - let element = this._visibleViewItems.get(download).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._itemsForElements.delete(element); - }, - - ////////////////////////////////////////////////////////////////////////////// - //// User interface event functions - - /** - * Helper function to do commands on a specific download item. - * - * @param aEvent - * Event object for the event being handled. If the event target is - * not a richlistitem that represents a download, this function will - * walk up the parent nodes until it finds a DOM node that is. - * @param aCommand - * The command to be performed. - */ - onDownloadCommand(aEvent, aCommand) { - let target = aEvent.target; - while (target.nodeName != "richlistitem") { - target = target.parentNode; - } - DownloadsView.itemForElement(target).doCommand(aCommand); - }, - - onDownloadClick(aEvent) { - // Handle primary clicks only, and exclude the action button. - if (aEvent.button == 0 && - !aEvent.originalTarget.hasAttribute("oncommand")) { - let target = aEvent.target; - while (target.nodeName != "richlistitem") { - target = target.parentNode; - } - let download = DownloadsView.itemForElement(target).download; - if (download.hasBlockedData) { - goDoCommand("downloadsCmd_showBlockedInfo"); - } else { - goDoCommand("downloadsCmd_open"); - } - } - }, - - /** - * Handles keypress events on a download item. - */ - 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") || - aEvent.originalTarget.hasAttribute("oncommand")) { - return; - } - - if (aEvent.charCode == " ".charCodeAt(0)) { - goDoCommand("downloadsCmd_pauseResume"); - return; - } - - if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) { - goDoCommand("downloadsCmd_doDefault"); - } - }, - - /** - * Event handlers to keep track of context menu state (open/closed) for - * download items. - */ - onContextPopupShown(aEvent) { - // Ignore events raised by nested popups. - if (aEvent.target != aEvent.currentTarget) { - return; - } - - DownloadsCommon.log("Context menu has shown."); - this.contextMenuOpen = true; - }, - - onContextPopupHidden(aEvent) { - // Ignore events raised by nested popups. - if (aEvent.target != aEvent.currentTarget) { - return; - } - - DownloadsCommon.log("Context menu has hidden."); - this.contextMenuOpen = false; - }, - - /** - * Mouse listeners to handle selection on hover. - */ - onDownloadMouseOver(aEvent) { - if (!(this.contextMenuOpen || this.subViewOpen) && - aEvent.target.parentNode == this.richListBox) { - this.richListBox.selectedItem = aEvent.target; - } - }, - - onDownloadMouseOut(aEvent) { - if (!(this.contextMenuOpen || this.subViewOpen) && - aEvent.target.parentNode == this.richListBox) { - // If the destination element is outside of the richlistitem, clear the - // selection. - let element = aEvent.relatedTarget; - while (element && element != aEvent.target) { - element = element.parentNode; - } - if (!element) { - this.richListBox.selectedIndex = -1; - } - } - }, - - onDownloadContextMenu(aEvent) { - let element = this.richListBox.selectedItem; - if (!element) { - return; - } - - DownloadsViewController.updateCommands(); - - // 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) { - 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.itemForElement(element) - .download.target.path); - if (!file.exists()) { - return; - } - - let dataTransfer = aEvent.dataTransfer; - dataTransfer.mozSetDataAt("application/x-moz-file", file, 0); - dataTransfer.effectAllowed = "copyMove"; - let spec = NetUtil.newURI(file).spec; - dataTransfer.setData("text/uri-list", spec); - dataTransfer.setData("text/plain", spec); - dataTransfer.addElement(element); - - aEvent.stopPropagation(); - }, -} - -XPCOMUtils.defineConstant(this, "DownloadsView", DownloadsView); - -//////////////////////////////////////////////////////////////////////////////// -//// DownloadsViewItem - -/** - * Builds and updates a single item in the downloads list widget, responding to - * changes in the download state and real-time data, and handles the user - * interaction events related to a single item in the downloads list widgets. - * - * @param download - * Download object 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"); - - this._updateState(); -} - -DownloadsViewItem.prototype = { - __proto__: DownloadsViewUI.DownloadElementShell.prototype, - - /** - * The XUL element corresponding to the associated richlistbox item. - */ - _element: null, - - onStateChanged() { - this._updateState(); - }, - - onChanged() { - this._updateProgress(); - }, - - 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(); - } - 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(); - } - case "cmd_delete": - case "downloadsCmd_cancel": - case "downloadsCmd_copyLocation": - case "downloadsCmd_doDefault": - return true; - case "downloadsCmd_showBlockedInfo": - return this.download.hasBlockedData; - } - return DownloadsViewUI.DownloadElementShell.prototype - .isCommandEnabled.call(this, aCommand); - }, - - doCommand(aCommand) { - if (this.isCommandEnabled(aCommand)) { - this[aCommand](); - } - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Item commands - - cmd_delete() { - DownloadsCommon.removeAndFinalizeDownload(this.download); - PlacesUtils.bhistory.removePage( - NetUtil.newURI(this.download.source.url)); - }, - - downloadsCmd_unblock() { - DownloadsPanel.hidePanel(); - this.confirmUnblock(window, "unblock"); - }, - - downloadsCmd_chooseUnblock() { - DownloadsPanel.hidePanel(); - this.confirmUnblock(window, "chooseUnblock"); - }, - - downloadsCmd_unblockAndOpen() { - DownloadsPanel.hidePanel(); - this.unblockAndOpenDownload().catch(Cu.reportError); - }, - - downloadsCmd_open() { - this.download.launch().catch(Cu.reportError); - - // 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 - // before the panel would close. This also helps to prevent the user from - // accidentally opening a file several times. - DownloadsPanel.hidePanel(); - }, - - downloadsCmd_show() { - let file = new FileUtils.File(this.download.target.path); - DownloadsCommon.showDownloadedFile(file); - - // 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 operating system file manager - // window to open before the panel closed. This also helps to prevent the - // user from opening the containing folder several times. - DownloadsPanel.hidePanel(); - }, - - downloadsCmd_showBlockedInfo() { - DownloadsBlockedSubview.toggle(this.element, - ...this.rawBlockedTitleAndDetails); - }, - - downloadsCmd_openReferrer() { - openURL(this.download.source.referrer); - }, - - downloadsCmd_copyLocation() { - let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"] - .getService(Ci.nsIClipboardHelper); - clipboard.copyString(this.download.source.url); - }, - - downloadsCmd_doDefault() { - let defaultCommand = this.currentDefaultCommandName; - if (defaultCommand && this.isCommandEnabled(defaultCommand)) { - this.doCommand(defaultCommand); - } - }, -}; - -//////////////////////////////////////////////////////////////////////////////// -//// DownloadsViewController - -/** - * Handles part of the user interaction events raised by the downloads list - * widget, in particular the "commands" that apply to multiple items, and - * dispatches the commands that apply to individual items. - */ -const DownloadsViewController = { - ////////////////////////////////////////////////////////////////////////////// - //// Initialization and termination - - initialize() { - window.controllers.insertControllerAt(0, this); - }, - - terminate() { - window.controllers.removeController(this); - }, - - ////////////////////////////////////////////////////////////////////////////// - //// nsIController - - supportsCommand(aCommand) { - if (aCommand === "downloadsCmd_clearList") { - return true; - } - // Firstly, determine if this is a command that we can handle. - if (!DownloadsViewUI.isCommandName(aCommand)) { - return false; - } - if (!(aCommand in this) && - !(aCommand in DownloadsViewItem.prototype)) { - return false; - } - // The currently supported commands depend on whether the blocked subview is - // showing. If it is, then take the following path. - if (DownloadsBlockedSubview.view.showingSubView) { - let blockedSubviewCmds = [ - "downloadsCmd_unblockAndOpen", - "cmd_delete", - ]; - return blockedSubviewCmds.indexOf(aCommand) >= 0; - } - // If the blocked subview is not showing, then determine if focus is on a - // control in the downloads list. - let element = document.commandDispatcher.focusedElement; - while (element && element != DownloadsView.richListBox) { - element = element.parentNode; - } - // We should handle the command only if the downloads list is among the - // ancestors of the focused element. - return !!element; - }, - - isCommandEnabled(aCommand) { - // Handle commands that are not selection-specific. - if (aCommand == "downloadsCmd_clearList") { - return DownloadsCommon.getData(window).canRemoveFinished; - } - - // Other commands are selection-specific. - let element = DownloadsView.richListBox.selectedItem; - return element && DownloadsView.itemForElement(element) - .isCommandEnabled(aCommand); - }, - - doCommand(aCommand) { - // If this command is not selection-specific, execute it. - if (aCommand in this) { - this[aCommand](); - return; - } - - // Other commands are selection-specific. - let element = DownloadsView.richListBox.selectedItem; - if (element) { - // The doCommand function also checks if the command is enabled. - DownloadsView.itemForElement(element).doCommand(aCommand); - } - }, - - onEvent() {}, - - ////////////////////////////////////////////////////////////////////////////// - //// Other functions - - updateCommands() { - function updateCommandsForObject(object) { - for (let name in object) { - if (DownloadsViewUI.isCommandName(name)) { - goUpdateCommand(name); - } - } - } - updateCommandsForObject(this); - updateCommandsForObject(DownloadsViewItem.prototype); - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Selection-independent commands - - downloadsCmd_clearList() { - DownloadsCommon.getData(window).removeFinished(); - }, -}; - -XPCOMUtils.defineConstant(this, "DownloadsViewController", DownloadsViewController); - -//////////////////////////////////////////////////////////////////////////////// -//// DownloadsSummary - -/** - * Manages the summary at the bottom of the downloads panel list if the number - * of items in the list exceeds the panels limit. - */ -const DownloadsSummary = { - - /** - * Sets the active state of the summary. When active, the summary subscribes - * to the DownloadsCommon DownloadsSummaryData singleton. - * - * @param aActive - * Set to true to activate the summary. - */ - set active(aActive) { - if (aActive == this._active || !this._summaryNode) { - return this._active; - } - if (aActive) { - DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit) - .refreshView(this); - } else { - DownloadsFooter.showingSummary = false; - } - - return this._active = aActive; - }, - - /** - * Returns the active state of the downloads summary. - */ - get active() { - return this._active; - }, - - _active: false, - - /** - * Sets whether or not we show the progress bar. - * - * @param aShowingProgress - * True if we should show the progress bar. - */ - set showingProgress(aShowingProgress) { - if (aShowingProgress) { - this._summaryNode.setAttribute("inprogress", "true"); - } else { - this._summaryNode.removeAttribute("inprogress"); - } - // If progress isn't being shown, then we simply do not show the summary. - return DownloadsFooter.showingSummary = aShowingProgress; - }, - - /** - * Sets the amount of progress that is visible in the progress bar. - * - * @param aValue - * A value between 0 and 100 to represent the progress of the - * summarized downloads. - */ - set percentComplete(aValue) { - if (this._progressNode) { - this._progressNode.setAttribute("value", aValue); - } - return aValue; - }, - - /** - * Sets the description for the download summary. - * - * @param aValue - * A string representing the description of the summarized - * downloads. - */ - set description(aValue) { - if (this._descriptionNode) { - this._descriptionNode.setAttribute("value", aValue); - this._descriptionNode.setAttribute("tooltiptext", aValue); - } - return aValue; - }, - - /** - * Sets the details for the download summary, such as the time remaining, - * the amount of bytes transferred, etc. - * - * @param aValue - * A string representing the details of the summarized - * downloads. - */ - set details(aValue) { - if (this._detailsNode) { - this._detailsNode.setAttribute("value", aValue); - this._detailsNode.setAttribute("tooltiptext", aValue); - } - return aValue; - }, - - /** - * Focuses the root element of the summary. - */ - focus() { - if (this._summaryNode) { - this._summaryNode.focus(); - } - }, - - /** - * Respond to keydown events on the Downloads Summary node. - * - * @param aEvent - * The keydown event being handled. - */ - onKeyDown(aEvent) { - if (aEvent.charCode == " ".charCodeAt(0) || - aEvent.keyCode == KeyEvent.DOM_VK_RETURN) { - DownloadsPanel.showDownloadsHistory(); - } - }, - - /** - * Respond to click events on the Downloads Summary node. - * - * @param aEvent - * The click event being handled. - */ - onClick(aEvent) { - DownloadsPanel.showDownloadsHistory(); - }, - - /** - * Element corresponding to the root of the downloads summary. - */ - get _summaryNode() { - let node = document.getElementById("downloadsSummary"); - if (!node) { - return null; - } - delete this._summaryNode; - return this._summaryNode = node; - }, - - /** - * Element corresponding to the progress bar in the downloads summary. - */ - get _progressNode() { - let node = document.getElementById("downloadsSummaryProgress"); - if (!node) { - return null; - } - delete this._progressNode; - return this._progressNode = node; - }, - - /** - * Element corresponding to the main description of the downloads - * summary. - */ - get _descriptionNode() { - let node = document.getElementById("downloadsSummaryDescription"); - if (!node) { - return null; - } - delete this._descriptionNode; - return this._descriptionNode = node; - }, - - /** - * Element corresponding to the secondary description of the downloads - * summary. - */ - get _detailsNode() { - let node = document.getElementById("downloadsSummaryDetails"); - if (!node) { - return null; - } - delete this._detailsNode; - return this._detailsNode = node; - } -}; - -XPCOMUtils.defineConstant(this, "DownloadsSummary", DownloadsSummary); - -//////////////////////////////////////////////////////////////////////////////// -//// DownloadsFooter - -/** - * Manages events sent to to the footer vbox, which contains both the - * DownloadsSummary as well as the "Show All Downloads" button. - */ -const DownloadsFooter = { - - /** - * Focuses the appropriate element within the footer. If the summary - * is visible, focus it. If not, focus the "Show All Downloads" - * button. - */ - focus() { - if (this._showingSummary) { - DownloadsSummary.focus(); - } else { - DownloadsView.downloadsHistory.focus(); - } - }, - - _showingSummary: false, - - /** - * 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) { - if (this._footerNode) { - if (aValue) { - this._footerNode.setAttribute("showingsummary", "true"); - } else { - this._footerNode.removeAttribute("showingsummary"); - } - if (!aValue && this._showingSummary) { - // Make sure the panel's height shrinks when the summary is hidden. - DownloadsPanel.setHeightToFit(); - } - this._showingSummary = aValue; - } - return aValue; - }, - - /** - * Element corresponding to the footer of the downloads panel. - */ - get _footerNode() { - let node = document.getElementById("downloadsFooter"); - if (!node) { - return null; - } - delete this._footerNode; - return this._footerNode = node; - } -}; - -XPCOMUtils.defineConstant(this, "DownloadsFooter", DownloadsFooter); - - -//////////////////////////////////////////////////////////////////////////////// -//// DownloadsBlockedSubview - -/** - * Manages the blocked subview that slides in when you click a blocked download. - */ -const DownloadsBlockedSubview = { - - get subview() { - let subview = document.getElementById("downloadsPanel-blockedSubview"); - delete this.subview; - return this.subview = subview; - }, - - /** - * Elements in the subview. - */ - get elements() { - let idSuffixes = [ - "title", - "details1", - "details2", - "openButton", - "deleteButton", - ]; - let elements = idSuffixes.reduce((memo, s) => { - memo[s] = document.getElementById("downloadsPanel-blockedSubview-" + s); - return memo; - }, {}); - delete this.elements; - return this.elements = elements; - }, - - /** - * The multiview that contains both the main view and the subview. - */ - get view() { - let view = document.getElementById("downloadsPanel-multiView"); - delete this.view; - return this.view = view; - }, - - /** - * The blocked-download richlistitem element that was clicked to show the - * subview. If the subview is not showing, this is undefined. - */ - element: undefined, - - /** - * Slides in the blocked subview. - * - * @param element - * The blocked-download richlistitem element that was clicked. - * @param title - * The title to show in the subview. - * @param details - * An array of strings with information about the block. - */ - toggle(element, title, details) { - if (this.view.showingSubView) { - this.hide(); - return; - } - - this.element = element; - element.setAttribute("showingsubview", "true"); - DownloadsView.subViewOpen = true; - DownloadsViewController.updateCommands(); - - let e = this.elements; - let s = DownloadsCommon.strings; - e.title.textContent = title; - e.details1.textContent = details[0]; - e.details2.textContent = details[1]; - e.openButton.label = s.unblockButtonOpen; - e.deleteButton.label = s.unblockButtonConfirmBlock; - - let verdict = element.getAttribute("verdict"); - this.subview.setAttribute("verdict", verdict); - this.subview.addEventListener("ViewHiding", this); - - this.view.showSubView(this.subview.id); - - // Without this, the mainView is more narrow than the panel once all - // downloads are removed from the panel. - document.getElementById("downloadsPanel-mainView").style.minWidth = - window.getComputedStyle(this.view).width; - }, - - handleEvent(event) { - switch (event.type) { - case "ViewHiding": - this.subview.removeEventListener(event.type, this); - this.element.removeAttribute("showingsubview"); - DownloadsView.subViewOpen = false; - delete this.element; - break; - default: - DownloadsCommon.log("Unhandled DownloadsBlockedSubview event: " + - event.type); - break; - } - }, - - /** - * Slides out the blocked subview and shows the main view. - */ - hide() { - this.view.showMainView(); - // The point of this is to focus the proper element in the panel now that - // the main view is showing again. showPanel handles that. - DownloadsPanel.showPanel(); - }, - - /** - * Deletes the download and hides the entire panel. - */ - confirmBlock() { - goDoCommand("cmd_delete"); - DownloadsPanel.hidePanel(); - }, -}; - -XPCOMUtils.defineConstant(this, "DownloadsBlockedSubview", - DownloadsBlockedSubview); diff --git a/browser/components/downloads/content/downloadsOverlay.xul b/browser/components/downloads/content/downloadsOverlay.xul deleted file mode 100644 index 9fe2ee022..000000000 --- a/browser/components/downloads/content/downloadsOverlay.xul +++ /dev/null @@ -1,210 +0,0 @@ -<?xml version="1.0"?> -# -*- Mode: HTML; 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, -# You can obtain one at http://mozilla.org/MPL/2.0/. - -<?xml-stylesheet href="chrome://browser/content/downloads/downloads.css"?> -<?xml-stylesheet href="chrome://browser/skin/downloads/downloads.css"?> - -<!DOCTYPE overlay SYSTEM "chrome://browser/locale/downloads/downloads.dtd"> - -<overlay xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - id="downloadsOverlay"> - - <commandset> - <command id="downloadsCmd_doDefault" - oncommand="goDoCommand('downloadsCmd_doDefault')"/> - <command id="downloadsCmd_pauseResume" - oncommand="goDoCommand('downloadsCmd_pauseResume')"/> - <command id="downloadsCmd_cancel" - oncommand="goDoCommand('downloadsCmd_cancel')"/> - <command id="downloadsCmd_unblock" - oncommand="goDoCommand('downloadsCmd_unblock')"/> - <command id="downloadsCmd_chooseUnblock" - oncommand="goDoCommand('downloadsCmd_chooseUnblock')"/> - <command id="downloadsCmd_unblockAndOpen" - oncommand="goDoCommand('downloadsCmd_unblockAndOpen')"/> - <command id="downloadsCmd_confirmBlock" - oncommand="goDoCommand('downloadsCmd_confirmBlock')"/> - <command id="downloadsCmd_open" - oncommand="goDoCommand('downloadsCmd_open')"/> - <command id="downloadsCmd_show" - oncommand="goDoCommand('downloadsCmd_show')"/> - <command id="downloadsCmd_retry" - oncommand="goDoCommand('downloadsCmd_retry')"/> - <command id="downloadsCmd_openReferrer" - oncommand="goDoCommand('downloadsCmd_openReferrer')"/> - <command id="downloadsCmd_copyLocation" - oncommand="goDoCommand('downloadsCmd_copyLocation')"/> - <command id="downloadsCmd_clearList" - oncommand="goDoCommand('downloadsCmd_clearList')"/> - </commandset> - - <popupset id="mainPopupSet"> - <!-- The panel has level="top" to ensure that it is never hidden by the - taskbar on Windows. See bug 672365. For accessibility to screen - readers, we use a label on the panel instead of the anchor because the - panel can also be displayed without an anchor. --> - <panel id="downloadsPanel" - aria-label="&downloads.title;" - role="group" - type="arrow" - orient="vertical" - level="top" - onpopupshown="DownloadsPanel.onPopupShown(event);" - onpopuphidden="DownloadsPanel.onPopupHidden(event);"> - <!-- The following popup menu should be a child of the panel element, - otherwise flickering may occur when the cursor is moved over the area - of a disabled menu item that overlaps the panel. See bug 492960. --> - <menupopup id="downloadsContextMenu" - onpopupshown="DownloadsView.onContextPopupShown(event);" - onpopuphidden="DownloadsView.onContextPopupHidden(event);" - class="download-state"> - <menuitem command="downloadsCmd_pauseResume" - class="downloadPauseMenuItem" - label="&cmd.pause.label;" - accesskey="&cmd.pause.accesskey;"/> - <menuitem command="downloadsCmd_pauseResume" - class="downloadResumeMenuItem" - label="&cmd.resume.label;" - accesskey="&cmd.resume.accesskey;"/> - <menuitem command="downloadsCmd_cancel" - class="downloadCancelMenuItem" - label="&cmd.cancel.label;" - accesskey="&cmd.cancel.accesskey;"/> - <menuitem command="downloadsCmd_unblock" - class="downloadUnblockMenuItem" - label="&cmd.unblock2.label;" - accesskey="&cmd.unblock2.accesskey;"/> - <menuitem command="cmd_delete" - class="downloadRemoveFromHistoryMenuItem" - label="&cmd.removeFromHistory.label;" - accesskey="&cmd.removeFromHistory.accesskey;"/> - <menuitem command="downloadsCmd_show" - class="downloadShowMenuItem" -#ifdef XP_MACOSX - label="&cmd.showMac.label;" - accesskey="&cmd.showMac.accesskey;" -#else - label="&cmd.show.label;" - accesskey="&cmd.show.accesskey;" -#endif - /> - - <menuseparator class="downloadCommandsSeparator"/> - - <menuitem command="downloadsCmd_openReferrer" - label="&cmd.goToDownloadPage.label;" - accesskey="&cmd.goToDownloadPage.accesskey;"/> - <menuitem command="downloadsCmd_copyLocation" - label="&cmd.copyDownloadLink.label;" - accesskey="&cmd.copyDownloadLink.accesskey;"/> - - <menuseparator/> - - <menuitem command="downloadsCmd_clearList" - label="&cmd.clearList2.label;" - accesskey="&cmd.clearList2.accesskey;"/> - </menupopup> - - <panelmultiview id="downloadsPanel-multiView" - mainViewId="downloadsPanel-mainView" - align="stretch"> - - <panelview id="downloadsPanel-mainView" - flex="1" - align="stretch"> - <richlistbox id="downloadsListBox" - context="downloadsContextMenu" - onmouseover="DownloadsView.onDownloadMouseOver(event);" - onmouseout="DownloadsView.onDownloadMouseOut(event);" - oncontextmenu="DownloadsView.onDownloadContextMenu(event);" - ondragstart="DownloadsView.onDownloadDragStart(event);"/> - <description id="emptyDownloads" - mousethrough="always"> - &downloadsPanelEmpty.label; - </description> - <spacer flex="1"/> - <vbox id="downloadsFooter" - class="downloadsPanelFooter"> - <stack> - <hbox id="downloadsSummary" - align="center" - orient="horizontal" - onkeydown="DownloadsSummary.onKeyDown(event);" - onclick="DownloadsSummary.onClick(event);"> - <image class="downloadTypeIcon" /> - <vbox pack="center" - class="downloadContainer" - style="width: &downloadDetails.width;"> - <description id="downloadsSummaryDescription" - style="min-width: &downloadsSummary.minWidth2;"/> - <progressmeter id="downloadsSummaryProgress" - class="downloadProgress" - min="0" - max="100" - mode="normal" /> - <description id="downloadsSummaryDetails" - crop="end"/> - </vbox> - </hbox> - <hbox id="downloadsFooterButtons"> - <button id="downloadsHistory" - class="downloadsPanelFooterButton" - label="&downloadsHistory.label;" - accesskey="&downloadsHistory.accesskey;" - flex="1" - oncommand="DownloadsPanel.showDownloadsHistory();"/> - <toolbarseparator id="downloadsFooterButtonsSplitter" - class="downloadsDropmarkerSplitter"/> - <button id="downloadsFooterDropmarker" - class="downloadsPanelFooterButton downloadsDropmarker" - type="menu"> - <menupopup id="downloadSubPanel" - onpopupshowing="DownloadsPanel.onFooterPopupShowing(event);" - onpopuphidden="DownloadsPanel.onFooterPopupHidden(event);" - position="after_end"> - <menuitem id="downloadsDropdownItemClearList" - command="downloadsCmd_clearList" - label="&cmd.clearList2.label;"/> - <menuitem id="downloadsDropdownItemOpenDownloadsFolder" - oncommand="DownloadsPanel.openDownloadsFolder();" - label="&openDownloadsFolder.label;"/> - </menupopup> - </button> - </hbox> - </stack> - </vbox> - </panelview> - - <panelview id="downloadsPanel-blockedSubview" - orient="vertical" - flex="1"> - <description id="downloadsPanel-blockedSubview-title"/> - <description id="downloadsPanel-blockedSubview-details1"/> - <description id="downloadsPanel-blockedSubview-details2"/> - <spacer flex="1"/> - <hbox id="downloadsPanel-blockedSubview-buttons" - class="downloadsPanelFooter" - align="stretch"> - <button id="downloadsPanel-blockedSubview-openButton" - class="downloadsPanelFooterButton" - command="downloadsCmd_unblockAndOpen" - flex="1"/> - <toolbarseparator/> - <button id="downloadsPanel-blockedSubview-deleteButton" - class="downloadsPanelFooterButton" - oncommand="DownloadsBlockedSubview.confirmBlock();" - default="true" - flex="1"/> - </hbox> - </panelview> - - </panelmultiview> - - </panel> - </popupset> -</overlay> diff --git a/browser/components/downloads/content/indicator.js b/browser/components/downloads/content/indicator.js deleted file mode 100644 index 4c22a6e5d..000000000 --- a/browser/components/downloads/content/indicator.js +++ /dev/null @@ -1,606 +0,0 @@ -/* -*- indent-tabs-mode: nil; js-indent-level: 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, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -/** - * Handles the indicator that displays the progress of ongoing downloads, which - * is also used as the anchor for the downloads panel. - * - * This module includes the following constructors and global objects: - * - * DownloadsButton - * Main entry point for the downloads indicator. Depending on how the toolbars - * have been customized, this object determines if we should show a fully - * functional indicator, a placeholder used during customization and in the - * customization palette, or a neutral view as a temporary anchor for the - * downloads panel. - * - * DownloadsIndicatorView - * Builds and updates the actual downloads status widget, responding to changes - * in the global status data, or provides a neutral view if the indicator is - * removed from the toolbars and only used as a temporary anchor. In addition, - * handles the user interaction events raised by the widget. - */ - -"use strict"; - -//////////////////////////////////////////////////////////////////////////////// -//// DownloadsButton - -/** - * Main entry point for the downloads indicator. Depending on how the toolbars - * have been customized, this object determines if we should show a fully - * functional indicator, a placeholder used during customization and in the - * customization palette, or a neutral view as a temporary anchor for the - * downloads panel. - */ -const DownloadsButton = { - /** - * Location of the indicator overlay. - */ - get kIndicatorOverlay() { - return "chrome://browser/content/downloads/indicatorOverlay.xul"; - }, - - /** - * Returns a reference to the downloads button position placeholder, or null - * if not available because it has been removed from the toolbars. - */ - get _placeholder() { - return document.getElementById("downloads-button"); - }, - - /** - * This function is called asynchronously just after window initialization. - * - * NOTE: This function should limit the input/output it performs to improve - * startup time. - */ - initializeIndicator() { - DownloadsIndicatorView.ensureInitialized(); - }, - - /** - * Indicates whether toolbar customization is in progress. - */ - _customizing: false, - - /** - * This function is called when toolbar customization starts. - * - * During customization, we never show the actual download progress indication - * or the event notifications, but we show a neutral placeholder. The neutral - * placeholder is an ordinary button defined in the browser window that can be - * moved freely between the toolbars and the customization palette. - */ - customizeStart() { - // Prevent the indicator from being displayed as a temporary anchor - // during customization, even if requested using the getAnchor method. - this._customizing = true; - this._anchorRequested = false; - }, - - /** - * This function is called when toolbar customization ends. - */ - customizeDone() { - this._customizing = false; - DownloadsIndicatorView.afterCustomize(); - }, - - /** - * Determines the position where the indicator should appear, and moves its - * associated element to the new position. - * - * @return Anchor element, or null if the indicator is not visible. - */ - _getAnchorInternal() { - let indicator = DownloadsIndicatorView.indicator; - if (!indicator) { - // Exit now if the indicator overlay isn't loaded yet, or if the button - // is not in the document. - return null; - } - - indicator.open = this._anchorRequested; - - let widget = CustomizableUI.getWidget("downloads-button") - .forWindow(window); - // Determine if the indicator is located on an invisible toolbar. - if (!isElementVisible(indicator.parentNode) && !widget.overflowed) { - return null; - } - - return DownloadsIndicatorView.indicatorAnchor; - }, - - /** - * Checks whether the indicator is, or will soon be visible in the browser - * window. - * - * @param aCallback - * Called once the indicator overlay has loaded. Gets a boolean - * argument representing the indicator visibility. - */ - checkIsVisible(aCallback) { - DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay, () => { - if (!this._placeholder) { - aCallback(false); - } else { - let element = DownloadsIndicatorView.indicator || this._placeholder; - aCallback(isElementVisible(element.parentNode)); - } - }); - }, - - /** - * Indicates whether we should try and show the indicator temporarily as an - * anchor for the panel, even if the indicator would be hidden by default. - */ - _anchorRequested: false, - - /** - * Ensures that there is an anchor available for the panel. - * - * @param aCallback - * Called when the anchor is available, passing the element where the - * panel should be anchored, or null if an anchor is not available (for - * example because both the tab bar and the navigation bar are hidden). - */ - getAnchor(aCallback) { - // Do not allow anchoring the panel to the element while customizing. - if (this._customizing) { - aCallback(null); - return; - } - - DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay, () => { - this._anchorRequested = true; - aCallback(this._getAnchorInternal()); - }); - }, - - /** - * Allows the temporary anchor to be hidden. - */ - releaseAnchor() { - this._anchorRequested = false; - this._getAnchorInternal(); - }, - - get _tabsToolbar() { - delete this._tabsToolbar; - return this._tabsToolbar = document.getElementById("TabsToolbar"); - }, - - get _navBar() { - delete this._navBar; - return this._navBar = document.getElementById("nav-bar"); - } -}; - -Object.defineProperty(this, "DownloadsButton", { - value: DownloadsButton, - enumerable: true, - writable: false -}); - -//////////////////////////////////////////////////////////////////////////////// -//// DownloadsIndicatorView - -/** - * Builds and updates the actual downloads status widget, responding to changes - * in the global status data, or provides a neutral view if the indicator is - * removed from the toolbars and only used as a temporary anchor. In addition, - * handles the user interaction events raised by the widget. - */ -const DownloadsIndicatorView = { - /** - * True when the view is connected with the underlying downloads data. - */ - _initialized: false, - - /** - * True when the user interface elements required to display the indicator - * have finished loading in the browser window, and can be referenced. - */ - _operational: false, - - /** - * Prepares the downloads indicator to be displayed. - */ - ensureInitialized() { - if (this._initialized) { - return; - } - this._initialized = true; - - window.addEventListener("unload", this.onWindowUnload, false); - DownloadsCommon.getIndicatorData(window).addView(this); - }, - - /** - * Frees the internal resources related to the indicator. - */ - ensureTerminated() { - if (!this._initialized) { - return; - } - this._initialized = false; - - window.removeEventListener("unload", this.onWindowUnload, false); - DownloadsCommon.getIndicatorData(window).removeView(this); - - // Reset the view properties, so that a neutral indicator is displayed if we - // are visible only temporarily as an anchor. - this.counter = ""; - this.percentComplete = 0; - this.paused = false; - this.attention = DownloadsCommon.ATTENTION_NONE; - }, - - /** - * Ensures that the user interface elements required to display the indicator - * are loaded, then invokes the given callback. - */ - _ensureOperational(aCallback) { - if (this._operational) { - if (aCallback) { - aCallback(); - } - return; - } - - // If we don't have a _placeholder, there's no chance that the overlay - // will load correctly: bail (and don't set _operational to true!) - if (!DownloadsButton._placeholder) { - return; - } - - DownloadsOverlayLoader.ensureOverlayLoaded( - DownloadsButton.kIndicatorOverlay, - () => { - this._operational = true; - - // If the view is initialized, we need to update the elements now that - // they are finally available in the document. - if (this._initialized) { - DownloadsCommon.getIndicatorData(window).refreshView(this); - } - - if (aCallback) { - aCallback(); - } - }); - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Direct control functions - - /** - * Set while we are waiting for a notification to fade out. - */ - _notificationTimeout: null, - - /** - * Check if the panel containing aNode is open. - * @param aNode - * the node whose panel we're interested in. - */ - _isAncestorPanelOpen(aNode) { - while (aNode && aNode.localName != "panel") { - aNode = aNode.parentNode; - } - return aNode && aNode.state == "open"; - }, - - /** - * If the status indicator is visible in its assigned position, shows for a - * brief time a visual notification of a relevant event, like a new download. - * - * @param aType - * Set to "start" for new downloads, "finish" for completed downloads. - */ - showEventNotification(aType) { - if (!this._initialized) { - return; - } - - if (!DownloadsCommon.animateNotifications) { - return; - } - - // No need to show visual notification if the panel is visible. - if (DownloadsPanel.isPanelShowing) { - return; - } - - let anchor = DownloadsButton._placeholder; - let widgetGroup = CustomizableUI.getWidget("downloads-button"); - let widget = widgetGroup.forWindow(window); - if (widget.overflowed || widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) { - if (anchor && this._isAncestorPanelOpen(anchor)) { - // If the containing panel is open, don't do anything, because the - // notification would appear under the open panel. See - // https://bugzilla.mozilla.org/show_bug.cgi?id=984023 - return; - } - - // Otherwise, try to use the anchor of the panel: - anchor = widget.anchor; - } - if (!anchor || !isElementVisible(anchor.parentNode)) { - // Our container isn't visible, so can't show the animation: - return; - } - - if (this._notificationTimeout) { - clearTimeout(this._notificationTimeout); - } - - // The notification element is positioned to show in the same location as - // the downloads button. It's not in the downloads button itself in order to - // be able to anchor the notification elsewhere if required, and to ensure - // the notification isn't clipped by overflow properties of the anchor's - // container. - let notifier = this.notifier; - if (notifier.style.transform == '') { - let anchorRect = anchor.getBoundingClientRect(); - let notifierRect = notifier.getBoundingClientRect(); - let topDiff = anchorRect.top - notifierRect.top; - let leftDiff = anchorRect.left - notifierRect.left; - let heightDiff = anchorRect.height - notifierRect.height; - let widthDiff = anchorRect.width - notifierRect.width; - let translateX = (leftDiff + .5 * widthDiff) + "px"; - let translateY = (topDiff + .5 * heightDiff) + "px"; - notifier.style.transform = "translate(" + translateX + ", " + translateY + ")"; - } - notifier.setAttribute("notification", aType); - this._notificationTimeout = setTimeout(() => { - notifier.removeAttribute("notification"); - notifier.style.transform = ''; - }, 1000); - }, - - ////////////////////////////////////////////////////////////////////////////// - //// Callback functions from DownloadsIndicatorData - - /** - * Indicates whether the indicator should be shown because there are some - * downloads to be displayed. - */ - set hasDownloads(aValue) { - if (this._hasDownloads != aValue || (!this._operational && aValue)) { - this._hasDownloads = aValue; - - // If there is at least one download, ensure that the view elements are - if (aValue) { - this._ensureOperational(); - } - } - return aValue; - }, - get hasDownloads() { - return this._hasDownloads; - }, - _hasDownloads: false, - - /** - * Status text displayed in the indicator. If this is set to an empty value, - * then the small downloads icon is displayed instead of the text. - */ - set counter(aValue) { - if (!this._operational) { - return this._counter; - } - - if (this._counter !== aValue) { - this._counter = aValue; - if (this._counter) - this.indicator.setAttribute("counter", "true"); - else - this.indicator.removeAttribute("counter"); - // We have to set the attribute instead of using the property because the - // XBL binding isn't applied if the element is invisible for any reason. - this._indicatorCounter.setAttribute("value", aValue); - } - return aValue; - }, - _counter: null, - - /** - * Progress indication to display, from 0 to 100, or -1 if unknown. The - * progress bar is hidden if the current progress is unknown and no status - * text is set in the "counter" property. - */ - set percentComplete(aValue) { - if (!this._operational) { - return this._percentComplete; - } - - if (this._percentComplete !== aValue) { - this._percentComplete = aValue; - if (this._percentComplete >= 0) - this.indicator.setAttribute("progress", "true"); - else - this.indicator.removeAttribute("progress"); - // We have to set the attribute instead of using the property because the - // XBL binding isn't applied if the element is invisible for any reason. - this._indicatorProgress.setAttribute("value", Math.max(aValue, 0)); - } - return aValue; - }, - _percentComplete: null, - - /** - * Indicates whether the progress won't advance because of a paused state. - * Setting this property forces a paused progress bar to be displayed, even if - * the current progress information is unavailable. - */ - set paused(aValue) { - if (!this._operational) { - return this._paused; - } - - if (this._paused != aValue) { - this._paused = aValue; - if (this._paused) { - this.indicator.setAttribute("paused", "true") - } else { - this.indicator.removeAttribute("paused"); - } - } - return aValue; - }, - _paused: false, - - /** - * Set when the indicator should draw user attention to itself. - */ - set attention(aValue) { - if (!this._operational) { - return this._attention; - } - - if (this._attention != aValue) { - this._attention = aValue; - - // Check if the downloads button is in the menu panel, to determine which - // button needs to get a badge. - let widgetGroup = CustomizableUI.getWidget("downloads-button"); - let inMenu = widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL; - - if (aValue == DownloadsCommon.ATTENTION_NONE) { - this.indicator.removeAttribute("attention"); - if (inMenu) { - gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD); - } - } else { - this.indicator.setAttribute("attention", aValue); - if (inMenu) { - let badgeClass = "download-" + aValue; - gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD, badgeClass); - } - } - } - return aValue; - }, - _attention: DownloadsCommon.ATTENTION_NONE, - - ////////////////////////////////////////////////////////////////////////////// - //// User interface event functions - - onWindowUnload() { - // This function is registered as an event listener, we can't use "this". - DownloadsIndicatorView.ensureTerminated(); - }, - - onCommand(aEvent) { - // If the downloads button is in the menu panel, open the Library - let widgetGroup = CustomizableUI.getWidget("downloads-button"); - if (widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) { - DownloadsPanel.showDownloadsHistory(); - } else { - DownloadsPanel.showPanel(); - } - - aEvent.stopPropagation(); - }, - - onDragOver(aEvent) { - browserDragAndDrop.dragOver(aEvent); - }, - - 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)) - return; - - let links = browserDragAndDrop.dropLinks(aEvent); - if (!links.length) - return; - let sourceDoc = dt.mozSourceNode ? dt.mozSourceNode.ownerDocument : document; - let handled = false; - for (let link of links) { - if (link.url.startsWith("about:")) - continue; - saveURL(link.url, link.name, null, true, true, null, sourceDoc); - handled = true; - } - if (handled) { - aEvent.preventDefault(); - } - }, - - _indicator: null, - __indicatorCounter: null, - __indicatorProgress: null, - - /** - * Returns a reference to the main indicator element, or null if the element - * is not present in the browser window yet. - */ - get indicator() { - if (this._indicator) { - return this._indicator; - } - - let indicator = document.getElementById("downloads-button"); - if (!indicator || indicator.getAttribute("indicator") != "true") { - return null; - } - - return this._indicator = indicator; - }, - - get indicatorAnchor() { - let widget = CustomizableUI.getWidget("downloads-button") - .forWindow(window); - if (widget.overflowed) { - return widget.anchor; - } - return document.getElementById("downloads-indicator-anchor"); - }, - - get _indicatorCounter() { - return this.__indicatorCounter || - (this.__indicatorCounter = document.getElementById("downloads-indicator-counter")); - }, - - get _indicatorProgress() { - return this.__indicatorProgress || - (this.__indicatorProgress = document.getElementById("downloads-indicator-progress")); - }, - - get notifier() { - return this._notifier || - (this._notifier = document.getElementById("downloads-notification-anchor")); - }, - - _onCustomizedAway() { - this._indicator = null; - this.__indicatorCounter = null; - this.__indicatorProgress = null; - }, - - afterCustomize() { - // If the cached indicator is not the one currently in the document, - // invalidate our references - if (this._indicator != document.getElementById("downloads-button")) { - this._onCustomizedAway(); - this._operational = false; - this.ensureTerminated(); - this.ensureInitialized(); - } - }, -}; - -Object.defineProperty(this, "DownloadsIndicatorView", { - value: DownloadsIndicatorView, - enumerable: true, - writable: false -}); diff --git a/browser/components/downloads/content/indicatorOverlay.xul b/browser/components/downloads/content/indicatorOverlay.xul deleted file mode 100644 index 07987c88c..000000000 --- a/browser/components/downloads/content/indicatorOverlay.xul +++ /dev/null @@ -1,36 +0,0 @@ -<?xml version="1.0"?> -<!-- -*- Mode: HTML; 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, - - You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<!DOCTYPE overlay [ - <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd" > - %browserDTD; -]> - -<overlay xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - id="indicatorOverlay"> - - <!-- We dynamically add the stack with the progress meter and notification icon, - originally loaded lazily because of performance reasons, to the existing - downloads-button. --> - <toolbarbutton id="downloads-button" indicator="true"> - <!-- The panel's anchor area is smaller than the outer button, but must - always be visible and must not move or resize when the indicator - state changes, otherwise the panel could change its position or lose - its arrow unexpectedly. --> - <stack id="downloads-indicator-anchor" - consumeanchor="downloads-button"> - <vbox id="downloads-indicator-progress-area" pack="center"> - <description id="downloads-indicator-counter"/> - <progressmeter id="downloads-indicator-progress" class="plain" - min="0" max="100"/> - </vbox> - <vbox id="downloads-indicator-icon"/> - </stack> - </toolbarbutton> -</overlay> diff --git a/browser/components/downloads/jar.mn b/browser/components/downloads/jar.mn deleted file mode 100644 index 2663454fa..000000000 --- a/browser/components/downloads/jar.mn +++ /dev/null @@ -1,16 +0,0 @@ -# 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/. - -browser.jar: -* content/browser/downloads/download.xml (content/download.xml) - content/browser/downloads/downloads.css (content/downloads.css) - content/browser/downloads/downloads.js (content/downloads.js) -* content/browser/downloads/downloadsOverlay.xul (content/downloadsOverlay.xul) - content/browser/downloads/indicator.js (content/indicator.js) - content/browser/downloads/indicatorOverlay.xul (content/indicatorOverlay.xul) -* content/browser/downloads/allDownloadsViewOverlay.xul (content/allDownloadsViewOverlay.xul) - content/browser/downloads/allDownloadsViewOverlay.js (content/allDownloadsViewOverlay.js) -* content/browser/downloads/contentAreaDownloadsView.xul (content/contentAreaDownloadsView.xul) - content/browser/downloads/contentAreaDownloadsView.js (content/contentAreaDownloadsView.js) - content/browser/downloads/contentAreaDownloadsView.css (content/contentAreaDownloadsView.css) diff --git a/browser/components/downloads/moz.build b/browser/components/downloads/moz.build deleted file mode 100644 index 241354619..000000000 --- a/browser/components/downloads/moz.build +++ /dev/null @@ -1,13 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# 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/. - -JAR_MANIFESTS += ['jar.mn'] - -EXTRA_JS_MODULES += [ - 'DownloadsCommon.jsm', - 'DownloadsTaskbar.jsm', - 'DownloadsViewUI.jsm', -] |