forked from mirrors/gecko-dev
		
	 4b693cc30d
			
		
	
	
		4b693cc30d
		
	
	
	
	
		
			
			MozReview-Commit-ID: 62VJbzJwxVW --HG-- extra : rebase_source : 8a33f8c14ebf892363da8edd8aca8565beca920d
		
			
				
	
	
		
			474 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			474 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			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 = [
 | |
|   "DownloadsSubview",
 | |
| ];
 | |
| 
 | |
| const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 | |
| 
 | |
| Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
 | |
|                                   "resource://gre/modules/AppConstants.jsm");
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
 | |
|                                   "resource://gre/modules/Downloads.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");
 | |
| 
 | |
| let gPanelViewInstances = new WeakMap();
 | |
| const kEvents = ["ViewShowing", "ViewHiding", "click", "command"];
 | |
| const kRefreshBatchSize = 10;
 | |
| const kMaxWaitForIdleMs = 200;
 | |
| XPCOMUtils.defineLazyGetter(this, "kButtonLabels", () => {
 | |
|   return {
 | |
|     show: DownloadsCommon.strings[AppConstants.platform == "macosx" ? "showMacLabel" : "showLabel"],
 | |
|     open: DownloadsCommon.strings.openFileLabel,
 | |
|     retry: DownloadsCommon.strings.retryLabel,
 | |
|   };
 | |
| });
 | |
| 
 | |
| class DownloadsSubview extends DownloadsViewUI.BaseView {
 | |
|   constructor(panelview) {
 | |
|     super();
 | |
|     this.document = panelview.ownerDocument;
 | |
|     this.window = panelview.ownerGlobal;
 | |
| 
 | |
|     this.context = "panelDownloadsContextMenu";
 | |
| 
 | |
|     this.panelview = panelview;
 | |
|     this.container = this.document.getElementById("panelMenu_downloadsMenu");
 | |
|     while (this.container.lastChild) {
 | |
|       this.container.lastChild.remove();
 | |
|     }
 | |
|     this.panelview.addEventListener("click", DownloadsSubview.onClick);
 | |
|     this.panelview.addEventListener("ViewHiding", DownloadsSubview.onViewHiding);
 | |
| 
 | |
|     this._viewItemsForDownloads = new WeakMap();
 | |
| 
 | |
|     let contextMenu = this.document.getElementById(this.context);
 | |
|     if (!contextMenu) {
 | |
|       contextMenu = this.document.getElementById("downloadsContextMenu").cloneNode(true);
 | |
|       contextMenu.setAttribute("closemenu", "none");
 | |
|       contextMenu.setAttribute("id", this.context);
 | |
|       contextMenu.removeAttribute("onpopupshown");
 | |
|       contextMenu.setAttribute("onpopupshowing",
 | |
|         "DownloadsSubview.updateContextMenu(document.popupNode, this);");
 | |
|       contextMenu.setAttribute("onpopuphidden", "DownloadsSubview.onContextMenuHidden(this);")
 | |
|       let clearButton = contextMenu.querySelector("menuitem[command='downloadsCmd_clearDownloads']");
 | |
|       clearButton.hidden = false;
 | |
|       clearButton.previousSibling.hidden = true;
 | |
|       contextMenu.querySelector("menuitem[command='cmd_delete']")
 | |
|         .setAttribute("command", "downloadsCmd_delete");
 | |
|     }
 | |
|     this.panelview.appendChild(contextMenu);
 | |
|     this.container.setAttribute("context", this.context);
 | |
| 
 | |
|     this._downloadsData = DownloadsCommon.getData(this.window, true, true, true);
 | |
|     this._downloadsData.addView(this);
 | |
|   }
 | |
| 
 | |
|   destructor(event) {
 | |
|     this.panelview.removeEventListener("click", DownloadsSubview.onClick);
 | |
|     this.panelview.removeEventListener("ViewHiding", DownloadsSubview.onViewHiding);
 | |
|     this._downloadsData.removeView(this);
 | |
|     gPanelViewInstances.delete(this);
 | |
|     this.destroyed = true;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * DataView handler; invoked when a batch of downloads is being passed in -
 | |
|    * usually when this instance is added as a view in the constructor.
 | |
|    */
 | |
|   onDownloadBatchStarting() {
 | |
|     this.batchFragment = this.document.createDocumentFragment();
 | |
|     this.window.clearTimeout(this._batchTimeout);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * DataView handler; invoked when the view stopped feeding its current list of
 | |
|    * downloads.
 | |
|    */
 | |
|   onDownloadBatchEnded() {
 | |
|     let {window} = this;
 | |
|     window.clearTimeout(this._batchTimeout);
 | |
|     let waitForMs = 200;
 | |
|     if (this.batchFragment.childElementCount) {
 | |
|       // Prepend the batch fragment.
 | |
|       this.container.insertBefore(this.batchFragment, this.container.firstChild || null);
 | |
|       waitForMs = 0;
 | |
|     }
 | |
|     // Wait a wee bit to dispatch the event, because another batch may start
 | |
|     // right away.
 | |
|     this._batchTimeout = window.setTimeout(() => {
 | |
|       this._updateStatsFromDisk();
 | |
|       this.panelview.dispatchEvent(new window.CustomEvent("DownloadsLoaded"));
 | |
|     }, waitForMs);
 | |
|     this.batchFragment = null;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * DataView handler; invoked when a new download is added to the list.
 | |
|    *
 | |
|    * @param {Download} download
 | |
|    * @param {DOMNode}  [options.insertBefore]
 | |
|    */
 | |
|   onDownloadAdded(download, { insertBefore } = {}) {
 | |
|     let shell = new DownloadsSubview.Button(download, this.document);
 | |
|     this._viewItemsForDownloads.set(download, shell);
 | |
|     // Triggger the code that update all attributes to match the downloads'
 | |
|     // current state.
 | |
|     shell.onChanged();
 | |
| 
 | |
|     // Since newest downloads are displayed at the top, either prepend the new
 | |
|     // element or insert it after the one indicated by the insertBefore option.
 | |
|     if (insertBefore) {
 | |
|       this._viewItemsForDownloads.get(insertBefore)
 | |
|           .element.insertAdjacentElement("afterend", shell.element);
 | |
|     } else {
 | |
|       (this.batchFragment || this.container).prepend(shell.element);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * DataView Handler; invoked when the state of a download changed.
 | |
|    *
 | |
|    * @param {Download} download
 | |
|    */
 | |
|   onDownloadChanged(download) {
 | |
|     this._viewItemsForDownloads.get(download).onChanged();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * DataView handler; invoked when a download is removed.
 | |
|    *
 | |
|    * @param {Download} download
 | |
|    */
 | |
|   onDownloadRemoved(download) {
 | |
|     this._viewItemsForDownloads.get(download).element.remove();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Schedule a refresh of the downloads that were added, which is mainly about
 | |
|    * checking whether the target file still exists.
 | |
|    * We're doing this during idle time and in chunks.
 | |
|    */
 | |
|   async _updateStatsFromDisk() {
 | |
|     if (this._updatingStats)
 | |
|       return;
 | |
| 
 | |
|     this._updatingStats = true;
 | |
| 
 | |
|     try {
 | |
|       let idleOptions = { timeout: kMaxWaitForIdleMs };
 | |
|       // Start with getting an idle moment to (maybe) refresh the list of downloads.
 | |
|       await new Promise(resolve => this.window.requestIdleCallback(resolve), idleOptions);
 | |
|       // In the meantime, this instance could have been destroyed, so take note.
 | |
|       if (this.destroyed)
 | |
|         return;
 | |
| 
 | |
|       let count = 0;
 | |
|       for (let button of this.container.childNodes) {
 | |
|         if (this.destroyed)
 | |
|           return;
 | |
|         if (!button._shell)
 | |
|           continue;
 | |
| 
 | |
|         await button._shell.refresh();
 | |
| 
 | |
|         // Make sure to request a new idle moment every `kRefreshBatchSize` buttons.
 | |
|         if (++count % kRefreshBatchSize === 0) {
 | |
|           await new Promise(resolve => this.window.requestIdleCallback(resolve, idleOptions));
 | |
|         }
 | |
|       }
 | |
|     } catch (ex) {
 | |
|       Cu.reportError(ex);
 | |
|     } finally {
 | |
|       this._updatingStats = false;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // ----- Static methods. -----
 | |
| 
 | |
|   /**
 | |
|    * Perform all tasks necessary to be able to show a Downloads Subview.
 | |
|    *
 | |
|    * @param  {DOMWindow} window  Global window object.
 | |
|    * @return {Promise}   Will resolve when all tasks are done.
 | |
|    */
 | |
|   static init(window) {
 | |
|     return new Promise(resolve =>
 | |
|       window.DownloadsOverlayLoader.ensureOverlayLoaded(window.DownloadsPanel.kDownloadsOverlay, resolve));
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Show the Downloads subview panel and listen for events that will trigger
 | |
|    * building the dynamic part of the view.
 | |
|    *
 | |
|    * @param {DOMNode} anchor The button that was commanded to trigger this function.
 | |
|    */
 | |
|   static async show(anchor) {
 | |
|     let document = anchor.ownerDocument;
 | |
|     let window = anchor.ownerGlobal;
 | |
|     await DownloadsSubview.init(window);
 | |
| 
 | |
|     let panelview = document.getElementById("PanelUI-downloads");
 | |
|     anchor.setAttribute("closemenu", "none");
 | |
|     gPanelViewInstances.set(panelview, new DownloadsSubview(panelview));
 | |
| 
 | |
|     // Since the DownloadsLists are propagated asynchronously, we need to wait a
 | |
|     // little to get the view propagated.
 | |
|     panelview.addEventListener("ViewShowing", event => {
 | |
|       event.detail.addBlocker(new Promise(resolve => {
 | |
|         panelview.addEventListener("DownloadsLoaded", resolve, { once: true });
 | |
|       }));
 | |
|     }, { once: true });
 | |
| 
 | |
|     window.PanelUI.showSubView("PanelUI-downloads", anchor);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handler method; reveal the users' download directory using the OS specific
 | |
|    * method.
 | |
|    */
 | |
|   static async onShowDownloads() {
 | |
|     // Retrieve the user's default download directory.
 | |
|     let preferredDir = await Downloads.getPreferredDownloadsDirectory();
 | |
|     DownloadsCommon.showDirectory(new FileUtils.File(preferredDir));
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handler method; clear the list downloads finished and old(er) downloads,
 | |
|    * just like in the Library.
 | |
|    *
 | |
|    * @param {DOMNode} button Button that was clicked to call this method.
 | |
|    */
 | |
|   static onClearDownloads(button) {
 | |
|     let instance = gPanelViewInstances.get(button.closest("panelview"));
 | |
|     if (!instance)
 | |
|       return;
 | |
|     instance._downloadsData.removeFinished();
 | |
|     Cc["@mozilla.org/browser/download-history;1"]
 | |
|       .getService(Ci.nsIDownloadHistory)
 | |
|       .removeAllDownloads();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Just before showing the context menu, anchored to a download item, we need
 | |
|    * to set the right properties to make sure the right menu-items are visible.
 | |
|    *
 | |
|    * @param {DOMNode} button The Button the context menu will be anchored to.
 | |
|    * @param {DOMNode} menu   The context menu.
 | |
|    */
 | |
|   static updateContextMenu(button, menu) {
 | |
|     while (!button._shell) {
 | |
|       button = button.parentNode;
 | |
|     }
 | |
|     menu.setAttribute("state", button.getAttribute("state"));
 | |
|     if (button.hasAttribute("exists"))
 | |
|       menu.setAttribute("exists", button.getAttribute("exists"));
 | |
|     else
 | |
|       menu.removeAttribute("exists");
 | |
|     menu.classList.toggle("temporary-block", button.classList.contains("temporary-block"));
 | |
|     for (let menuitem of menu.getElementsByTagName("menuitem")) {
 | |
|       let command = menuitem.getAttribute("command");
 | |
|       if (!command)
 | |
|         continue;
 | |
|       if (command == "downloadsCmd_clearDownloads") {
 | |
|         menuitem.disabled = !DownloadsSubview.canClearDownloads(button);
 | |
|       } else {
 | |
|         menuitem.disabled = !button._shell.isCommandEnabled(command);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // The menu anchorNode property is not available long enough to be used elsewhere,
 | |
|     // so tack it another property name.
 | |
|     menu._anchorNode = button;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Right after the context menu was hidden, perform a bit of cleanup.
 | |
|    *
 | |
|    * @param {DOMNode} menu The context menu.
 | |
|    */
 | |
|   static onContextMenuHidden(menu) {
 | |
|     delete menu._anchorNode;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Static version of DownloadsSubview#canClearDownloads().
 | |
|    *
 | |
|    * @param {DOMNode} button Button that we'll use to find the right
 | |
|    *                         DownloadsSubview instance.
 | |
|    */
 | |
|   static canClearDownloads(button) {
 | |
|     let instance = gPanelViewInstances.get(button.closest("panelview"));
 | |
|     if (!instance)
 | |
|       return false;
 | |
|     return instance.canClearDownloads(instance.container);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handler method; invoked when the Downloads panel is hidden and should be
 | |
|    * torn down & cleaned up.
 | |
|    *
 | |
|    * @param {DOMEvent} event
 | |
|    */
 | |
|   static onViewHiding(event) {
 | |
|     let instance = gPanelViewInstances.get(event.target);
 | |
|     if (!instance)
 | |
|       return;
 | |
|     instance.destructor(event);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handler method; invoked when anything is clicked inside the Downloads panel.
 | |
|    * Depending on the context, it will find the appropriate command to invoke.
 | |
|    *
 | |
|    * We don't have a command dispatcher registered for this view, so we don't go
 | |
|    * through the goDoCommand path like we do for the other views.
 | |
|    *
 | |
|    * @param {DOMMouseEvent} event
 | |
|    */
 | |
|   static onClick(event) {
 | |
|     // Middle clicks fall through and are regarded as left clicks.
 | |
|     if (event.button > 1)
 | |
|       return;
 | |
| 
 | |
|     let button = event.originalTarget;
 | |
|     if (!button.hasAttribute || button.classList.contains("subviewbutton-back"))
 | |
|       return;
 | |
| 
 | |
|     let command = "downloadsCmd_open";
 | |
|     if (button.classList.contains("action-button")) {
 | |
|       button = button.parentNode;
 | |
|       command = button.hasAttribute("showLabel") ? "downloadsCmd_show" : "downloadsCmd_retry";
 | |
|     } else if (button.localName == "menuitem") {
 | |
|       command = button.getAttribute("command");
 | |
|       button = button.parentNode._anchorNode;
 | |
|     }
 | |
|     while (button && !button._shell && button != this.panelview &&
 | |
|            (!button.hasAttribute || !button.hasAttribute("oncommand"))) {
 | |
|       button = button.parentNode;
 | |
|     }
 | |
| 
 | |
|     // We don't need to do anything when no button was clicked, like a separator
 | |
|     // or a blank panel area. Also, when 'oncommand' is set, the button will invoke
 | |
|     // its own, custom command handler.
 | |
|     if (!button || button == this.panelview || button.hasAttribute("oncommand"))
 | |
|       return;
 | |
| 
 | |
|     if (command == "downloadsCmd_clearDownloads") {
 | |
|       DownloadsSubview.onClearDownloads(button);
 | |
|     } else if (button._shell.isCommandEnabled(command)) {
 | |
|       button._shell[command]();
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| DownloadsSubview.Button = class extends DownloadsViewUI.DownloadElementShell {
 | |
|   constructor(download, document) {
 | |
|     super();
 | |
|     this.download = download;
 | |
| 
 | |
|     this.element = document.createElement("toolbarbutton");
 | |
|     this.element._shell = this;
 | |
| 
 | |
|     this.element.classList.add("subviewbutton", "subviewbutton-iconic", "download",
 | |
|       "download-state");
 | |
|   }
 | |
| 
 | |
|   get browserWindow() {
 | |
|     return this.element.ownerGlobal;
 | |
|   }
 | |
| 
 | |
|   async refresh() {
 | |
|     if (this._targetFileChecked)
 | |
|       return;
 | |
| 
 | |
|     try {
 | |
|       await this.download.refresh();
 | |
|     } catch (ex) {
 | |
|       Cu.reportError(ex);
 | |
|     } finally {
 | |
|       this._targetFileChecked = true;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handle state changes of a download.
 | |
|    */
 | |
|   onStateChanged() {
 | |
|     // Since the state changed, we may need to check the target file again.
 | |
|     this._targetFileChecked = false;
 | |
| 
 | |
|     this._updateState();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Handler method; invoked when any state attribute of a download changed.
 | |
|    */
 | |
|   onChanged() {
 | |
|     let newState = DownloadsCommon.stateOfDownload(this.download);
 | |
|     if (this._downloadState !== newState) {
 | |
|       this._downloadState = newState;
 | |
|       this.onStateChanged();
 | |
|     } else {
 | |
|       this._updateState();
 | |
|     }
 | |
| 
 | |
|     // 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);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Update the DOM representation of this download to match the current, recently
 | |
|    * updated, state.
 | |
|    */
 | |
|   _updateState() {
 | |
|     super._updateState();
 | |
|     this.element.setAttribute("label", this.element.getAttribute("displayName"));
 | |
|     this.element.setAttribute("tooltiptext", this.element.getAttribute("fullStatus"));
 | |
| 
 | |
|     if (this.isCommandEnabled("downloadsCmd_show")) {
 | |
|       this.element.setAttribute("openLabel", kButtonLabels.open);
 | |
|       this.element.setAttribute("showLabel", kButtonLabels.show);
 | |
|       this.element.removeAttribute("retryLabel");
 | |
|     } else if (this.isCommandEnabled("downloadsCmd_retry")) {
 | |
|       this.element.setAttribute("retryLabel", kButtonLabels.retry);
 | |
|       this.element.removeAttribute("openLabel");
 | |
|       this.element.removeAttribute("showLabel");
 | |
|     } else {
 | |
|       this.element.removeAttribute("openLabel");
 | |
|       this.element.removeAttribute("retryLabel");
 | |
|       this.element.removeAttribute("showLabel");
 | |
|     }
 | |
| 
 | |
|     this._updateVisibility();
 | |
|   }
 | |
| 
 | |
|   _updateVisibility() {
 | |
|     let state = this.element.getAttribute("state");
 | |
|     // This view only show completed and failed downloads.
 | |
|     this.element.hidden = !(state == DownloadsCommon.DOWNLOAD_FINISHED ||
 | |
|       state == DownloadsCommon.DOWNLOAD_FAILED);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Command handler; copy the download URL to the OS general clipboard.
 | |
|    */
 | |
|   downloadsCmd_copyLocation() {
 | |
|     let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
 | |
|                       .getService(Ci.nsIClipboardHelper);
 | |
|     clipboard.copyString(this.download.source.url);
 | |
|   }
 | |
| };
 |