browser/components/downloads/content/downloads.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     2 /* vim: set ts=2 et sw=2 tw=80: */
     3 /* This Source Code Form is subject to the terms of the Mozilla Public
     4  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     5  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     7 /**
     8  * Handles the Downloads panel user interface for each browser window.
     9  *
    10  * This file includes the following constructors and global objects:
    11  *
    12  * DownloadsPanel
    13  * Main entry point for the downloads panel interface.
    14  *
    15  * DownloadsOverlayLoader
    16  * Allows loading the downloads panel and the status indicator interfaces on
    17  * demand, to improve startup performance.
    18  *
    19  * DownloadsView
    20  * Builds and updates the downloads list widget, responding to changes in the
    21  * download state and real-time data.  In addition, handles part of the user
    22  * interaction events raised by the downloads list widget.
    23  *
    24  * DownloadsViewItem
    25  * Builds and updates a single item in the downloads list widget, responding to
    26  * changes in the download state and real-time data.
    27  *
    28  * DownloadsViewController
    29  * Handles part of the user interaction events raised by the downloads list
    30  * widget, in particular the "commands" that apply to multiple items, and
    31  * dispatches the commands that apply to individual items.
    32  *
    33  * DownloadsViewItemController
    34  * Handles all the user interaction events, in particular the "commands",
    35  * related to a single item in the downloads list widgets.
    36  */
    38 /**
    39  * A few words on focus and focusrings
    40  *
    41  * We do quite a few hacks in the Downloads Panel for focusrings. In fact, we
    42  * basically suppress most if not all XUL-level focusrings, and style/draw
    43  * them ourselves (using :focus instead of -moz-focusring). There are a few
    44  * reasons for this:
    45  *
    46  * 1) Richlists on OSX don't have focusrings; instead, they are shown as
    47  *    selected. This makes for some ambiguity when we have a focused/selected
    48  *    item in the list, and the mouse is hovering a completed download (which
    49  *    highlights).
    50  * 2) Windows doesn't show focusrings until after the first time that tab is
    51  *    pressed (and by then you're focusing the second item in the panel).
    52  * 3) Richlistbox sets -moz-focusring even when we select it with a mouse.
    53  *
    54  * In general, the desired behaviour is to focus the first item after pressing
    55  * tab/down, and show that focus with a ring. Then, if the mouse moves over
    56  * the panel, to hide that focus ring; essentially resetting us to the state
    57  * before pressing the key.
    58  *
    59  * We end up capturing the tab/down key events, and preventing their default
    60  * behaviour. We then set a "keyfocus" attribute on the panel, which allows
    61  * us to draw a ring around the currently focused element. If the panel is
    62  * closed or the mouse moves over the panel, we remove the attribute.
    63  */
    65 "use strict";
    67 ////////////////////////////////////////////////////////////////////////////////
    68 //// Globals
    70 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
    71                                   "resource://gre/modules/DownloadUtils.jsm");
    72 XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
    73                                   "resource:///modules/DownloadsCommon.jsm");
    74 XPCOMUtils.defineLazyModuleGetter(this, "OS",
    75                                   "resource://gre/modules/osfile.jsm");
    76 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
    77                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
    78 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
    79                                   "resource://gre/modules/PlacesUtils.jsm");
    80 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
    81                                   "resource://gre/modules/NetUtil.jsm");
    83 ////////////////////////////////////////////////////////////////////////////////
    84 //// DownloadsPanel
    86 /**
    87  * Main entry point for the downloads panel interface.
    88  */
    89 const DownloadsPanel = {
    90   //////////////////////////////////////////////////////////////////////////////
    91   //// Initialization and termination
    93   /**
    94    * Internal state of the downloads panel, based on one of the kState
    95    * constants.  This is not the same state as the XUL panel element.
    96    */
    97   _state: 0,
    99   /** The panel is not linked to downloads data yet. */
   100   get kStateUninitialized() 0,
   101   /** This object is linked to data, but the panel is invisible. */
   102   get kStateHidden() 1,
   103   /** The panel will be shown as soon as possible. */
   104   get kStateWaitingData() 2,
   105   /** The panel is almost shown - we're just waiting to get a handle on the
   106       anchor. */
   107   get kStateWaitingAnchor() 3,
   108   /** The panel is open. */
   109   get kStateShown() 4,
   111   /**
   112    * Location of the panel overlay.
   113    */
   114   get kDownloadsOverlay()
   115       "chrome://browser/content/downloads/downloadsOverlay.xul",
   117   /**
   118    * Starts loading the download data in background, without opening the panel.
   119    * Use showPanel instead to load the data and open the panel at the same time.
   120    *
   121    * @param aCallback
   122    *        Called when initialization is complete.
   123    */
   124   initialize: function DP_initialize(aCallback)
   125   {
   126     DownloadsCommon.log("Attempting to initialize DownloadsPanel for a window.");
   127     if (this._state != this.kStateUninitialized) {
   128       DownloadsCommon.log("DownloadsPanel is already initialized.");
   129       DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay,
   130                                                  aCallback);
   131       return;
   132     }
   133     this._state = this.kStateHidden;
   135     window.addEventListener("unload", this.onWindowUnload, false);
   137     // Load and resume active downloads if required.  If there are downloads to
   138     // be shown in the panel, they will be loaded asynchronously.
   139     DownloadsCommon.initializeAllDataLinks();
   141     // Now that data loading has eventually started, load the required XUL
   142     // elements and initialize our views.
   143     DownloadsCommon.log("Ensuring DownloadsPanel overlay loaded.");
   144     DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay,
   145                                                function DP_I_callback() {
   146       DownloadsViewController.initialize();
   147       DownloadsCommon.log("Attaching DownloadsView...");
   148       DownloadsCommon.getData(window).addView(DownloadsView);
   149       DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit)
   150                      .addView(DownloadsSummary);
   151       DownloadsCommon.log("DownloadsView attached - the panel for this window",
   152                           "should now see download items come in.");
   153       DownloadsPanel._attachEventListeners();
   154       DownloadsCommon.log("DownloadsPanel initialized.");
   155       aCallback();
   156     });
   157   },
   159   /**
   160    * Closes the downloads panel and frees the internal resources related to the
   161    * downloads.  The downloads panel can be reopened later, even after this
   162    * function has been called.
   163    */
   164   terminate: function DP_terminate()
   165   {
   166     DownloadsCommon.log("Attempting to terminate DownloadsPanel for a window.");
   167     if (this._state == this.kStateUninitialized) {
   168       DownloadsCommon.log("DownloadsPanel was never initialized. Nothing to do.");
   169       return;
   170     }
   172     window.removeEventListener("unload", this.onWindowUnload, false);
   174     // Ensure that the panel is closed before shutting down.
   175     this.hidePanel();
   177     DownloadsViewController.terminate();
   178     DownloadsCommon.getData(window).removeView(DownloadsView);
   179     DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit)
   180                    .removeView(DownloadsSummary);
   181     this._unattachEventListeners();
   183     this._state = this.kStateUninitialized;
   185     DownloadsSummary.active = false;
   186     DownloadsCommon.log("DownloadsPanel terminated.");
   187   },
   189   //////////////////////////////////////////////////////////////////////////////
   190   //// Panel interface
   192   /**
   193    * Main panel element in the browser window, or null if the panel overlay
   194    * hasn't been loaded yet.
   195    */
   196   get panel()
   197   {
   198     // If the downloads panel overlay hasn't loaded yet, just return null
   199     // without reseting this.panel.
   200     let downloadsPanel = document.getElementById("downloadsPanel");
   201     if (!downloadsPanel)
   202       return null;
   204     delete this.panel;
   205     return this.panel = downloadsPanel;
   206   },
   208   /**
   209    * Starts opening the downloads panel interface, anchored to the downloads
   210    * button of the browser window.  The list of downloads to display is
   211    * initialized the first time this method is called, and the panel is shown
   212    * only when data is ready.
   213    */
   214   showPanel: function DP_showPanel()
   215   {
   216     DownloadsCommon.log("Opening the downloads panel.");
   218     if (this.isPanelShowing) {
   219       DownloadsCommon.log("Panel is already showing - focusing instead.");
   220       this._focusPanel();
   221       return;
   222     }
   224     this.initialize(function DP_SP_callback() {
   225       // Delay displaying the panel because this function will sometimes be
   226       // called while another window is closing (like the window for selecting
   227       // whether to save or open the file), and that would cause the panel to
   228       // close immediately.
   229       setTimeout(function () DownloadsPanel._openPopupIfDataReady(), 0);
   230     }.bind(this));
   232     DownloadsCommon.log("Waiting for the downloads panel to appear.");
   233     this._state = this.kStateWaitingData;
   234   },
   236   /**
   237    * Hides the downloads panel, if visible, but keeps the internal state so that
   238    * the panel can be reopened quickly if required.
   239    */
   240   hidePanel: function DP_hidePanel()
   241   {
   242     DownloadsCommon.log("Closing the downloads panel.");
   244     if (!this.isPanelShowing) {
   245       DownloadsCommon.log("Downloads panel is not showing - nothing to do.");
   246       return;
   247     }
   249     this.panel.hidePopup();
   251     // Ensure that we allow the panel to be reopened.  Note that, if the popup
   252     // was open, then the onPopupHidden event handler has already updated the
   253     // current state, otherwise we must update the state ourselves.
   254     this._state = this.kStateHidden;
   255     DownloadsCommon.log("Downloads panel is now closed.");
   256   },
   258   /**
   259    * Indicates whether the panel is shown or will be shown.
   260    */
   261   get isPanelShowing()
   262   {
   263     return this._state == this.kStateWaitingData ||
   264            this._state == this.kStateWaitingAnchor ||
   265            this._state == this.kStateShown;
   266   },
   268   /**
   269    * Returns whether the user has started keyboard navigation.
   270    */
   271   get keyFocusing()
   272   {
   273     return this.panel.hasAttribute("keyfocus");
   274   },
   276   /**
   277    * Set to true if the user has started keyboard navigation, and we should be
   278    * showing focusrings in the panel. Also adds a mousemove event handler to
   279    * the panel which disables keyFocusing.
   280    */
   281   set keyFocusing(aValue)
   282   {
   283     if (aValue) {
   284       this.panel.setAttribute("keyfocus", "true");
   285       this.panel.addEventListener("mousemove", this);
   286     } else {
   287       this.panel.removeAttribute("keyfocus");
   288       this.panel.removeEventListener("mousemove", this);
   289     }
   290     return aValue;
   291   },
   293   /**
   294    * Handles the mousemove event for the panel, which disables focusring
   295    * visualization.
   296    */
   297   handleEvent: function DP_handleEvent(aEvent)
   298   {
   299     if (aEvent.type == "mousemove") {
   300       this.keyFocusing = false;
   301     }
   302   },
   304   //////////////////////////////////////////////////////////////////////////////
   305   //// Callback functions from DownloadsView
   307   /**
   308    * Called after data loading finished.
   309    */
   310   onViewLoadCompleted: function DP_onViewLoadCompleted()
   311   {
   312     this._openPopupIfDataReady();
   313   },
   315   //////////////////////////////////////////////////////////////////////////////
   316   //// User interface event functions
   318   onWindowUnload: function DP_onWindowUnload()
   319   {
   320     // This function is registered as an event listener, we can't use "this".
   321     DownloadsPanel.terminate();
   322   },
   324   onPopupShown: function DP_onPopupShown(aEvent)
   325   {
   326     // Ignore events raised by nested popups.
   327     if (aEvent.target != aEvent.currentTarget) {
   328       return;
   329     }
   331     DownloadsCommon.log("Downloads panel has shown.");
   332     this._state = this.kStateShown;
   334     // Since at most one popup is open at any given time, we can set globally.
   335     DownloadsCommon.getIndicatorData(window).attentionSuppressed = true;
   337     // Ensure that the first item is selected when the panel is focused.
   338     if (DownloadsView.richListBox.itemCount > 0 &&
   339         DownloadsView.richListBox.selectedIndex == -1) {
   340       DownloadsView.richListBox.selectedIndex = 0;
   341     }
   343     this._focusPanel();
   344   },
   346   onPopupHidden: function DP_onPopupHidden(aEvent)
   347   {
   348     // Ignore events raised by nested popups.
   349     if (aEvent.target != aEvent.currentTarget) {
   350       return;
   351     }
   353     DownloadsCommon.log("Downloads panel has hidden.");
   355     // Removes the keyfocus attribute so that we stop handling keyboard
   356     // navigation.
   357     this.keyFocusing = false;
   359     // Since at most one popup is open at any given time, we can set globally.
   360     DownloadsCommon.getIndicatorData(window).attentionSuppressed = false;
   362     // Allow the anchor to be hidden.
   363     DownloadsButton.releaseAnchor();
   365     // Allow the panel to be reopened.
   366     this._state = this.kStateHidden;
   367   },
   369   //////////////////////////////////////////////////////////////////////////////
   370   //// Related operations
   372   /**
   373    * Shows or focuses the user interface dedicated to downloads history.
   374    */
   375   showDownloadsHistory: function DP_showDownloadsHistory()
   376   {
   377     DownloadsCommon.log("Showing download history.");
   378     // Hide the panel before showing another window, otherwise focus will return
   379     // to the browser window when the panel closes automatically.
   380     this.hidePanel();
   382     BrowserDownloadsUI();
   383   },
   385   //////////////////////////////////////////////////////////////////////////////
   386   //// Internal functions
   388   /**
   389    * Attach event listeners to a panel element. These listeners should be
   390    * removed in _unattachEventListeners. This is called automatically after the
   391    * panel has successfully loaded.
   392    */
   393   _attachEventListeners: function DP__attachEventListeners()
   394   {
   395     // Handle keydown to support accel-V.
   396     this.panel.addEventListener("keydown", this._onKeyDown.bind(this), false);
   397     // Handle keypress to be able to preventDefault() events before they reach
   398     // the richlistbox, for keyboard navigation.
   399     this.panel.addEventListener("keypress", this._onKeyPress.bind(this), false);
   400   },
   402   /**
   403    * Unattach event listeners that were added in _attachEventListeners. This
   404    * is called automatically on panel termination.
   405    */
   406   _unattachEventListeners: function DP__unattachEventListeners()
   407   {
   408     this.panel.removeEventListener("keydown", this._onKeyDown.bind(this),
   409                                    false);
   410     this.panel.removeEventListener("keypress", this._onKeyPress.bind(this),
   411                                    false);
   412   },
   414   _onKeyPress: function DP__onKeyPress(aEvent)
   415   {
   416     // Handle unmodified keys only.
   417     if (aEvent.altKey || aEvent.ctrlKey || aEvent.shiftKey || aEvent.metaKey) {
   418       return;
   419     }
   421     let richListBox = DownloadsView.richListBox;
   423     // If the user has pressed the tab, up, or down cursor key, start keyboard
   424     // navigation, thus enabling focusrings in the panel.  Keyboard navigation
   425     // is automatically disabled if the user moves the mouse on the panel, or
   426     // if the panel is closed.
   427     if ((aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_TAB ||
   428         aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP ||
   429         aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) &&
   430         !this.keyFocusing) {
   431       this.keyFocusing = true;
   432       // Ensure there's a selection, we will show the focus ring around it and
   433       // prevent the richlistbox from changing the selection.
   434       if (DownloadsView.richListBox.selectedIndex == -1)
   435         DownloadsView.richListBox.selectedIndex = 0;
   436       aEvent.preventDefault();
   437       return;
   438     }
   440     if (aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) {
   441       // If the last element in the list is selected, or the footer is already
   442       // focused, focus the footer.
   443       if (richListBox.selectedItem === richListBox.lastChild ||
   444           document.activeElement.parentNode.id === "downloadsFooter") {
   445         DownloadsFooter.focus();
   446         aEvent.preventDefault();
   447         return;
   448       }
   449     }
   451     // Pass keypress events to the richlistbox view when it's focused.
   452     if (document.activeElement === richListBox) {
   453       DownloadsView.onDownloadKeyPress(aEvent);
   454     }
   455   },
   457   /**
   458    * Keydown listener that listens for the keys to start key focusing, as well
   459    * as the the accel-V "paste" event, which initiates a file download if the
   460    * pasted item can be resolved to a URI.
   461    */
   462   _onKeyDown: function DP__onKeyDown(aEvent)
   463   {
   464     // If the footer is focused and the downloads list has at least 1 element
   465     // in it, focus the last element in the list when going up.
   466     if (aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP &&
   467         document.activeElement.parentNode.id === "downloadsFooter" &&
   468         DownloadsView.richListBox.firstChild) {
   469       DownloadsView.richListBox.focus();
   470       DownloadsView.richListBox.selectedItem = DownloadsView.richListBox.lastChild;
   471       aEvent.preventDefault();
   472       return;
   473     }
   475     let pasting = aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_V &&
   476 #ifdef XP_MACOSX
   477                   aEvent.metaKey;
   478 #else
   479                   aEvent.ctrlKey;
   480 #endif
   482     if (!pasting) {
   483       return;
   484     }
   486     DownloadsCommon.log("Received a paste event.");
   488     let trans = Cc["@mozilla.org/widget/transferable;1"]
   489                   .createInstance(Ci.nsITransferable);
   490     trans.init(null);
   491     let flavors = ["text/x-moz-url", "text/unicode"];
   492     flavors.forEach(trans.addDataFlavor);
   493     Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard);
   494     // Getting the data or creating the nsIURI might fail
   495     try {
   496       let data = {};
   497       trans.getAnyTransferData({}, data, {});
   498       let [url, name] = data.value
   499                             .QueryInterface(Ci.nsISupportsString)
   500                             .data
   501                             .split("\n");
   502       if (!url) {
   503         return;
   504       }
   506       let uri = NetUtil.newURI(url);
   507       DownloadsCommon.log("Pasted URL seems valid. Starting download.");
   508       DownloadURL(uri.spec, name, document);
   509     } catch (ex) {}
   510   },
   512   /**
   513    * Move focus to the main element in the downloads panel, unless another
   514    * element in the panel is already focused.
   515    */
   516   _focusPanel: function DP_focusPanel()
   517   {
   518     // We may be invoked while the panel is still waiting to be shown.
   519     if (this._state != this.kStateShown) {
   520       return;
   521     }
   523     let element = document.commandDispatcher.focusedElement;
   524     while (element && element != this.panel) {
   525       element = element.parentNode;
   526     }
   527     if (!element) {
   528       if (DownloadsView.richListBox.itemCount > 0) {
   529         DownloadsView.richListBox.focus();
   530       } else {
   531         DownloadsFooter.focus();
   532       }
   533     }
   534   },
   536   /**
   537    * Opens the downloads panel when data is ready to be displayed.
   538    */
   539   _openPopupIfDataReady: function DP_openPopupIfDataReady()
   540   {
   541     // We don't want to open the popup if we already displayed it, or if we are
   542     // still loading data.
   543     if (this._state != this.kStateWaitingData || DownloadsView.loading) {
   544       return;
   545     }
   547     this._state = this.kStateWaitingAnchor;
   549     // Ensure the anchor is visible.  If that is not possible, show the panel
   550     // anchored to the top area of the window, near the default anchor position.
   551     DownloadsButton.getAnchor(function DP_OPIDR_callback(aAnchor) {
   552       // If somehow we've switched states already (by getting a panel hiding
   553       // event before an overlay is loaded, for example), bail out.
   554       if (this._state != this.kStateWaitingAnchor)
   555         return;
   557       // At this point, if the window is minimized, opening the panel could fail
   558       // without any notification, and there would be no way to either open or
   559       // close the panel anymore.  To prevent this, check if the window is
   560       // minimized and in that case force the panel to the closed state.
   561       if (window.windowState == Ci.nsIDOMChromeWindow.STATE_MINIMIZED) {
   562         DownloadsButton.releaseAnchor();
   563         this._state = this.kStateHidden;
   564         return;
   565       }
   567       // When the panel is opened, we check if the target files of visible items
   568       // still exist, and update the allowed items interactions accordingly.  We
   569       // do these checks on a background thread, and don't prevent the panel to
   570       // be displayed while these checks are being performed.
   571       for each (let viewItem in DownloadsView._viewItems) {
   572         viewItem.verifyTargetExists();
   573       }
   575       if (aAnchor) {
   576         DownloadsCommon.log("Opening downloads panel popup.");
   577         this.panel.openPopup(aAnchor, "bottomcenter topright", 0, 0, false,
   578                              null);
   579       } else {
   580         DownloadsCommon.error("We can't find the anchor! Failure case - opening",
   581                               "downloads panel on TabsToolbar. We should never",
   582                               "get here!");
   583         Components.utils.reportError(
   584           "Downloads button cannot be found");
   585       }
   586     }.bind(this));
   587   }
   588 };
   590 ////////////////////////////////////////////////////////////////////////////////
   591 //// DownloadsOverlayLoader
   593 /**
   594  * Allows loading the downloads panel and the status indicator interfaces on
   595  * demand, to improve startup performance.
   596  */
   597 const DownloadsOverlayLoader = {
   598   /**
   599    * We cannot load two overlays at the same time, thus we use a queue of
   600    * pending load requests.
   601    */
   602   _loadRequests: [],
   604   /**
   605    * True while we are waiting for an overlay to be loaded.
   606    */
   607   _overlayLoading: false,
   609   /**
   610    * This object has a key for each overlay URI that is already loaded.
   611    */
   612   _loadedOverlays: {},
   614   /**
   615    * Loads the specified overlay and invokes the given callback when finished.
   616    *
   617    * @param aOverlay
   618    *        String containing the URI of the overlay to load in the current
   619    *        window.  If this overlay has already been loaded using this
   620    *        function, then the overlay is not loaded again.
   621    * @param aCallback
   622    *        Invoked when loading is completed.  If the overlay is already
   623    *        loaded, the function is called immediately.
   624    */
   625   ensureOverlayLoaded: function DOL_ensureOverlayLoaded(aOverlay, aCallback)
   626   {
   627     // The overlay is already loaded, invoke the callback immediately.
   628     if (aOverlay in this._loadedOverlays) {
   629       aCallback();
   630       return;
   631     }
   633     // The callback will be invoked when loading is finished.
   634     this._loadRequests.push({ overlay: aOverlay, callback: aCallback });
   635     if (this._overlayLoading) {
   636       return;
   637     }
   639     function DOL_EOL_loadCallback() {
   640       this._overlayLoading = false;
   641       this._loadedOverlays[aOverlay] = true;
   643       this.processPendingRequests();
   644     }
   646     this._overlayLoading = true;
   647     DownloadsCommon.log("Loading overlay ", aOverlay);
   648     document.loadOverlay(aOverlay, DOL_EOL_loadCallback.bind(this));
   649   },
   651   /**
   652    * Re-processes all the currently pending requests, invoking the callbacks
   653    * and/or loading more overlays as needed.  In most cases, there will be a
   654    * single request for one overlay, that will be processed immediately.
   655    */
   656   processPendingRequests: function DOL_processPendingRequests()
   657   {
   658     // Re-process all the currently pending requests, yet allow more requests
   659     // to be appended at the end of the array if we're not ready for them.
   660     let currentLength = this._loadRequests.length;
   661     for (let i = 0; i < currentLength; i++) {
   662       let request = this._loadRequests.shift();
   664       // We must call ensureOverlayLoaded again for each request, to check if
   665       // the associated callback can be invoked now, or if we must still wait
   666       // for the associated overlay to load.
   667       this.ensureOverlayLoaded(request.overlay, request.callback);
   668     }
   669   }
   670 };
   672 ////////////////////////////////////////////////////////////////////////////////
   673 //// DownloadsView
   675 /**
   676  * Builds and updates the downloads list widget, responding to changes in the
   677  * download state and real-time data.  In addition, handles part of the user
   678  * interaction events raised by the downloads list widget.
   679  */
   680 const DownloadsView = {
   681   //////////////////////////////////////////////////////////////////////////////
   682   //// Functions handling download items in the list
   684   /**
   685    * Maximum number of items shown by the list at any given time.
   686    */
   687   kItemCountLimit: 3,
   689   /**
   690    * Indicates whether we are still loading downloads data asynchronously.
   691    */
   692   loading: false,
   694   /**
   695    * Ordered array of all DownloadsDataItem objects.  We need to keep this array
   696    * because only a limited number of items are shown at once, and if an item
   697    * that is currently visible is removed from the list, we might need to take
   698    * another item from the array and make it appear at the bottom.
   699    */
   700   _dataItems: [],
   702   /**
   703    * Object containing the available DownloadsViewItem objects, indexed by their
   704    * numeric download identifier.  There is a limited number of view items in
   705    * the panel at any given time.
   706    */
   707   _viewItems: {},
   709   /**
   710    * Called when the number of items in the list changes.
   711    */
   712   _itemCountChanged: function DV_itemCountChanged()
   713   {
   714     DownloadsCommon.log("The downloads item count has changed - we are tracking",
   715                         this._dataItems.length, "downloads in total.");
   716     let count = this._dataItems.length;
   717     let hiddenCount = count - this.kItemCountLimit;
   719     if (count > 0) {
   720       DownloadsCommon.log("Setting the panel's hasdownloads attribute to true.");
   721       DownloadsPanel.panel.setAttribute("hasdownloads", "true");
   722     } else {
   723       DownloadsCommon.log("Removing the panel's hasdownloads attribute.");
   724       DownloadsPanel.panel.removeAttribute("hasdownloads");
   725     }
   727     // If we've got some hidden downloads, we should activate the
   728     // DownloadsSummary. The DownloadsSummary will determine whether or not
   729     // it's appropriate to actually display the summary.
   730     DownloadsSummary.active = hiddenCount > 0;
   731   },
   733   /**
   734    * Element corresponding to the list of downloads.
   735    */
   736   get richListBox()
   737   {
   738     delete this.richListBox;
   739     return this.richListBox = document.getElementById("downloadsListBox");
   740   },
   742   /**
   743    * Element corresponding to the button for showing more downloads.
   744    */
   745   get downloadsHistory()
   746   {
   747     delete this.downloadsHistory;
   748     return this.downloadsHistory = document.getElementById("downloadsHistory");
   749   },
   751   //////////////////////////////////////////////////////////////////////////////
   752   //// Callback functions from DownloadsData
   754   /**
   755    * Called before multiple downloads are about to be loaded.
   756    */
   757   onDataLoadStarting: function DV_onDataLoadStarting()
   758   {
   759     DownloadsCommon.log("onDataLoadStarting called for DownloadsView.");
   760     this.loading = true;
   761   },
   763   /**
   764    * Called after data loading finished.
   765    */
   766   onDataLoadCompleted: function DV_onDataLoadCompleted()
   767   {
   768     DownloadsCommon.log("onDataLoadCompleted called for DownloadsView.");
   770     this.loading = false;
   772     // We suppressed item count change notifications during the batch load, at
   773     // this point we should just call the function once.
   774     this._itemCountChanged();
   776     // Notify the panel that all the initially available downloads have been
   777     // loaded.  This ensures that the interface is visible, if still required.
   778     DownloadsPanel.onViewLoadCompleted();
   779   },
   781   /**
   782    * Called when a new download data item is available, either during the
   783    * asynchronous data load or when a new download is started.
   784    *
   785    * @param aDataItem
   786    *        DownloadsDataItem object that was just added.
   787    * @param aNewest
   788    *        When true, indicates that this item is the most recent and should be
   789    *        added in the topmost position.  This happens when a new download is
   790    *        started.  When false, indicates that the item is the least recent
   791    *        and should be appended.  The latter generally happens during the
   792    *        asynchronous data load.
   793    */
   794   onDataItemAdded: function DV_onDataItemAdded(aDataItem, aNewest)
   795   {
   796     DownloadsCommon.log("A new download data item was added - aNewest =",
   797                         aNewest);
   799     if (aNewest) {
   800       this._dataItems.unshift(aDataItem);
   801     } else {
   802       this._dataItems.push(aDataItem);
   803     }
   805     let itemsNowOverflow = this._dataItems.length > this.kItemCountLimit;
   806     if (aNewest || !itemsNowOverflow) {
   807       // The newly added item is visible in the panel and we must add the
   808       // corresponding element.  This is either because it is the first item, or
   809       // because it was added at the bottom but the list still doesn't overflow.
   810       this._addViewItem(aDataItem, aNewest);
   811     }
   812     if (aNewest && itemsNowOverflow) {
   813       // If the list overflows, remove the last item from the panel to make room
   814       // for the new one that we just added at the top.
   815       this._removeViewItem(this._dataItems[this.kItemCountLimit]);
   816     }
   818     // For better performance during batch loads, don't update the count for
   819     // every item, because the interface won't be visible until load finishes.
   820     if (!this.loading) {
   821       this._itemCountChanged();
   822     }
   823   },
   825   /**
   826    * Called when a data item is removed.  Ensures that the widget associated
   827    * with the view item is removed from the user interface.
   828    *
   829    * @param aDataItem
   830    *        DownloadsDataItem object that is being removed.
   831    */
   832   onDataItemRemoved: function DV_onDataItemRemoved(aDataItem)
   833   {
   834     DownloadsCommon.log("A download data item was removed.");
   836     let itemIndex = this._dataItems.indexOf(aDataItem);
   837     this._dataItems.splice(itemIndex, 1);
   839     if (itemIndex < this.kItemCountLimit) {
   840       // The item to remove is visible in the panel.
   841       this._removeViewItem(aDataItem);
   842       if (this._dataItems.length >= this.kItemCountLimit) {
   843         // Reinsert the next item into the panel.
   844         this._addViewItem(this._dataItems[this.kItemCountLimit - 1], false);
   845       }
   846     }
   848     this._itemCountChanged();
   849   },
   851   /**
   852    * Returns the view item associated with the provided data item for this view.
   853    *
   854    * @param aDataItem
   855    *        DownloadsDataItem object for which the view item is requested.
   856    *
   857    * @return Object that can be used to notify item status events.
   858    */
   859   getViewItem: function DV_getViewItem(aDataItem)
   860   {
   861     // If the item is visible, just return it, otherwise return a mock object
   862     // that doesn't react to notifications.
   863     if (aDataItem.downloadGuid in this._viewItems) {
   864       return this._viewItems[aDataItem.downloadGuid];
   865     }
   866     return this._invisibleViewItem;
   867   },
   869   /**
   870    * Mock DownloadsDataItem object that doesn't react to notifications.
   871    */
   872   _invisibleViewItem: Object.freeze({
   873     onStateChange: function () { },
   874     onProgressChange: function () { }
   875   }),
   877   /**
   878    * Creates a new view item associated with the specified data item, and adds
   879    * it to the top or the bottom of the list.
   880    */
   881   _addViewItem: function DV_addViewItem(aDataItem, aNewest)
   882   {
   883     DownloadsCommon.log("Adding a new DownloadsViewItem to the downloads list.",
   884                         "aNewest =", aNewest);
   886     let element = document.createElement("richlistitem");
   887     let viewItem = new DownloadsViewItem(aDataItem, element);
   888     this._viewItems[aDataItem.downloadGuid] = viewItem;
   889     if (aNewest) {
   890       this.richListBox.insertBefore(element, this.richListBox.firstChild);
   891     } else {
   892       this.richListBox.appendChild(element);
   893     }
   894   },
   896   /**
   897    * Removes the view item associated with the specified data item.
   898    */
   899   _removeViewItem: function DV_removeViewItem(aDataItem)
   900   {
   901     DownloadsCommon.log("Removing a DownloadsViewItem from the downloads list.");
   902     let element = this.getViewItem(aDataItem)._element;
   903     let previousSelectedIndex = this.richListBox.selectedIndex;
   904     this.richListBox.removeChild(element);
   905     if (previousSelectedIndex != -1) {
   906       this.richListBox.selectedIndex = Math.min(previousSelectedIndex,
   907                                                 this.richListBox.itemCount - 1);
   908     }
   909     delete this._viewItems[aDataItem.downloadGuid];
   910   },
   912   //////////////////////////////////////////////////////////////////////////////
   913   //// User interface event functions
   915   /**
   916    * Helper function to do commands on a specific download item.
   917    *
   918    * @param aEvent
   919    *        Event object for the event being handled.  If the event target is
   920    *        not a richlistitem that represents a download, this function will
   921    *        walk up the parent nodes until it finds a DOM node that is.
   922    * @param aCommand
   923    *        The command to be performed.
   924    */
   925   onDownloadCommand: function DV_onDownloadCommand(aEvent, aCommand)
   926   {
   927     let target = aEvent.target;
   928     while (target.nodeName != "richlistitem") {
   929       target = target.parentNode;
   930     }
   931     new DownloadsViewItemController(target).doCommand(aCommand);
   932   },
   934   onDownloadClick: function DV_onDownloadClick(aEvent)
   935   {
   936     // Handle primary clicks only, and exclude the action button.
   937     if (aEvent.button == 0 &&
   938         !aEvent.originalTarget.hasAttribute("oncommand")) {
   939       goDoCommand("downloadsCmd_open");
   940     }
   941   },
   943   /**
   944    * Handles keypress events on a download item.
   945    */
   946   onDownloadKeyPress: function DV_onDownloadKeyPress(aEvent)
   947   {
   948     // Pressing the key on buttons should not invoke the action because the
   949     // event has already been handled by the button itself.
   950     if (aEvent.originalTarget.hasAttribute("command") ||
   951         aEvent.originalTarget.hasAttribute("oncommand")) {
   952       return;
   953     }
   955     if (aEvent.charCode == " ".charCodeAt(0)) {
   956       goDoCommand("downloadsCmd_pauseResume");
   957       return;
   958     }
   960     if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
   961       goDoCommand("downloadsCmd_doDefault");
   962     }
   963   },
   966   /**
   967    * Mouse listeners to handle selection on hover.
   968    */
   969   onDownloadMouseOver: function DV_onDownloadMouseOver(aEvent)
   970   {
   971     if (aEvent.originalTarget.parentNode == this.richListBox)
   972       this.richListBox.selectedItem = aEvent.originalTarget;
   973   },
   974   onDownloadMouseOut: function DV_onDownloadMouseOut(aEvent)
   975   {
   976     if (aEvent.originalTarget.parentNode == this.richListBox) {
   977       // If the destination element is outside of the richlistitem, clear the
   978       // selection.
   979       let element = aEvent.relatedTarget;
   980       while (element && element != aEvent.originalTarget) {
   981         element = element.parentNode;
   982       }
   983       if (!element)
   984         this.richListBox.selectedIndex = -1;
   985     }
   986   },
   988   onDownloadContextMenu: function DV_onDownloadContextMenu(aEvent)
   989   {
   990     let element = this.richListBox.selectedItem;
   991     if (!element) {
   992       return;
   993     }
   995     DownloadsViewController.updateCommands();
   997     // Set the state attribute so that only the appropriate items are displayed.
   998     let contextMenu = document.getElementById("downloadsContextMenu");
   999     contextMenu.setAttribute("state", element.getAttribute("state"));
  1000   },
  1002   onDownloadDragStart: function DV_onDownloadDragStart(aEvent)
  1004     let element = this.richListBox.selectedItem;
  1005     if (!element) {
  1006       return;
  1009     let controller = new DownloadsViewItemController(element);
  1010     let localFile = controller.dataItem.localFile;
  1011     if (!localFile.exists()) {
  1012       return;
  1015     let dataTransfer = aEvent.dataTransfer;
  1016     dataTransfer.mozSetDataAt("application/x-moz-file", localFile, 0);
  1017     dataTransfer.effectAllowed = "copyMove";
  1018     var url = Services.io.newFileURI(localFile).spec;
  1019     dataTransfer.setData("text/uri-list", url);
  1020     dataTransfer.setData("text/plain", url);
  1021     dataTransfer.addElement(element);
  1023     aEvent.stopPropagation();
  1027 ////////////////////////////////////////////////////////////////////////////////
  1028 //// DownloadsViewItem
  1030 /**
  1031  * Builds and updates a single item in the downloads list widget, responding to
  1032  * changes in the download state and real-time data.
  1034  * @param aDataItem
  1035  *        DownloadsDataItem to be associated with the view item.
  1036  * @param aElement
  1037  *        XUL element corresponding to the single download item in the view.
  1038  */
  1039 function DownloadsViewItem(aDataItem, aElement)
  1041   this._element = aElement;
  1042   this.dataItem = aDataItem;
  1044   this.lastEstimatedSecondsLeft = Infinity;
  1046   // Set the URI that represents the correct icon for the target file.  As soon
  1047   // as bug 239948 comment 12 is handled, the "file" property will be always a
  1048   // file URL rather than a file name.  At that point we should remove the "//"
  1049   // (double slash) from the icon URI specification (see test_moz_icon_uri.js).
  1050   this.image = "moz-icon://" + this.dataItem.file + "?size=32";
  1052   let attributes = {
  1053     "type": "download",
  1054     "class": "download-state",
  1055     "id": "downloadsItem_" + this.dataItem.downloadGuid,
  1056     "downloadGuid": this.dataItem.downloadGuid,
  1057     "state": this.dataItem.state,
  1058     "progress": this.dataItem.inProgress ? this.dataItem.percentComplete : 100,
  1059     "target": this.dataItem.target,
  1060     "image": this.image
  1061   };
  1063   for (let attributeName in attributes) {
  1064     this._element.setAttribute(attributeName, attributes[attributeName]);
  1067   // Initialize more complex attributes.
  1068   this._updateProgress();
  1069   this._updateStatusLine();
  1070   this.verifyTargetExists();
  1073 DownloadsViewItem.prototype = {
  1074   /**
  1075    * The DownloadDataItem associated with this view item.
  1076    */
  1077   dataItem: null,
  1079   /**
  1080    * The XUL element corresponding to the associated richlistbox item.
  1081    */
  1082   _element: null,
  1084   /**
  1085    * The inner XUL element for the progress bar, or null if not available.
  1086    */
  1087   _progressElement: null,
  1089   //////////////////////////////////////////////////////////////////////////////
  1090   //// Callback functions from DownloadsData
  1092   /**
  1093    * Called when the download state might have changed.  Sometimes the state of
  1094    * the download might be the same as before, if the data layer received
  1095    * multiple events for the same download.
  1096    */
  1097   onStateChange: function DVI_onStateChange(aOldState)
  1099     // If a download just finished successfully, it means that the target file
  1100     // now exists and we can extract its specific icon.  To ensure that the icon
  1101     // is reloaded, we must change the URI used by the XUL image element, for
  1102     // example by adding a query parameter.  Since this URI has a "moz-icon"
  1103     // scheme, this only works if we add one of the parameters explicitly
  1104     // supported by the nsIMozIconURI interface.
  1105     if (aOldState != Ci.nsIDownloadManager.DOWNLOAD_FINISHED &&
  1106         aOldState != this.dataItem.state) {
  1107       this._element.setAttribute("image", this.image + "&state=normal");
  1109       // We assume the existence of the target of a download that just completed
  1110       // successfully, without checking the condition in the background.  If the
  1111       // panel is already open, this will take effect immediately.  If the panel
  1112       // is opened later, a new background existence check will be performed.
  1113       this._element.setAttribute("exists", "true");
  1116     // Update the user interface after switching states.
  1117     this._element.setAttribute("state", this.dataItem.state);
  1118     this._updateProgress();
  1119     this._updateStatusLine();
  1120   },
  1122   /**
  1123    * Called when the download progress has changed.
  1124    */
  1125   onProgressChange: function DVI_onProgressChange() {
  1126     this._updateProgress();
  1127     this._updateStatusLine();
  1128   },
  1130   //////////////////////////////////////////////////////////////////////////////
  1131   //// Functions for updating the user interface
  1133   /**
  1134    * Updates the progress bar.
  1135    */
  1136   _updateProgress: function DVI_updateProgress() {
  1137     if (this.dataItem.starting) {
  1138       // Before the download starts, the progress meter has its initial value.
  1139       this._element.setAttribute("progressmode", "normal");
  1140       this._element.setAttribute("progress", "0");
  1141     } else if (this.dataItem.state == Ci.nsIDownloadManager.DOWNLOAD_SCANNING ||
  1142                this.dataItem.percentComplete == -1) {
  1143       // We might not know the progress of a running download, and we don't know
  1144       // the remaining time during the malware scanning phase.
  1145       this._element.setAttribute("progressmode", "undetermined");
  1146     } else {
  1147       // This is a running download of which we know the progress.
  1148       this._element.setAttribute("progressmode", "normal");
  1149       this._element.setAttribute("progress", this.dataItem.percentComplete);
  1152     // Find the progress element as soon as the download binding is accessible.
  1153     if (!this._progressElement) {
  1154       this._progressElement =
  1155            document.getAnonymousElementByAttribute(this._element, "anonid",
  1156                                                    "progressmeter");
  1159     // Dispatch the ValueChange event for accessibility, if possible.
  1160     if (this._progressElement) {
  1161       let event = document.createEvent("Events");
  1162       event.initEvent("ValueChange", true, true);
  1163       this._progressElement.dispatchEvent(event);
  1165   },
  1167   /**
  1168    * Updates the main status line, including bytes transferred, bytes total,
  1169    * download rate, and time remaining.
  1170    */
  1171   _updateStatusLine: function DVI_updateStatusLine() {
  1172     const nsIDM = Ci.nsIDownloadManager;
  1174     let status = "";
  1175     let statusTip = "";
  1177     if (this.dataItem.paused) {
  1178       let transfer = DownloadUtils.getTransferTotal(this.dataItem.currBytes,
  1179                                                     this.dataItem.maxBytes);
  1181       // We use the same XUL label to display both the state and the amount
  1182       // transferred, for example "Paused -  1.1 MB".
  1183       status = DownloadsCommon.strings.statusSeparatorBeforeNumber(
  1184                                             DownloadsCommon.strings.statePaused,
  1185                                             transfer);
  1186     } else if (this.dataItem.state == nsIDM.DOWNLOAD_DOWNLOADING) {
  1187       // We don't show the rate for each download in order to reduce clutter.
  1188       // The remaining time per download is likely enough information for the
  1189       // panel.
  1190       [status] =
  1191         DownloadUtils.getDownloadStatusNoRate(this.dataItem.currBytes,
  1192                                               this.dataItem.maxBytes,
  1193                                               this.dataItem.speed,
  1194                                               this.lastEstimatedSecondsLeft);
  1196       // We are, however, OK with displaying the rate in the tooltip.
  1197       let newEstimatedSecondsLeft;
  1198       [statusTip, newEstimatedSecondsLeft] =
  1199         DownloadUtils.getDownloadStatus(this.dataItem.currBytes,
  1200                                         this.dataItem.maxBytes,
  1201                                         this.dataItem.speed,
  1202                                         this.lastEstimatedSecondsLeft);
  1203       this.lastEstimatedSecondsLeft = newEstimatedSecondsLeft;
  1204     } else if (this.dataItem.starting) {
  1205       status = DownloadsCommon.strings.stateStarting;
  1206     } else if (this.dataItem.state == nsIDM.DOWNLOAD_SCANNING) {
  1207       status = DownloadsCommon.strings.stateScanning;
  1208     } else if (!this.dataItem.inProgress) {
  1209       let stateLabel = function () {
  1210         let s = DownloadsCommon.strings;
  1211         switch (this.dataItem.state) {
  1212           case nsIDM.DOWNLOAD_FAILED:           return s.stateFailed;
  1213           case nsIDM.DOWNLOAD_CANCELED:         return s.stateCanceled;
  1214           case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: return s.stateBlockedParentalControls;
  1215           case nsIDM.DOWNLOAD_BLOCKED_POLICY:   return s.stateBlockedPolicy;
  1216           case nsIDM.DOWNLOAD_DIRTY:            return s.stateDirty;
  1217           case nsIDM.DOWNLOAD_FINISHED:         return this._fileSizeText;
  1219         return null;
  1220       }.apply(this);
  1222       let [displayHost, fullHost] =
  1223         DownloadUtils.getURIHost(this.dataItem.referrer || this.dataItem.uri);
  1225       let end = new Date(this.dataItem.endTime);
  1226       let [displayDate, fullDate] = DownloadUtils.getReadableDates(end);
  1228       // We use the same XUL label to display the state, the host name, and the
  1229       // end time, for example "Canceled - 222.net - 11:15" or "1.1 MB -
  1230       // website2.com - Yesterday".  We show the full host and the complete date
  1231       // in the tooltip.
  1232       let firstPart = DownloadsCommon.strings.statusSeparator(stateLabel,
  1233                                                               displayHost);
  1234       status = DownloadsCommon.strings.statusSeparator(firstPart, displayDate);
  1235       statusTip = DownloadsCommon.strings.statusSeparator(fullHost, fullDate);
  1238     this._element.setAttribute("status", status);
  1239     this._element.setAttribute("statusTip", statusTip || status);
  1240   },
  1242   /**
  1243    * Localized string representing the total size of completed downloads, for
  1244    * example "1.5 MB" or "Unknown size".
  1245    */
  1246   get _fileSizeText()
  1248     // Display the file size, but show "Unknown" for negative sizes.
  1249     let fileSize = this.dataItem.maxBytes;
  1250     if (fileSize < 0) {
  1251       return DownloadsCommon.strings.sizeUnknown;
  1253     let [size, unit] = DownloadUtils.convertByteUnits(fileSize);
  1254     return DownloadsCommon.strings.sizeWithUnits(size, unit);
  1255   },
  1257   //////////////////////////////////////////////////////////////////////////////
  1258   //// Functions called by the panel
  1260   /**
  1261    * Starts checking whether the target file of a finished download is still
  1262    * available on disk, and sets an attribute that controls how the item is
  1263    * presented visually.
  1265    * The existence check is executed on a background thread.
  1266    */
  1267   verifyTargetExists: function DVI_verifyTargetExists() {
  1268     // We don't need to check if the download is not finished successfully.
  1269     if (!this.dataItem.openable) {
  1270       return;
  1273     OS.File.exists(this.dataItem.localFile.path).then(
  1274       function DVI_RTE_onSuccess(aExists) {
  1275         if (aExists) {
  1276           this._element.setAttribute("exists", "true");
  1277         } else {
  1278           this._element.removeAttribute("exists");
  1280       }.bind(this), Cu.reportError);
  1281   },
  1282 };
  1284 ////////////////////////////////////////////////////////////////////////////////
  1285 //// DownloadsViewController
  1287 /**
  1288  * Handles part of the user interaction events raised by the downloads list
  1289  * widget, in particular the "commands" that apply to multiple items, and
  1290  * dispatches the commands that apply to individual items.
  1291  */
  1292 const DownloadsViewController = {
  1293   //////////////////////////////////////////////////////////////////////////////
  1294   //// Initialization and termination
  1296   initialize: function DVC_initialize()
  1298     window.controllers.insertControllerAt(0, this);
  1299   },
  1301   terminate: function DVC_terminate()
  1303     window.controllers.removeController(this);
  1304   },
  1306   //////////////////////////////////////////////////////////////////////////////
  1307   //// nsIController
  1309   supportsCommand: function DVC_supportsCommand(aCommand)
  1311     // Firstly, determine if this is a command that we can handle.
  1312     if (!(aCommand in this.commands) &&
  1313         !(aCommand in DownloadsViewItemController.prototype.commands)) {
  1314       return false;
  1316     // Secondly, determine if focus is on a control in the downloads list.
  1317     let element = document.commandDispatcher.focusedElement;
  1318     while (element && element != DownloadsView.richListBox) {
  1319       element = element.parentNode;
  1321     // We should handle the command only if the downloads list is among the
  1322     // ancestors of the focused element.
  1323     return !!element;
  1324   },
  1326   isCommandEnabled: function DVC_isCommandEnabled(aCommand)
  1328     // Handle commands that are not selection-specific.
  1329     if (aCommand == "downloadsCmd_clearList") {
  1330       return DownloadsCommon.getData(window).canRemoveFinished;
  1333     // Other commands are selection-specific.
  1334     let element = DownloadsView.richListBox.selectedItem;
  1335     return element &&
  1336            new DownloadsViewItemController(element).isCommandEnabled(aCommand);
  1337   },
  1339   doCommand: function DVC_doCommand(aCommand)
  1341     // If this command is not selection-specific, execute it.
  1342     if (aCommand in this.commands) {
  1343       this.commands[aCommand].apply(this);
  1344       return;
  1347     // Other commands are selection-specific.
  1348     let element = DownloadsView.richListBox.selectedItem;
  1349     if (element) {
  1350       // The doCommand function also checks if the command is enabled.
  1351       new DownloadsViewItemController(element).doCommand(aCommand);
  1353   },
  1355   onEvent: function () { },
  1357   //////////////////////////////////////////////////////////////////////////////
  1358   //// Other functions
  1360   updateCommands: function DVC_updateCommands()
  1362     Object.keys(this.commands).forEach(goUpdateCommand);
  1363     Object.keys(DownloadsViewItemController.prototype.commands)
  1364           .forEach(goUpdateCommand);
  1365   },
  1367   //////////////////////////////////////////////////////////////////////////////
  1368   //// Selection-independent commands
  1370   /**
  1371    * This object contains one key for each command that operates regardless of
  1372    * the currently selected item in the list.
  1373    */
  1374   commands: {
  1375     downloadsCmd_clearList: function DVC_downloadsCmd_clearList()
  1377       DownloadsCommon.getData(window).removeFinished();
  1380 };
  1382 ////////////////////////////////////////////////////////////////////////////////
  1383 //// DownloadsViewItemController
  1385 /**
  1386  * Handles all the user interaction events, in particular the "commands",
  1387  * related to a single item in the downloads list widgets.
  1388  */
  1389 function DownloadsViewItemController(aElement) {
  1390   let downloadGuid = aElement.getAttribute("downloadGuid");
  1391   this.dataItem = DownloadsCommon.getData(window).dataItems[downloadGuid];
  1394 DownloadsViewItemController.prototype = {
  1395   //////////////////////////////////////////////////////////////////////////////
  1396   //// Command dispatching
  1398   /**
  1399    * The DownloadDataItem controlled by this object.
  1400    */
  1401   dataItem: null,
  1403   isCommandEnabled: function DVIC_isCommandEnabled(aCommand)
  1405     switch (aCommand) {
  1406       case "downloadsCmd_open": {
  1407         return this.dataItem.openable && this.dataItem.localFile.exists();
  1409       case "downloadsCmd_show": {
  1410         return this.dataItem.localFile.exists() ||
  1411                this.dataItem.partFile.exists();
  1413       case "downloadsCmd_pauseResume":
  1414         return this.dataItem.inProgress && this.dataItem.resumable;
  1415       case "downloadsCmd_retry":
  1416         return this.dataItem.canRetry;
  1417       case "downloadsCmd_openReferrer":
  1418         return !!this.dataItem.referrer;
  1419       case "cmd_delete":
  1420       case "downloadsCmd_cancel":
  1421       case "downloadsCmd_copyLocation":
  1422       case "downloadsCmd_doDefault":
  1423         return true;
  1425     return false;
  1426   },
  1428   doCommand: function DVIC_doCommand(aCommand)
  1430     if (this.isCommandEnabled(aCommand)) {
  1431       this.commands[aCommand].apply(this);
  1433   },
  1435   //////////////////////////////////////////////////////////////////////////////
  1436   //// Item commands
  1438   /**
  1439    * This object contains one key for each command that operates on this item.
  1441    * In commands, the "this" identifier points to the controller item.
  1442    */
  1443   commands: {
  1444     cmd_delete: function DVIC_cmd_delete()
  1446       this.dataItem.remove();
  1447       PlacesUtils.bhistory.removePage(NetUtil.newURI(this.dataItem.uri));
  1448     },
  1450     downloadsCmd_cancel: function DVIC_downloadsCmd_cancel()
  1452       this.dataItem.cancel();
  1453     },
  1455     downloadsCmd_open: function DVIC_downloadsCmd_open()
  1457       this.dataItem.openLocalFile();
  1459       // We explicitly close the panel here to give the user the feedback that
  1460       // their click has been received, and we're handling the action.
  1461       // Otherwise, we'd have to wait for the file-type handler to execute
  1462       // before the panel would close. This also helps to prevent the user from
  1463       // accidentally opening a file several times.
  1464       DownloadsPanel.hidePanel();
  1465     },
  1467     downloadsCmd_show: function DVIC_downloadsCmd_show()
  1469       this.dataItem.showLocalFile();
  1471       // We explicitly close the panel here to give the user the feedback that
  1472       // their click has been received, and we're handling the action.
  1473       // Otherwise, we'd have to wait for the operating system file manager
  1474       // window to open before the panel closed. This also helps to prevent the
  1475       // user from opening the containing folder several times.
  1476       DownloadsPanel.hidePanel();
  1477     },
  1479     downloadsCmd_pauseResume: function DVIC_downloadsCmd_pauseResume()
  1481       this.dataItem.togglePauseResume();
  1482     },
  1484     downloadsCmd_retry: function DVIC_downloadsCmd_retry()
  1486       this.dataItem.retry();
  1487     },
  1489     downloadsCmd_openReferrer: function DVIC_downloadsCmd_openReferrer()
  1491       openURL(this.dataItem.referrer);
  1492     },
  1494     downloadsCmd_copyLocation: function DVIC_downloadsCmd_copyLocation()
  1496       let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
  1497                       .getService(Ci.nsIClipboardHelper);
  1498       clipboard.copyString(this.dataItem.uri, document);
  1499     },
  1501     downloadsCmd_doDefault: function DVIC_downloadsCmd_doDefault()
  1503       const nsIDM = Ci.nsIDownloadManager;
  1505       // Determine the default command for the current item.
  1506       let defaultCommand = function () {
  1507         switch (this.dataItem.state) {
  1508           case nsIDM.DOWNLOAD_NOTSTARTED:       return "downloadsCmd_cancel";
  1509           case nsIDM.DOWNLOAD_FINISHED:         return "downloadsCmd_open";
  1510           case nsIDM.DOWNLOAD_FAILED:           return "downloadsCmd_retry";
  1511           case nsIDM.DOWNLOAD_CANCELED:         return "downloadsCmd_retry";
  1512           case nsIDM.DOWNLOAD_PAUSED:           return "downloadsCmd_pauseResume";
  1513           case nsIDM.DOWNLOAD_QUEUED:           return "downloadsCmd_cancel";
  1514           case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: return "downloadsCmd_openReferrer";
  1515           case nsIDM.DOWNLOAD_SCANNING:         return "downloadsCmd_show";
  1516           case nsIDM.DOWNLOAD_DIRTY:            return "downloadsCmd_openReferrer";
  1517           case nsIDM.DOWNLOAD_BLOCKED_POLICY:   return "downloadsCmd_openReferrer";
  1519         return "";
  1520       }.apply(this);
  1521       if (defaultCommand && this.isCommandEnabled(defaultCommand))
  1522         this.doCommand(defaultCommand);
  1525 };
  1528 ////////////////////////////////////////////////////////////////////////////////
  1529 //// DownloadsSummary
  1531 /**
  1532  * Manages the summary at the bottom of the downloads panel list if the number
  1533  * of items in the list exceeds the panels limit.
  1534  */
  1535 const DownloadsSummary = {
  1537   /**
  1538    * Sets the active state of the summary. When active, the summary subscribes
  1539    * to the DownloadsCommon DownloadsSummaryData singleton.
  1541    * @param aActive
  1542    *        Set to true to activate the summary.
  1543    */
  1544   set active(aActive)
  1546     if (aActive == this._active || !this._summaryNode) {
  1547       return this._active;
  1549     if (aActive) {
  1550       DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit)
  1551                      .refreshView(this);
  1552     } else {
  1553       DownloadsFooter.showingSummary = false;
  1556     return this._active = aActive;
  1557   },
  1559   /**
  1560    * Returns the active state of the downloads summary.
  1561    */
  1562   get active() this._active,
  1564   _active: false,
  1566   /**
  1567    * Sets whether or not we show the progress bar.
  1569    * @param aShowingProgress
  1570    *        True if we should show the progress bar.
  1571    */
  1572   set showingProgress(aShowingProgress)
  1574     if (aShowingProgress) {
  1575       this._summaryNode.setAttribute("inprogress", "true");
  1576     } else {
  1577       this._summaryNode.removeAttribute("inprogress");
  1579     // If progress isn't being shown, then we simply do not show the summary.
  1580     return DownloadsFooter.showingSummary = aShowingProgress;
  1581   },
  1583   /**
  1584    * Sets the amount of progress that is visible in the progress bar.
  1586    * @param aValue
  1587    *        A value between 0 and 100 to represent the progress of the
  1588    *        summarized downloads.
  1589    */
  1590   set percentComplete(aValue)
  1592     if (this._progressNode) {
  1593       this._progressNode.setAttribute("value", aValue);
  1595     return aValue;
  1596   },
  1598   /**
  1599    * Sets the description for the download summary.
  1601    * @param aValue
  1602    *        A string representing the description of the summarized
  1603    *        downloads.
  1604    */
  1605   set description(aValue)
  1607     if (this._descriptionNode) {
  1608       this._descriptionNode.setAttribute("value", aValue);
  1609       this._descriptionNode.setAttribute("tooltiptext", aValue);
  1611     return aValue;
  1612   },
  1614   /**
  1615    * Sets the details for the download summary, such as the time remaining,
  1616    * the amount of bytes transferred, etc.
  1618    * @param aValue
  1619    *        A string representing the details of the summarized
  1620    *        downloads.
  1621    */
  1622   set details(aValue)
  1624     if (this._detailsNode) {
  1625       this._detailsNode.setAttribute("value", aValue);
  1626       this._detailsNode.setAttribute("tooltiptext", aValue);
  1628     return aValue;
  1629   },
  1631   /**
  1632    * Focuses the root element of the summary.
  1633    */
  1634   focus: function()
  1636     if (this._summaryNode) {
  1637       this._summaryNode.focus();
  1639   },
  1641   /**
  1642    * Respond to keydown events on the Downloads Summary node.
  1644    * @param aEvent
  1645    *        The keydown event being handled.
  1646    */
  1647   onKeyDown: function DS_onKeyDown(aEvent)
  1649     if (aEvent.charCode == " ".charCodeAt(0) ||
  1650         aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
  1651       DownloadsPanel.showDownloadsHistory();
  1653   },
  1655   /**
  1656    * Respond to click events on the Downloads Summary node.
  1658    * @param aEvent
  1659    *        The click event being handled.
  1660    */
  1661   onClick: function DS_onClick(aEvent)
  1663     DownloadsPanel.showDownloadsHistory();
  1664   },
  1666   /**
  1667    * Element corresponding to the root of the downloads summary.
  1668    */
  1669   get _summaryNode()
  1671     let node = document.getElementById("downloadsSummary");
  1672     if (!node) {
  1673       return null;
  1675     delete this._summaryNode;
  1676     return this._summaryNode = node;
  1677   },
  1679   /**
  1680    * Element corresponding to the progress bar in the downloads summary.
  1681    */
  1682   get _progressNode()
  1684     let node = document.getElementById("downloadsSummaryProgress");
  1685     if (!node) {
  1686       return null;
  1688     delete this._progressNode;
  1689     return this._progressNode = node;
  1690   },
  1692   /**
  1693    * Element corresponding to the main description of the downloads
  1694    * summary.
  1695    */
  1696   get _descriptionNode()
  1698     let node = document.getElementById("downloadsSummaryDescription");
  1699     if (!node) {
  1700       return null;
  1702     delete this._descriptionNode;
  1703     return this._descriptionNode = node;
  1704   },
  1706   /**
  1707    * Element corresponding to the secondary description of the downloads
  1708    * summary.
  1709    */
  1710   get _detailsNode()
  1712     let node = document.getElementById("downloadsSummaryDetails");
  1713     if (!node) {
  1714       return null;
  1716     delete this._detailsNode;
  1717     return this._detailsNode = node;
  1721 ////////////////////////////////////////////////////////////////////////////////
  1722 //// DownloadsFooter
  1724 /**
  1725  * Manages events sent to to the footer vbox, which contains both the
  1726  * DownloadsSummary as well as the "Show All Downloads" button.
  1727  */
  1728 const DownloadsFooter = {
  1730   /**
  1731    * Focuses the appropriate element within the footer. If the summary
  1732    * is visible, focus it. If not, focus the "Show All Downloads"
  1733    * button.
  1734    */
  1735   focus: function DF_focus()
  1737     if (this._showingSummary) {
  1738       DownloadsSummary.focus();
  1739     } else {
  1740       DownloadsView.downloadsHistory.focus();
  1742   },
  1744   _showingSummary: false,
  1746   /**
  1747    * Sets whether or not the Downloads Summary should be displayed in the
  1748    * footer. If not, the "Show All Downloads" button is shown instead.
  1749    */
  1750   set showingSummary(aValue)
  1752     if (this._footerNode) {
  1753       if (aValue) {
  1754         this._footerNode.setAttribute("showingsummary", "true");
  1755       } else {
  1756         this._footerNode.removeAttribute("showingsummary");
  1758       this._showingSummary = aValue;
  1760     return aValue;
  1761   },
  1763   /**
  1764    * Element corresponding to the footer of the downloads panel.
  1765    */
  1766   get _footerNode()
  1768     let node = document.getElementById("downloadsFooter");
  1769     if (!node) {
  1770       return null;
  1772     delete this._footerNode;
  1773     return this._footerNode = node;
  1775 };

mercurial