forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1687 lines
		
	
	
	
		
			50 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1687 lines
		
	
	
	
		
			50 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- 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/. */
 | |
| /* eslint-env mozilla/browser-window */
 | |
| 
 | |
| /**
 | |
|  * 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.
 | |
|  *
 | |
|  * 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.
 | |
|  */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| var { XPCOMUtils } = ChromeUtils.importESModule(
 | |
|   "resource://gre/modules/XPCOMUtils.sys.mjs"
 | |
| );
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(this, {
 | |
|   DownloadsViewUI: "resource:///modules/DownloadsViewUI.sys.mjs",
 | |
|   FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
 | |
|   NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
 | |
|   PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
 | |
| });
 | |
| 
 | |
| const { Integration } = ChromeUtils.importESModule(
 | |
|   "resource://gre/modules/Integration.sys.mjs"
 | |
| );
 | |
| 
 | |
| /* global DownloadIntegration */
 | |
| Integration.downloads.defineESModuleGetter(
 | |
|   this,
 | |
|   "DownloadIntegration",
 | |
|   "resource://gre/modules/DownloadIntegration.sys.mjs"
 | |
| );
 | |
| 
 | |
| // DownloadsPanel
 | |
| 
 | |
| /**
 | |
|  * Main entry point for the downloads panel interface.
 | |
|  */
 | |
| var DownloadsPanel = {
 | |
|   // Initialization and termination
 | |
| 
 | |
|   /**
 | |
|    * Timeout that re-enables previously disabled download items in the downloads panel
 | |
|    * after some time has passed.
 | |
|    */
 | |
|   _delayTimeout: null,
 | |
| 
 | |
|   /** The panel is not linked to downloads data yet. */
 | |
|   _initialized: false,
 | |
| 
 | |
|   /** The panel will be shown as soon as data is available. */
 | |
|   _waitingDataForOpen: false,
 | |
| 
 | |
|   /**
 | |
|    * 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.
 | |
|    */
 | |
|   initialize() {
 | |
|     DownloadsCommon.log(
 | |
|       "Attempting to initialize DownloadsPanel for a window."
 | |
|     );
 | |
| 
 | |
|     if (DownloadIntegration.downloadSpamProtection) {
 | |
|       DownloadIntegration.downloadSpamProtection.register(
 | |
|         DownloadsView,
 | |
|         window
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (this._initialized) {
 | |
|       DownloadsCommon.log("DownloadsPanel is already initialized.");
 | |
|       return;
 | |
|     }
 | |
|     this._initialized = true;
 | |
| 
 | |
|     window.addEventListener("unload", this.onWindowUnload);
 | |
| 
 | |
|     // 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.
 | |
| 
 | |
|     this.panel.hidden = false;
 | |
|     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.");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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._initialized) {
 | |
|       DownloadsCommon.log(
 | |
|         "DownloadsPanel was never initialized. Nothing to do."
 | |
|       );
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     window.removeEventListener("unload", this.onWindowUnload);
 | |
| 
 | |
|     // 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();
 | |
| 
 | |
|     if (DownloadIntegration.downloadSpamProtection) {
 | |
|       DownloadIntegration.downloadSpamProtection.unregister(window);
 | |
|     }
 | |
| 
 | |
|     this._initialized = false;
 | |
| 
 | |
|     DownloadsSummary.active = false;
 | |
|     DownloadsCommon.log("DownloadsPanel terminated.");
 | |
|   },
 | |
| 
 | |
|   // Panel interface
 | |
| 
 | |
|   /**
 | |
|    * Main panel element in the browser window.
 | |
|    */
 | |
|   get panel() {
 | |
|     delete this.panel;
 | |
|     return (this.panel = document.getElementById("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(openedManually = false, isKeyPress = false) {
 | |
|     Services.telemetry.scalarAdd("downloads.panel_shown", 1);
 | |
|     DownloadsCommon.log("Opening the downloads panel.");
 | |
| 
 | |
|     this._openedManually = openedManually;
 | |
|     this._preventFocusRing = !openedManually || !isKeyPress;
 | |
| 
 | |
|     if (this.isPanelShowing) {
 | |
|       DownloadsCommon.log("Panel is already showing - focusing instead.");
 | |
|       this._focusPanel();
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // As a belt-and-suspenders check, ensure the button is not hidden.
 | |
|     DownloadsButton.unhide();
 | |
| 
 | |
|     this.initialize();
 | |
|     // 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._waitingDataForOpen = true;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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;
 | |
|     }
 | |
| 
 | |
|     PanelMultiView.hidePopup(this.panel);
 | |
|     DownloadsCommon.log("Downloads panel is now closed.");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Indicates whether the panel is showing.
 | |
|    * @note this includes the hiding state.
 | |
|    */
 | |
|   get isPanelShowing() {
 | |
|     return this._waitingDataForOpen || this.panel.state != "closed";
 | |
|   },
 | |
| 
 | |
|   handleEvent(aEvent) {
 | |
|     switch (aEvent.type) {
 | |
|       case "mousemove":
 | |
|         if (
 | |
|           !DownloadsView.contextMenuOpen &&
 | |
|           !DownloadsView.subViewOpen &&
 | |
|           this.panel.contains(document.activeElement)
 | |
|         ) {
 | |
|           // Let mouse movement remove focus rings and reset focus in the panel.
 | |
|           // This behavior is copied from PanelMultiView.
 | |
|           document.activeElement.blur();
 | |
|           DownloadsView.richListBox.removeAttribute("force-focus-visible");
 | |
|           this._preventFocusRing = true;
 | |
|           this._focusPanel();
 | |
|         }
 | |
|         break;
 | |
|       case "keydown":
 | |
|         this._onKeyDown(aEvent);
 | |
|         break;
 | |
|       case "keypress":
 | |
|         this._onKeyPress(aEvent);
 | |
|         break;
 | |
|       case "focus":
 | |
|       case "select":
 | |
|         this._onSelect(aEvent);
 | |
|         break;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   // 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.");
 | |
| 
 | |
|     // Since at most one popup is open at any given time, we can set globally.
 | |
|     DownloadsCommon.getIndicatorData(window).attentionSuppressed |=
 | |
|       DownloadsCommon.SUPPRESS_PANEL_OPEN;
 | |
| 
 | |
|     // Ensure that the first item is selected when the panel is focused.
 | |
|     if (DownloadsView.richListBox.itemCount > 0) {
 | |
|       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.");
 | |
| 
 | |
|     if (this._delayTimeout) {
 | |
|       DownloadsView.richListBox.removeAttribute("disabled");
 | |
|       clearTimeout(this._delayTimeout);
 | |
|       this._stopWatchingForSpammyDownloadActivation();
 | |
|       this._delayTimeout = null;
 | |
|     }
 | |
| 
 | |
|     DownloadsView.richListBox.removeAttribute("force-focus-visible");
 | |
| 
 | |
|     // Since at most one popup is open at any given time, we can set globally.
 | |
|     DownloadsCommon.getIndicatorData(window).attentionSuppressed &=
 | |
|       ~DownloadsCommon.SUPPRESS_PANEL_OPEN;
 | |
| 
 | |
|     // Allow the anchor to be hidden.
 | |
|     DownloadsButton.releaseAnchor();
 | |
|   },
 | |
| 
 | |
|   // 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();
 | |
|   },
 | |
| 
 | |
|   // 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);
 | |
|     // Handle keypress to be able to preventDefault() events before they reach
 | |
|     // the richlistbox, for keyboard navigation.
 | |
|     this.panel.addEventListener("keypress", this);
 | |
|     this.panel.addEventListener("mousemove", this);
 | |
|     DownloadsView.richListBox.addEventListener("focus", this);
 | |
|     DownloadsView.richListBox.addEventListener("select", this);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Unattach event listeners that were added in _attachEventListeners. This
 | |
|    * is called automatically on panel termination.
 | |
|    */
 | |
|   _unattachEventListeners() {
 | |
|     this.panel.removeEventListener("keydown", this);
 | |
|     this.panel.removeEventListener("keypress", this);
 | |
|     this.panel.removeEventListener("mousemove", this);
 | |
|     DownloadsView.richListBox.removeEventListener("focus", this);
 | |
|     DownloadsView.richListBox.removeEventListener("select", this);
 | |
|   },
 | |
| 
 | |
|   _onKeyPress(aEvent) {
 | |
|     // Handle unmodified keys only.
 | |
|     if (aEvent.altKey || aEvent.ctrlKey || aEvent.shiftKey || aEvent.metaKey) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Pass keypress events to the richlistbox view when it's focused.
 | |
|     if (document.activeElement === DownloadsView.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 (DownloadsView.richListBox.hasAttribute("disabled")) {
 | |
|       this._handlePotentiallySpammyDownloadActivation(aEvent);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let richListBox = DownloadsView.richListBox;
 | |
| 
 | |
|     // If the user has pressed the up or down cursor key, force-enable focus
 | |
|     // indicators for the richlistbox.  :focus-visible doesn't work in this case
 | |
|     // because the the focused element may not change here if the richlistbox
 | |
|     // already had focus.  The force-focus-visible attribute will be removed
 | |
|     // again if the user moves the mouse on the panel or if the panel is closed.
 | |
|     if (
 | |
|       aEvent.keyCode == aEvent.DOM_VK_UP ||
 | |
|       aEvent.keyCode == aEvent.DOM_VK_DOWN
 | |
|     ) {
 | |
|       richListBox.setAttribute("force-focus-visible", "true");
 | |
|     }
 | |
| 
 | |
|     // 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 == aEvent.DOM_VK_UP && richListBox.firstElementChild) {
 | |
|       if (
 | |
|         document
 | |
|           .getElementById("downloadsFooter")
 | |
|           .contains(document.activeElement)
 | |
|       ) {
 | |
|         richListBox.selectedItem = richListBox.lastElementChild;
 | |
|         richListBox.focus();
 | |
|         aEvent.preventDefault();
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (aEvent.keyCode == aEvent.DOM_VK_DOWN) {
 | |
|       // If the last element in the list is selected, or the footer is already
 | |
|       // focused, focus the footer.
 | |
|       if (
 | |
|         DownloadsView.canChangeSelectedItem &&
 | |
|         (richListBox.selectedItem === richListBox.lastElementChild ||
 | |
|           document
 | |
|             .getElementById("downloadsFooter")
 | |
|             .contains(document.activeElement))
 | |
|       ) {
 | |
|         richListBox.selectedIndex = -1;
 | |
|         DownloadsFooter.focus();
 | |
|         aEvent.preventDefault();
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     let pasting =
 | |
|       aEvent.keyCode == aEvent.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/plain"];
 | |
|     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) {}
 | |
|   },
 | |
| 
 | |
|   _onSelect() {
 | |
|     let richlistbox = DownloadsView.richListBox;
 | |
|     richlistbox.itemChildren.forEach(item => {
 | |
|       let button = item.querySelector("button");
 | |
|       if (item.selected) {
 | |
|         button.removeAttribute("tabindex");
 | |
|       } else {
 | |
|         button.setAttribute("tabindex", -1);
 | |
|       }
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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.panel.state != "open") {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (
 | |
|       document.activeElement &&
 | |
|       (this.panel.contains(document.activeElement) ||
 | |
|         this.panel.shadowRoot.contains(document.activeElement))
 | |
|     ) {
 | |
|       return;
 | |
|     }
 | |
|     let focusOptions = {};
 | |
|     if (this._preventFocusRing) {
 | |
|       focusOptions.focusVisible = false;
 | |
|     }
 | |
|     if (DownloadsView.richListBox.itemCount > 0) {
 | |
|       if (DownloadsView.canChangeSelectedItem) {
 | |
|         DownloadsView.richListBox.selectedIndex = 0;
 | |
|       }
 | |
|       DownloadsView.richListBox.focus(focusOptions);
 | |
|     } else {
 | |
|       DownloadsFooter.focus(focusOptions);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _delayPopupItems() {
 | |
|     DownloadsView.richListBox.setAttribute("disabled", true);
 | |
|     this._startWatchingForSpammyDownloadActivation();
 | |
| 
 | |
|     this._refreshDelayTimer();
 | |
|   },
 | |
| 
 | |
|   _refreshDelayTimer() {
 | |
|     // If timeout already exists, overwrite it to avoid multiple timeouts.
 | |
|     if (this._delayTimeout) {
 | |
|       clearTimeout(this._delayTimeout);
 | |
|     }
 | |
| 
 | |
|     let delay = Services.prefs.getIntPref("security.dialog_enable_delay");
 | |
|     this._delayTimeout = setTimeout(() => {
 | |
|       DownloadsView.richListBox.removeAttribute("disabled");
 | |
|       this._stopWatchingForSpammyDownloadActivation();
 | |
|       this._focusPanel();
 | |
|       this._delayTimeout = null;
 | |
|     }, delay);
 | |
|   },
 | |
| 
 | |
|   _startWatchingForSpammyDownloadActivation() {
 | |
|     Services.els.addSystemEventListener(window, "keydown", this, true);
 | |
|   },
 | |
| 
 | |
|   _lastBeepTime: 0,
 | |
|   _handlePotentiallySpammyDownloadActivation(aEvent) {
 | |
|     if (aEvent.key == "Enter" || aEvent.key == " ") {
 | |
|       // Throttle our beeping to a maximum of once per second, otherwise it
 | |
|       // appears on Win10 that beeps never make it through at all.
 | |
|       if (Date.now() - this._lastBeepTime > 1000) {
 | |
|         Cc["@mozilla.org/sound;1"].getService(Ci.nsISound).beep();
 | |
|         this._lastBeepTime = Date.now();
 | |
|       }
 | |
| 
 | |
|       this._refreshDelayTimer();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _stopWatchingForSpammyDownloadActivation() {
 | |
|     Services.els.removeSystemEventListener(window, "keydown", this, true);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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._waitingDataForOpen || DownloadsView.loading) {
 | |
|       return;
 | |
|     }
 | |
|     this._waitingDataForOpen = false;
 | |
| 
 | |
|     // 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 == window.STATE_MINIMIZED) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // 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.
 | |
|     let anchor = DownloadsButton.getAnchor();
 | |
| 
 | |
|     if (!anchor) {
 | |
|       DownloadsCommon.error("Downloads button cannot be found.");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let onBookmarksToolbar = !!anchor.closest("#PersonalToolbar");
 | |
|     this.panel.classList.toggle("bookmarks-toolbar", onBookmarksToolbar);
 | |
| 
 | |
|     // 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(console.error);
 | |
|     }
 | |
| 
 | |
|     DownloadsCommon.log("Opening downloads panel popup.");
 | |
| 
 | |
|     // 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(() => {
 | |
|       PanelMultiView.openPopup(
 | |
|         this.panel,
 | |
|         anchor,
 | |
|         "bottomright topright",
 | |
|         0,
 | |
|         0,
 | |
|         false,
 | |
|         null
 | |
|       ).then(() => {
 | |
|         if (!this._openedManually) {
 | |
|           this._delayPopupItems();
 | |
|         }
 | |
|       }, console.error);
 | |
|     }, 0);
 | |
|   },
 | |
| };
 | |
| 
 | |
| XPCOMUtils.defineConstant(this, "DownloadsPanel", DownloadsPanel);
 | |
| 
 | |
| // 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.
 | |
|  */
 | |
| var 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 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.
 | |
|    */
 | |
|   onDownloadBatchStarting() {
 | |
|     DownloadsCommon.log("onDownloadBatchStarting called for DownloadsView.");
 | |
|     this.loading = true;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Called after data loading finished.
 | |
|    */
 | |
|   onDownloadBatchEnded() {
 | |
|     DownloadsCommon.log("onDownloadBatchEnded 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.
 | |
|    */
 | |
|   onDownloadAdded(download) {
 | |
|     DownloadsCommon.log("A new download data item was added");
 | |
| 
 | |
|     this._downloads.unshift(download);
 | |
| 
 | |
|     // The newly added item is visible in the panel and we must add the
 | |
|     // corresponding element. 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._addViewItem(download, true);
 | |
|     if (this._downloads.length > this.kItemCountLimit) {
 | |
|       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();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   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();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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.createXULElement("richlistitem");
 | |
|     element.setAttribute("align", "center");
 | |
| 
 | |
|     let viewItem = new DownloadsViewItem(download, element);
 | |
|     this._visibleViewItems.set(download, viewItem);
 | |
|     this._itemsForElements.set(element, viewItem);
 | |
|     if (aNewest) {
 | |
|       this.richListBox.insertBefore(
 | |
|         element,
 | |
|         this.richListBox.firstElementChild
 | |
|       );
 | |
|     } else {
 | |
|       this.richListBox.appendChild(element);
 | |
|     }
 | |
|     viewItem.ensureActive();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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
 | |
| 
 | |
|   onDownloadClick(aEvent) {
 | |
|     // Handle primary clicks in the main area only:
 | |
|     if (aEvent.button == 0 && aEvent.target.closest(".downloadMainArea")) {
 | |
|       let target = aEvent.target;
 | |
|       while (target.nodeName != "richlistitem") {
 | |
|         target = target.parentNode;
 | |
|       }
 | |
|       let download = DownloadsView.itemForElement(target).download;
 | |
|       if (download.succeeded) {
 | |
|         download._launchedFromPanel = true;
 | |
|       }
 | |
|       let command = "downloadsCmd_open";
 | |
|       if (download.hasBlockedData) {
 | |
|         command = "downloadsCmd_showBlockedInfo";
 | |
|       } else if (aEvent.shiftKey || aEvent.ctrlKey || aEvent.metaKey) {
 | |
|         // We adjust the command for supported modifiers to suggest where the download
 | |
|         // may be opened
 | |
|         let openWhere = target.ownerGlobal.whereToOpenLink(aEvent, false, true);
 | |
|         if (["tab", "window", "tabshifted"].includes(openWhere)) {
 | |
|           command += ":" + openWhere;
 | |
|         }
 | |
|       }
 | |
|       // Toggle opening the file after the download has completed
 | |
|       if (!download.stopped && command.startsWith("downloadsCmd_open")) {
 | |
|         download.launchWhenSucceeded = !download.launchWhenSucceeded;
 | |
|         download._launchedFromPanel = download.launchWhenSucceeded;
 | |
|       }
 | |
| 
 | |
|       DownloadsCommon.log("onDownloadClick, resolved command: ", command);
 | |
|       goDoCommand(command);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onDownloadButton(event) {
 | |
|     let target = event.target.closest("richlistitem");
 | |
|     DownloadsView.itemForElement(target).onButton();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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)) {
 | |
|       aEvent.preventDefault();
 | |
|       goDoCommand("downloadsCmd_pauseResume");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
 | |
|       let readyToDownload = !DownloadsView.richListBox.disabled;
 | |
|       if (readyToDownload) {
 | |
|         goDoCommand("downloadsCmd_doDefault");
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   get contextMenu() {
 | |
|     let menu = document.getElementById("downloadsContextMenu");
 | |
|     if (menu) {
 | |
|       delete this.contextMenu;
 | |
|       this.contextMenu = menu;
 | |
|     }
 | |
|     return menu;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Indicates whether there is an open contextMenu for a download item.
 | |
|    */
 | |
|   get contextMenuOpen() {
 | |
|     return this.contextMenu.state != "closed";
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Whether it's possible to change the currently selected item.
 | |
|    */
 | |
|   get canChangeSelectedItem() {
 | |
|     // When the context menu or a subview are open, the selected item should
 | |
|     // not change.
 | |
|     return !this.contextMenuOpen && !this.subViewOpen;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Mouse listeners to handle selection on hover.
 | |
|    */
 | |
|   onDownloadMouseOver(aEvent) {
 | |
|     let item = aEvent.target.closest("richlistitem,richlistbox");
 | |
|     if (item.localName != "richlistitem") {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (aEvent.target.classList.contains("downloadButton")) {
 | |
|       item.classList.add("downloadHoveringButton");
 | |
|     }
 | |
| 
 | |
|     item.classList.toggle(
 | |
|       "hoveringMainArea",
 | |
|       aEvent.target.closest(".downloadMainArea")
 | |
|     );
 | |
| 
 | |
|     if (this.canChangeSelectedItem) {
 | |
|       this.richListBox.selectedItem = item;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onDownloadMouseOut(aEvent) {
 | |
|     let item = aEvent.target.closest("richlistitem,richlistbox");
 | |
|     if (item.localName != "richlistitem") {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (aEvent.target.classList.contains("downloadButton")) {
 | |
|       item.classList.remove("downloadHoveringButton");
 | |
|     }
 | |
| 
 | |
|     // If the destination element is outside of the richlistitem, clear the
 | |
|     // selection.
 | |
|     if (this.canChangeSelectedItem && !item.contains(aEvent.relatedTarget)) {
 | |
|       this.richListBox.selectedIndex = -1;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onDownloadContextMenu(aEvent) {
 | |
|     let element = aEvent.originalTarget.closest("richlistitem");
 | |
|     if (!element) {
 | |
|       aEvent.preventDefault();
 | |
|       return;
 | |
|     }
 | |
|     // Ensure the selected item is the expected one, so commands and the
 | |
|     // context menu are updated appropriately.
 | |
|     this.richListBox.selectedItem = element;
 | |
|     DownloadsViewController.updateCommands();
 | |
| 
 | |
|     DownloadsViewUI.updateContextMenuForElement(this.contextMenu, element);
 | |
|     // Hide the copy location item if there is somehow no URL. We have to do
 | |
|     // this here instead of in DownloadsViewUI because DownloadsPlacesView
 | |
|     // allows selecting multiple downloads, so in that view the menuitem will be
 | |
|     // shown according to whether at least one of the selected items has a URL.
 | |
|     this.contextMenu.querySelector(".downloadCopyLocationMenuItem").hidden =
 | |
|       !element._shell.download.source?.url;
 | |
|   },
 | |
| 
 | |
|   onDownloadDragStart(aEvent) {
 | |
|     let element = aEvent.target.closest("richlistitem");
 | |
|     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.
 | |
|  */
 | |
| 
 | |
| class DownloadsViewItem extends DownloadsViewUI.DownloadElementShell {
 | |
|   constructor(download, aElement) {
 | |
|     super();
 | |
| 
 | |
|     this.download = download;
 | |
|     this.element = aElement;
 | |
|     this.element._shell = this;
 | |
| 
 | |
|     this.element.setAttribute("type", "download");
 | |
|     this.element.classList.add("download-state");
 | |
| 
 | |
|     this.isPanel = true;
 | |
|   }
 | |
| 
 | |
|   onChanged() {
 | |
|     let newState = DownloadsCommon.stateOfDownload(this.download);
 | |
|     if (this.downloadState !== newState) {
 | |
|       this.downloadState = newState;
 | |
|       this._updateState();
 | |
|     } else {
 | |
|       this._updateStateInner();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   isCommandEnabled(aCommand) {
 | |
|     switch (aCommand) {
 | |
|       case "downloadsCmd_open":
 | |
|       case "downloadsCmd_open:current":
 | |
|       case "downloadsCmd_open:tab":
 | |
|       case "downloadsCmd_open:tabshifted":
 | |
|       case "downloadsCmd_open:window":
 | |
|       case "downloadsCmd_alwaysOpenSimilarFiles": {
 | |
|         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 "downloadsCmd_copyLocation":
 | |
|         return !!this.download.source?.url;
 | |
|       case "cmd_delete":
 | |
|       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)) {
 | |
|       let [command, modifier] = aCommand.split(":");
 | |
|       // split off an optional command "modifier" into an argument,
 | |
|       // e.g. "downloadsCmd_open:window"
 | |
|       this[command](modifier);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Item commands
 | |
| 
 | |
|   downloadsCmd_unblock() {
 | |
|     DownloadsPanel.hidePanel();
 | |
|     this.confirmUnblock(window, "unblock");
 | |
|   }
 | |
| 
 | |
|   downloadsCmd_chooseUnblock() {
 | |
|     DownloadsPanel.hidePanel();
 | |
|     this.confirmUnblock(window, "chooseUnblock");
 | |
|   }
 | |
| 
 | |
|   downloadsCmd_unblockAndOpen() {
 | |
|     DownloadsPanel.hidePanel();
 | |
|     this.unblockAndOpenDownload().catch(console.error);
 | |
|   }
 | |
|   downloadsCmd_unblockAndSave() {
 | |
|     DownloadsPanel.hidePanel();
 | |
|     this.unblockAndSave();
 | |
|   }
 | |
| 
 | |
|   downloadsCmd_open(openWhere) {
 | |
|     super.downloadsCmd_open(openWhere);
 | |
| 
 | |
|     // 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_openInSystemViewer() {
 | |
|     super.downloadsCmd_openInSystemViewer();
 | |
| 
 | |
|     // We explicitly close the panel here to give the user the feedback that
 | |
|     // their click has been received, and we're handling the action.
 | |
|     DownloadsPanel.hidePanel();
 | |
|   }
 | |
| 
 | |
|   downloadsCmd_alwaysOpenInSystemViewer() {
 | |
|     super.downloadsCmd_alwaysOpenInSystemViewer();
 | |
| 
 | |
|     // We explicitly close the panel here to give the user the feedback that
 | |
|     // their click has been received, and we're handling the action.
 | |
|     DownloadsPanel.hidePanel();
 | |
|   }
 | |
| 
 | |
|   downloadsCmd_alwaysOpenSimilarFiles() {
 | |
|     super.downloadsCmd_alwaysOpenSimilarFiles();
 | |
| 
 | |
|     // We explicitly close the panel here to give the user the feedback that
 | |
|     // their click has been received, and we're handling the action.
 | |
|     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();
 | |
|   }
 | |
| 
 | |
|   async downloadsCmd_deleteFile() {
 | |
|     await super.downloadsCmd_deleteFile();
 | |
|     // Protects against an unusual edge case where the user:
 | |
|     // 1) downloads a file with Firefox; 2) deletes the file from outside of Firefox, e.g., a file manager;
 | |
|     // 3) downloads the same file from the same source; 4) opens the downloads panel and uses the menuitem to delete one of those 2 files;
 | |
|     // Under those conditions, Firefox will make 2 view items even though there's only 1 file.
 | |
|     // Using this method will only delete the view item it was called on, because this instance is not aware of other view items with identical targets.
 | |
|     // So the remaining view item needs to be refreshed to hide the "Delete" option.
 | |
|     // That example only concerns 2 duplicate view items but you can have an arbitrary number, so iterate over all items...
 | |
|     for (let viewItem of DownloadsView._visibleViewItems.values()) {
 | |
|       viewItem.download.refresh().catch(console.error);
 | |
|     }
 | |
|     // Don't use DownloadsPanel.hidePanel for this method because it will remove
 | |
|     // the view item from the list, which is already sufficient feedback.
 | |
|   }
 | |
| 
 | |
|   downloadsCmd_showBlockedInfo() {
 | |
|     DownloadsBlockedSubview.toggle(
 | |
|       this.element,
 | |
|       ...this.rawBlockedTitleAndDetails
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   downloadsCmd_openReferrer() {
 | |
|     openURL(this.download.source.referrerInfo.originalReferrer);
 | |
|   }
 | |
| 
 | |
|   downloadsCmd_copyLocation() {
 | |
|     DownloadsCommon.copyDownloadLink(this.download);
 | |
|   }
 | |
| 
 | |
|   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.
 | |
|  */
 | |
| var 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;
 | |
|     }
 | |
|     // Strip off any :modifier suffix before checking if the command name is
 | |
|     // a method on our view
 | |
|     let [command] = aCommand.split(":");
 | |
|     if (!(command in this) && !(command 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 (DownloadsView.subViewOpen) {
 | |
|       let blockedSubviewCmds = [
 | |
|         "downloadsCmd_unblockAndOpen",
 | |
|         "cmd_delete",
 | |
|         "downloadsCmd_unblockAndSave",
 | |
|       ];
 | |
|       return blockedSubviewCmds.includes(aCommand);
 | |
|     }
 | |
|     // 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.
 | |
|  */
 | |
| var 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;
 | |
|     }
 | |
|     if (aActive) {
 | |
|       DownloadsCommon.getSummary(
 | |
|         window,
 | |
|         DownloadsView.kItemCountLimit
 | |
|       ).refreshView(this);
 | |
|     } else {
 | |
|       DownloadsFooter.showingSummary = false;
 | |
|     }
 | |
| 
 | |
|     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.
 | |
|     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);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Focuses the root element of the summary.
 | |
|    */
 | |
|   focus(focusOptions) {
 | |
|     if (this._summaryNode) {
 | |
|       this._summaryNode.focus(focusOptions);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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.
 | |
|  */
 | |
| var DownloadsFooter = {
 | |
|   /**
 | |
|    * Focuses the appropriate element within the footer. If the summary
 | |
|    * is visible, focus it. If not, focus the "Show all downloads"
 | |
|    * button.
 | |
|    */
 | |
|   focus(focusOptions) {
 | |
|     if (this._showingSummary) {
 | |
|       DownloadsSummary.focus(focusOptions);
 | |
|     } else {
 | |
|       DownloadsView.downloadsHistory.focus(focusOptions);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   _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");
 | |
|       }
 | |
|       this._showingSummary = 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.
 | |
|  */
 | |
| var DownloadsBlockedSubview = {
 | |
|   /**
 | |
|    * Elements in the subview.
 | |
|    */
 | |
|   get elements() {
 | |
|     let idSuffixes = [
 | |
|       "title",
 | |
|       "details1",
 | |
|       "details2",
 | |
|       "unblockButton",
 | |
|       "deleteButton",
 | |
|     ];
 | |
|     let elements = idSuffixes.reduce((memo, s) => {
 | |
|       memo[s] = document.getElementById("downloadsPanel-blockedSubview-" + s);
 | |
|       return memo;
 | |
|     }, {});
 | |
|     delete this.elements;
 | |
|     return (this.elements = elements);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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) {
 | |
|     DownloadsView.subViewOpen = true;
 | |
|     DownloadsViewController.updateCommands();
 | |
|     const { download } = DownloadsView.itemForElement(element);
 | |
| 
 | |
|     let e = this.elements;
 | |
|     let s = DownloadsCommon.strings;
 | |
| 
 | |
|     title.l10n
 | |
|       ? document.l10n.setAttributes(e.title, title.l10n.id, title.l10n.args)
 | |
|       : (e.title.textContent = title);
 | |
| 
 | |
|     details[0].l10n
 | |
|       ? document.l10n.setAttributes(
 | |
|           e.details1,
 | |
|           details[0].l10n.id,
 | |
|           details[0].l10n.args
 | |
|         )
 | |
|       : (e.details1.textContent = details[0]);
 | |
| 
 | |
|     e.details2.textContent = details[1];
 | |
| 
 | |
|     if (download.launchWhenSucceeded) {
 | |
|       e.unblockButton.label = s.unblockButtonOpen;
 | |
|       e.unblockButton.command = "downloadsCmd_unblockAndOpen";
 | |
|     } else {
 | |
|       e.unblockButton.label = s.unblockButtonUnblock;
 | |
|       e.unblockButton.command = "downloadsCmd_unblockAndSave";
 | |
|     }
 | |
| 
 | |
|     e.deleteButton.label = s.unblockButtonConfirmBlock;
 | |
| 
 | |
|     let verdict = element.getAttribute("verdict");
 | |
|     this.subview.setAttribute("verdict", verdict);
 | |
| 
 | |
|     this.mainView.addEventListener("ViewShown", this);
 | |
|     DownloadsPanel.panel.addEventListener("popuphidden", this);
 | |
|     this.panelMultiView.showSubView(this.subview);
 | |
| 
 | |
|     // Without this, the mainView is more narrow than the panel once all
 | |
|     // downloads are removed from the panel.
 | |
|     this.mainView.style.minWidth = window.getComputedStyle(this.subview).width;
 | |
|   },
 | |
| 
 | |
|   handleEvent(event) {
 | |
|     // This is called when the main view is shown or the panel is hidden.
 | |
|     DownloadsView.subViewOpen = false;
 | |
|     this.mainView.removeEventListener("ViewShown", this);
 | |
|     DownloadsPanel.panel.removeEventListener("popuphidden", this);
 | |
|     // Focus the proper element if we're going back to the main panel.
 | |
|     if (event.type == "ViewShown") {
 | |
|       DownloadsPanel.showPanel();
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Deletes the download and hides the entire panel.
 | |
|    */
 | |
|   confirmBlock() {
 | |
|     goDoCommand("cmd_delete");
 | |
|     DownloadsPanel.hidePanel();
 | |
|   },
 | |
| };
 | |
| 
 | |
| ChromeUtils.defineLazyGetter(DownloadsBlockedSubview, "panelMultiView", () =>
 | |
|   document.getElementById("downloadsPanel-multiView")
 | |
| );
 | |
| ChromeUtils.defineLazyGetter(DownloadsBlockedSubview, "mainView", () =>
 | |
|   document.getElementById("downloadsPanel-mainView")
 | |
| );
 | |
| ChromeUtils.defineLazyGetter(DownloadsBlockedSubview, "subview", () =>
 | |
|   document.getElementById("downloadsPanel-blockedSubview")
 | |
| );
 | |
| 
 | |
| XPCOMUtils.defineConstant(
 | |
|   this,
 | |
|   "DownloadsBlockedSubview",
 | |
|   DownloadsBlockedSubview
 | |
| );
 | 
