browser/components/downloads/content/downloads.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/components/downloads/content/downloads.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1775 @@
     1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* vim: set ts=2 et sw=2 tw=80: */
     1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.8 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.9 +
    1.10 +/**
    1.11 + * Handles the Downloads panel user interface for each browser window.
    1.12 + *
    1.13 + * This file includes the following constructors and global objects:
    1.14 + *
    1.15 + * DownloadsPanel
    1.16 + * Main entry point for the downloads panel interface.
    1.17 + *
    1.18 + * DownloadsOverlayLoader
    1.19 + * Allows loading the downloads panel and the status indicator interfaces on
    1.20 + * demand, to improve startup performance.
    1.21 + *
    1.22 + * DownloadsView
    1.23 + * Builds and updates the downloads list widget, responding to changes in the
    1.24 + * download state and real-time data.  In addition, handles part of the user
    1.25 + * interaction events raised by the downloads list widget.
    1.26 + *
    1.27 + * DownloadsViewItem
    1.28 + * Builds and updates a single item in the downloads list widget, responding to
    1.29 + * changes in the download state and real-time data.
    1.30 + *
    1.31 + * DownloadsViewController
    1.32 + * Handles part of the user interaction events raised by the downloads list
    1.33 + * widget, in particular the "commands" that apply to multiple items, and
    1.34 + * dispatches the commands that apply to individual items.
    1.35 + *
    1.36 + * DownloadsViewItemController
    1.37 + * Handles all the user interaction events, in particular the "commands",
    1.38 + * related to a single item in the downloads list widgets.
    1.39 + */
    1.40 +
    1.41 +/**
    1.42 + * A few words on focus and focusrings
    1.43 + *
    1.44 + * We do quite a few hacks in the Downloads Panel for focusrings. In fact, we
    1.45 + * basically suppress most if not all XUL-level focusrings, and style/draw
    1.46 + * them ourselves (using :focus instead of -moz-focusring). There are a few
    1.47 + * reasons for this:
    1.48 + *
    1.49 + * 1) Richlists on OSX don't have focusrings; instead, they are shown as
    1.50 + *    selected. This makes for some ambiguity when we have a focused/selected
    1.51 + *    item in the list, and the mouse is hovering a completed download (which
    1.52 + *    highlights).
    1.53 + * 2) Windows doesn't show focusrings until after the first time that tab is
    1.54 + *    pressed (and by then you're focusing the second item in the panel).
    1.55 + * 3) Richlistbox sets -moz-focusring even when we select it with a mouse.
    1.56 + *
    1.57 + * In general, the desired behaviour is to focus the first item after pressing
    1.58 + * tab/down, and show that focus with a ring. Then, if the mouse moves over
    1.59 + * the panel, to hide that focus ring; essentially resetting us to the state
    1.60 + * before pressing the key.
    1.61 + *
    1.62 + * We end up capturing the tab/down key events, and preventing their default
    1.63 + * behaviour. We then set a "keyfocus" attribute on the panel, which allows
    1.64 + * us to draw a ring around the currently focused element. If the panel is
    1.65 + * closed or the mouse moves over the panel, we remove the attribute.
    1.66 + */
    1.67 +
    1.68 +"use strict";
    1.69 +
    1.70 +////////////////////////////////////////////////////////////////////////////////
    1.71 +//// Globals
    1.72 +
    1.73 +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
    1.74 +                                  "resource://gre/modules/DownloadUtils.jsm");
    1.75 +XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
    1.76 +                                  "resource:///modules/DownloadsCommon.jsm");
    1.77 +XPCOMUtils.defineLazyModuleGetter(this, "OS",
    1.78 +                                  "resource://gre/modules/osfile.jsm");
    1.79 +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
    1.80 +                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
    1.81 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
    1.82 +                                  "resource://gre/modules/PlacesUtils.jsm");
    1.83 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
    1.84 +                                  "resource://gre/modules/NetUtil.jsm");
    1.85 +
    1.86 +////////////////////////////////////////////////////////////////////////////////
    1.87 +//// DownloadsPanel
    1.88 +
    1.89 +/**
    1.90 + * Main entry point for the downloads panel interface.
    1.91 + */
    1.92 +const DownloadsPanel = {
    1.93 +  //////////////////////////////////////////////////////////////////////////////
    1.94 +  //// Initialization and termination
    1.95 +
    1.96 +  /**
    1.97 +   * Internal state of the downloads panel, based on one of the kState
    1.98 +   * constants.  This is not the same state as the XUL panel element.
    1.99 +   */
   1.100 +  _state: 0,
   1.101 +
   1.102 +  /** The panel is not linked to downloads data yet. */
   1.103 +  get kStateUninitialized() 0,
   1.104 +  /** This object is linked to data, but the panel is invisible. */
   1.105 +  get kStateHidden() 1,
   1.106 +  /** The panel will be shown as soon as possible. */
   1.107 +  get kStateWaitingData() 2,
   1.108 +  /** The panel is almost shown - we're just waiting to get a handle on the
   1.109 +      anchor. */
   1.110 +  get kStateWaitingAnchor() 3,
   1.111 +  /** The panel is open. */
   1.112 +  get kStateShown() 4,
   1.113 +
   1.114 +  /**
   1.115 +   * Location of the panel overlay.
   1.116 +   */
   1.117 +  get kDownloadsOverlay()
   1.118 +      "chrome://browser/content/downloads/downloadsOverlay.xul",
   1.119 +
   1.120 +  /**
   1.121 +   * Starts loading the download data in background, without opening the panel.
   1.122 +   * Use showPanel instead to load the data and open the panel at the same time.
   1.123 +   *
   1.124 +   * @param aCallback
   1.125 +   *        Called when initialization is complete.
   1.126 +   */
   1.127 +  initialize: function DP_initialize(aCallback)
   1.128 +  {
   1.129 +    DownloadsCommon.log("Attempting to initialize DownloadsPanel for a window.");
   1.130 +    if (this._state != this.kStateUninitialized) {
   1.131 +      DownloadsCommon.log("DownloadsPanel is already initialized.");
   1.132 +      DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay,
   1.133 +                                                 aCallback);
   1.134 +      return;
   1.135 +    }
   1.136 +    this._state = this.kStateHidden;
   1.137 +
   1.138 +    window.addEventListener("unload", this.onWindowUnload, false);
   1.139 +
   1.140 +    // Load and resume active downloads if required.  If there are downloads to
   1.141 +    // be shown in the panel, they will be loaded asynchronously.
   1.142 +    DownloadsCommon.initializeAllDataLinks();
   1.143 +
   1.144 +    // Now that data loading has eventually started, load the required XUL
   1.145 +    // elements and initialize our views.
   1.146 +    DownloadsCommon.log("Ensuring DownloadsPanel overlay loaded.");
   1.147 +    DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay,
   1.148 +                                               function DP_I_callback() {
   1.149 +      DownloadsViewController.initialize();
   1.150 +      DownloadsCommon.log("Attaching DownloadsView...");
   1.151 +      DownloadsCommon.getData(window).addView(DownloadsView);
   1.152 +      DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit)
   1.153 +                     .addView(DownloadsSummary);
   1.154 +      DownloadsCommon.log("DownloadsView attached - the panel for this window",
   1.155 +                          "should now see download items come in.");
   1.156 +      DownloadsPanel._attachEventListeners();
   1.157 +      DownloadsCommon.log("DownloadsPanel initialized.");
   1.158 +      aCallback();
   1.159 +    });
   1.160 +  },
   1.161 +
   1.162 +  /**
   1.163 +   * Closes the downloads panel and frees the internal resources related to the
   1.164 +   * downloads.  The downloads panel can be reopened later, even after this
   1.165 +   * function has been called.
   1.166 +   */
   1.167 +  terminate: function DP_terminate()
   1.168 +  {
   1.169 +    DownloadsCommon.log("Attempting to terminate DownloadsPanel for a window.");
   1.170 +    if (this._state == this.kStateUninitialized) {
   1.171 +      DownloadsCommon.log("DownloadsPanel was never initialized. Nothing to do.");
   1.172 +      return;
   1.173 +    }
   1.174 +
   1.175 +    window.removeEventListener("unload", this.onWindowUnload, false);
   1.176 +
   1.177 +    // Ensure that the panel is closed before shutting down.
   1.178 +    this.hidePanel();
   1.179 +
   1.180 +    DownloadsViewController.terminate();
   1.181 +    DownloadsCommon.getData(window).removeView(DownloadsView);
   1.182 +    DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit)
   1.183 +                   .removeView(DownloadsSummary);
   1.184 +    this._unattachEventListeners();
   1.185 +
   1.186 +    this._state = this.kStateUninitialized;
   1.187 +
   1.188 +    DownloadsSummary.active = false;
   1.189 +    DownloadsCommon.log("DownloadsPanel terminated.");
   1.190 +  },
   1.191 +
   1.192 +  //////////////////////////////////////////////////////////////////////////////
   1.193 +  //// Panel interface
   1.194 +
   1.195 +  /**
   1.196 +   * Main panel element in the browser window, or null if the panel overlay
   1.197 +   * hasn't been loaded yet.
   1.198 +   */
   1.199 +  get panel()
   1.200 +  {
   1.201 +    // If the downloads panel overlay hasn't loaded yet, just return null
   1.202 +    // without reseting this.panel.
   1.203 +    let downloadsPanel = document.getElementById("downloadsPanel");
   1.204 +    if (!downloadsPanel)
   1.205 +      return null;
   1.206 +
   1.207 +    delete this.panel;
   1.208 +    return this.panel = downloadsPanel;
   1.209 +  },
   1.210 +
   1.211 +  /**
   1.212 +   * Starts opening the downloads panel interface, anchored to the downloads
   1.213 +   * button of the browser window.  The list of downloads to display is
   1.214 +   * initialized the first time this method is called, and the panel is shown
   1.215 +   * only when data is ready.
   1.216 +   */
   1.217 +  showPanel: function DP_showPanel()
   1.218 +  {
   1.219 +    DownloadsCommon.log("Opening the downloads panel.");
   1.220 +
   1.221 +    if (this.isPanelShowing) {
   1.222 +      DownloadsCommon.log("Panel is already showing - focusing instead.");
   1.223 +      this._focusPanel();
   1.224 +      return;
   1.225 +    }
   1.226 +
   1.227 +    this.initialize(function DP_SP_callback() {
   1.228 +      // Delay displaying the panel because this function will sometimes be
   1.229 +      // called while another window is closing (like the window for selecting
   1.230 +      // whether to save or open the file), and that would cause the panel to
   1.231 +      // close immediately.
   1.232 +      setTimeout(function () DownloadsPanel._openPopupIfDataReady(), 0);
   1.233 +    }.bind(this));
   1.234 +
   1.235 +    DownloadsCommon.log("Waiting for the downloads panel to appear.");
   1.236 +    this._state = this.kStateWaitingData;
   1.237 +  },
   1.238 +
   1.239 +  /**
   1.240 +   * Hides the downloads panel, if visible, but keeps the internal state so that
   1.241 +   * the panel can be reopened quickly if required.
   1.242 +   */
   1.243 +  hidePanel: function DP_hidePanel()
   1.244 +  {
   1.245 +    DownloadsCommon.log("Closing the downloads panel.");
   1.246 +
   1.247 +    if (!this.isPanelShowing) {
   1.248 +      DownloadsCommon.log("Downloads panel is not showing - nothing to do.");
   1.249 +      return;
   1.250 +    }
   1.251 +
   1.252 +    this.panel.hidePopup();
   1.253 +
   1.254 +    // Ensure that we allow the panel to be reopened.  Note that, if the popup
   1.255 +    // was open, then the onPopupHidden event handler has already updated the
   1.256 +    // current state, otherwise we must update the state ourselves.
   1.257 +    this._state = this.kStateHidden;
   1.258 +    DownloadsCommon.log("Downloads panel is now closed.");
   1.259 +  },
   1.260 +
   1.261 +  /**
   1.262 +   * Indicates whether the panel is shown or will be shown.
   1.263 +   */
   1.264 +  get isPanelShowing()
   1.265 +  {
   1.266 +    return this._state == this.kStateWaitingData ||
   1.267 +           this._state == this.kStateWaitingAnchor ||
   1.268 +           this._state == this.kStateShown;
   1.269 +  },
   1.270 +
   1.271 +  /**
   1.272 +   * Returns whether the user has started keyboard navigation.
   1.273 +   */
   1.274 +  get keyFocusing()
   1.275 +  {
   1.276 +    return this.panel.hasAttribute("keyfocus");
   1.277 +  },
   1.278 +
   1.279 +  /**
   1.280 +   * Set to true if the user has started keyboard navigation, and we should be
   1.281 +   * showing focusrings in the panel. Also adds a mousemove event handler to
   1.282 +   * the panel which disables keyFocusing.
   1.283 +   */
   1.284 +  set keyFocusing(aValue)
   1.285 +  {
   1.286 +    if (aValue) {
   1.287 +      this.panel.setAttribute("keyfocus", "true");
   1.288 +      this.panel.addEventListener("mousemove", this);
   1.289 +    } else {
   1.290 +      this.panel.removeAttribute("keyfocus");
   1.291 +      this.panel.removeEventListener("mousemove", this);
   1.292 +    }
   1.293 +    return aValue;
   1.294 +  },
   1.295 +
   1.296 +  /**
   1.297 +   * Handles the mousemove event for the panel, which disables focusring
   1.298 +   * visualization.
   1.299 +   */
   1.300 +  handleEvent: function DP_handleEvent(aEvent)
   1.301 +  {
   1.302 +    if (aEvent.type == "mousemove") {
   1.303 +      this.keyFocusing = false;
   1.304 +    }
   1.305 +  },
   1.306 +
   1.307 +  //////////////////////////////////////////////////////////////////////////////
   1.308 +  //// Callback functions from DownloadsView
   1.309 +
   1.310 +  /**
   1.311 +   * Called after data loading finished.
   1.312 +   */
   1.313 +  onViewLoadCompleted: function DP_onViewLoadCompleted()
   1.314 +  {
   1.315 +    this._openPopupIfDataReady();
   1.316 +  },
   1.317 +
   1.318 +  //////////////////////////////////////////////////////////////////////////////
   1.319 +  //// User interface event functions
   1.320 +
   1.321 +  onWindowUnload: function DP_onWindowUnload()
   1.322 +  {
   1.323 +    // This function is registered as an event listener, we can't use "this".
   1.324 +    DownloadsPanel.terminate();
   1.325 +  },
   1.326 +
   1.327 +  onPopupShown: function DP_onPopupShown(aEvent)
   1.328 +  {
   1.329 +    // Ignore events raised by nested popups.
   1.330 +    if (aEvent.target != aEvent.currentTarget) {
   1.331 +      return;
   1.332 +    }
   1.333 +
   1.334 +    DownloadsCommon.log("Downloads panel has shown.");
   1.335 +    this._state = this.kStateShown;
   1.336 +
   1.337 +    // Since at most one popup is open at any given time, we can set globally.
   1.338 +    DownloadsCommon.getIndicatorData(window).attentionSuppressed = true;
   1.339 +
   1.340 +    // Ensure that the first item is selected when the panel is focused.
   1.341 +    if (DownloadsView.richListBox.itemCount > 0 &&
   1.342 +        DownloadsView.richListBox.selectedIndex == -1) {
   1.343 +      DownloadsView.richListBox.selectedIndex = 0;
   1.344 +    }
   1.345 +
   1.346 +    this._focusPanel();
   1.347 +  },
   1.348 +
   1.349 +  onPopupHidden: function DP_onPopupHidden(aEvent)
   1.350 +  {
   1.351 +    // Ignore events raised by nested popups.
   1.352 +    if (aEvent.target != aEvent.currentTarget) {
   1.353 +      return;
   1.354 +    }
   1.355 +
   1.356 +    DownloadsCommon.log("Downloads panel has hidden.");
   1.357 +
   1.358 +    // Removes the keyfocus attribute so that we stop handling keyboard
   1.359 +    // navigation.
   1.360 +    this.keyFocusing = false;
   1.361 +
   1.362 +    // Since at most one popup is open at any given time, we can set globally.
   1.363 +    DownloadsCommon.getIndicatorData(window).attentionSuppressed = false;
   1.364 +
   1.365 +    // Allow the anchor to be hidden.
   1.366 +    DownloadsButton.releaseAnchor();
   1.367 +
   1.368 +    // Allow the panel to be reopened.
   1.369 +    this._state = this.kStateHidden;
   1.370 +  },
   1.371 +
   1.372 +  //////////////////////////////////////////////////////////////////////////////
   1.373 +  //// Related operations
   1.374 +
   1.375 +  /**
   1.376 +   * Shows or focuses the user interface dedicated to downloads history.
   1.377 +   */
   1.378 +  showDownloadsHistory: function DP_showDownloadsHistory()
   1.379 +  {
   1.380 +    DownloadsCommon.log("Showing download history.");
   1.381 +    // Hide the panel before showing another window, otherwise focus will return
   1.382 +    // to the browser window when the panel closes automatically.
   1.383 +    this.hidePanel();
   1.384 +
   1.385 +    BrowserDownloadsUI();
   1.386 +  },
   1.387 +
   1.388 +  //////////////////////////////////////////////////////////////////////////////
   1.389 +  //// Internal functions
   1.390 +
   1.391 +  /**
   1.392 +   * Attach event listeners to a panel element. These listeners should be
   1.393 +   * removed in _unattachEventListeners. This is called automatically after the
   1.394 +   * panel has successfully loaded.
   1.395 +   */
   1.396 +  _attachEventListeners: function DP__attachEventListeners()
   1.397 +  {
   1.398 +    // Handle keydown to support accel-V.
   1.399 +    this.panel.addEventListener("keydown", this._onKeyDown.bind(this), false);
   1.400 +    // Handle keypress to be able to preventDefault() events before they reach
   1.401 +    // the richlistbox, for keyboard navigation.
   1.402 +    this.panel.addEventListener("keypress", this._onKeyPress.bind(this), false);
   1.403 +  },
   1.404 +
   1.405 +  /**
   1.406 +   * Unattach event listeners that were added in _attachEventListeners. This
   1.407 +   * is called automatically on panel termination.
   1.408 +   */
   1.409 +  _unattachEventListeners: function DP__unattachEventListeners()
   1.410 +  {
   1.411 +    this.panel.removeEventListener("keydown", this._onKeyDown.bind(this),
   1.412 +                                   false);
   1.413 +    this.panel.removeEventListener("keypress", this._onKeyPress.bind(this),
   1.414 +                                   false);
   1.415 +  },
   1.416 +
   1.417 +  _onKeyPress: function DP__onKeyPress(aEvent)
   1.418 +  {
   1.419 +    // Handle unmodified keys only.
   1.420 +    if (aEvent.altKey || aEvent.ctrlKey || aEvent.shiftKey || aEvent.metaKey) {
   1.421 +      return;
   1.422 +    }
   1.423 +
   1.424 +    let richListBox = DownloadsView.richListBox;
   1.425 +
   1.426 +    // If the user has pressed the tab, up, or down cursor key, start keyboard
   1.427 +    // navigation, thus enabling focusrings in the panel.  Keyboard navigation
   1.428 +    // is automatically disabled if the user moves the mouse on the panel, or
   1.429 +    // if the panel is closed.
   1.430 +    if ((aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_TAB ||
   1.431 +        aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP ||
   1.432 +        aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) &&
   1.433 +        !this.keyFocusing) {
   1.434 +      this.keyFocusing = true;
   1.435 +      // Ensure there's a selection, we will show the focus ring around it and
   1.436 +      // prevent the richlistbox from changing the selection.
   1.437 +      if (DownloadsView.richListBox.selectedIndex == -1)
   1.438 +        DownloadsView.richListBox.selectedIndex = 0;
   1.439 +      aEvent.preventDefault();
   1.440 +      return;
   1.441 +    }
   1.442 +
   1.443 +    if (aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) {
   1.444 +      // If the last element in the list is selected, or the footer is already
   1.445 +      // focused, focus the footer.
   1.446 +      if (richListBox.selectedItem === richListBox.lastChild ||
   1.447 +          document.activeElement.parentNode.id === "downloadsFooter") {
   1.448 +        DownloadsFooter.focus();
   1.449 +        aEvent.preventDefault();
   1.450 +        return;
   1.451 +      }
   1.452 +    }
   1.453 +
   1.454 +    // Pass keypress events to the richlistbox view when it's focused.
   1.455 +    if (document.activeElement === richListBox) {
   1.456 +      DownloadsView.onDownloadKeyPress(aEvent);
   1.457 +    }
   1.458 +  },
   1.459 +
   1.460 +  /**
   1.461 +   * Keydown listener that listens for the keys to start key focusing, as well
   1.462 +   * as the the accel-V "paste" event, which initiates a file download if the
   1.463 +   * pasted item can be resolved to a URI.
   1.464 +   */
   1.465 +  _onKeyDown: function DP__onKeyDown(aEvent)
   1.466 +  {
   1.467 +    // If the footer is focused and the downloads list has at least 1 element
   1.468 +    // in it, focus the last element in the list when going up.
   1.469 +    if (aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP &&
   1.470 +        document.activeElement.parentNode.id === "downloadsFooter" &&
   1.471 +        DownloadsView.richListBox.firstChild) {
   1.472 +      DownloadsView.richListBox.focus();
   1.473 +      DownloadsView.richListBox.selectedItem = DownloadsView.richListBox.lastChild;
   1.474 +      aEvent.preventDefault();
   1.475 +      return;
   1.476 +    }
   1.477 +
   1.478 +    let pasting = aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_V &&
   1.479 +#ifdef XP_MACOSX
   1.480 +                  aEvent.metaKey;
   1.481 +#else
   1.482 +                  aEvent.ctrlKey;
   1.483 +#endif
   1.484 +
   1.485 +    if (!pasting) {
   1.486 +      return;
   1.487 +    }
   1.488 +
   1.489 +    DownloadsCommon.log("Received a paste event.");
   1.490 +
   1.491 +    let trans = Cc["@mozilla.org/widget/transferable;1"]
   1.492 +                  .createInstance(Ci.nsITransferable);
   1.493 +    trans.init(null);
   1.494 +    let flavors = ["text/x-moz-url", "text/unicode"];
   1.495 +    flavors.forEach(trans.addDataFlavor);
   1.496 +    Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard);
   1.497 +    // Getting the data or creating the nsIURI might fail
   1.498 +    try {
   1.499 +      let data = {};
   1.500 +      trans.getAnyTransferData({}, data, {});
   1.501 +      let [url, name] = data.value
   1.502 +                            .QueryInterface(Ci.nsISupportsString)
   1.503 +                            .data
   1.504 +                            .split("\n");
   1.505 +      if (!url) {
   1.506 +        return;
   1.507 +      }
   1.508 +
   1.509 +      let uri = NetUtil.newURI(url);
   1.510 +      DownloadsCommon.log("Pasted URL seems valid. Starting download.");
   1.511 +      DownloadURL(uri.spec, name, document);
   1.512 +    } catch (ex) {}
   1.513 +  },
   1.514 +
   1.515 +  /**
   1.516 +   * Move focus to the main element in the downloads panel, unless another
   1.517 +   * element in the panel is already focused.
   1.518 +   */
   1.519 +  _focusPanel: function DP_focusPanel()
   1.520 +  {
   1.521 +    // We may be invoked while the panel is still waiting to be shown.
   1.522 +    if (this._state != this.kStateShown) {
   1.523 +      return;
   1.524 +    }
   1.525 +
   1.526 +    let element = document.commandDispatcher.focusedElement;
   1.527 +    while (element && element != this.panel) {
   1.528 +      element = element.parentNode;
   1.529 +    }
   1.530 +    if (!element) {
   1.531 +      if (DownloadsView.richListBox.itemCount > 0) {
   1.532 +        DownloadsView.richListBox.focus();
   1.533 +      } else {
   1.534 +        DownloadsFooter.focus();
   1.535 +      }
   1.536 +    }
   1.537 +  },
   1.538 +
   1.539 +  /**
   1.540 +   * Opens the downloads panel when data is ready to be displayed.
   1.541 +   */
   1.542 +  _openPopupIfDataReady: function DP_openPopupIfDataReady()
   1.543 +  {
   1.544 +    // We don't want to open the popup if we already displayed it, or if we are
   1.545 +    // still loading data.
   1.546 +    if (this._state != this.kStateWaitingData || DownloadsView.loading) {
   1.547 +      return;
   1.548 +    }
   1.549 +
   1.550 +    this._state = this.kStateWaitingAnchor;
   1.551 +
   1.552 +    // Ensure the anchor is visible.  If that is not possible, show the panel
   1.553 +    // anchored to the top area of the window, near the default anchor position.
   1.554 +    DownloadsButton.getAnchor(function DP_OPIDR_callback(aAnchor) {
   1.555 +      // If somehow we've switched states already (by getting a panel hiding
   1.556 +      // event before an overlay is loaded, for example), bail out.
   1.557 +      if (this._state != this.kStateWaitingAnchor)
   1.558 +        return;
   1.559 +
   1.560 +      // At this point, if the window is minimized, opening the panel could fail
   1.561 +      // without any notification, and there would be no way to either open or
   1.562 +      // close the panel anymore.  To prevent this, check if the window is
   1.563 +      // minimized and in that case force the panel to the closed state.
   1.564 +      if (window.windowState == Ci.nsIDOMChromeWindow.STATE_MINIMIZED) {
   1.565 +        DownloadsButton.releaseAnchor();
   1.566 +        this._state = this.kStateHidden;
   1.567 +        return;
   1.568 +      }
   1.569 +
   1.570 +      // When the panel is opened, we check if the target files of visible items
   1.571 +      // still exist, and update the allowed items interactions accordingly.  We
   1.572 +      // do these checks on a background thread, and don't prevent the panel to
   1.573 +      // be displayed while these checks are being performed.
   1.574 +      for each (let viewItem in DownloadsView._viewItems) {
   1.575 +        viewItem.verifyTargetExists();
   1.576 +      }
   1.577 +
   1.578 +      if (aAnchor) {
   1.579 +        DownloadsCommon.log("Opening downloads panel popup.");
   1.580 +        this.panel.openPopup(aAnchor, "bottomcenter topright", 0, 0, false,
   1.581 +                             null);
   1.582 +      } else {
   1.583 +        DownloadsCommon.error("We can't find the anchor! Failure case - opening",
   1.584 +                              "downloads panel on TabsToolbar. We should never",
   1.585 +                              "get here!");
   1.586 +        Components.utils.reportError(
   1.587 +          "Downloads button cannot be found");
   1.588 +      }
   1.589 +    }.bind(this));
   1.590 +  }
   1.591 +};
   1.592 +
   1.593 +////////////////////////////////////////////////////////////////////////////////
   1.594 +//// DownloadsOverlayLoader
   1.595 +
   1.596 +/**
   1.597 + * Allows loading the downloads panel and the status indicator interfaces on
   1.598 + * demand, to improve startup performance.
   1.599 + */
   1.600 +const DownloadsOverlayLoader = {
   1.601 +  /**
   1.602 +   * We cannot load two overlays at the same time, thus we use a queue of
   1.603 +   * pending load requests.
   1.604 +   */
   1.605 +  _loadRequests: [],
   1.606 +
   1.607 +  /**
   1.608 +   * True while we are waiting for an overlay to be loaded.
   1.609 +   */
   1.610 +  _overlayLoading: false,
   1.611 +
   1.612 +  /**
   1.613 +   * This object has a key for each overlay URI that is already loaded.
   1.614 +   */
   1.615 +  _loadedOverlays: {},
   1.616 +
   1.617 +  /**
   1.618 +   * Loads the specified overlay and invokes the given callback when finished.
   1.619 +   *
   1.620 +   * @param aOverlay
   1.621 +   *        String containing the URI of the overlay to load in the current
   1.622 +   *        window.  If this overlay has already been loaded using this
   1.623 +   *        function, then the overlay is not loaded again.
   1.624 +   * @param aCallback
   1.625 +   *        Invoked when loading is completed.  If the overlay is already
   1.626 +   *        loaded, the function is called immediately.
   1.627 +   */
   1.628 +  ensureOverlayLoaded: function DOL_ensureOverlayLoaded(aOverlay, aCallback)
   1.629 +  {
   1.630 +    // The overlay is already loaded, invoke the callback immediately.
   1.631 +    if (aOverlay in this._loadedOverlays) {
   1.632 +      aCallback();
   1.633 +      return;
   1.634 +    }
   1.635 +
   1.636 +    // The callback will be invoked when loading is finished.
   1.637 +    this._loadRequests.push({ overlay: aOverlay, callback: aCallback });
   1.638 +    if (this._overlayLoading) {
   1.639 +      return;
   1.640 +    }
   1.641 +
   1.642 +    function DOL_EOL_loadCallback() {
   1.643 +      this._overlayLoading = false;
   1.644 +      this._loadedOverlays[aOverlay] = true;
   1.645 +
   1.646 +      this.processPendingRequests();
   1.647 +    }
   1.648 +
   1.649 +    this._overlayLoading = true;
   1.650 +    DownloadsCommon.log("Loading overlay ", aOverlay);
   1.651 +    document.loadOverlay(aOverlay, DOL_EOL_loadCallback.bind(this));
   1.652 +  },
   1.653 +
   1.654 +  /**
   1.655 +   * Re-processes all the currently pending requests, invoking the callbacks
   1.656 +   * and/or loading more overlays as needed.  In most cases, there will be a
   1.657 +   * single request for one overlay, that will be processed immediately.
   1.658 +   */
   1.659 +  processPendingRequests: function DOL_processPendingRequests()
   1.660 +  {
   1.661 +    // Re-process all the currently pending requests, yet allow more requests
   1.662 +    // to be appended at the end of the array if we're not ready for them.
   1.663 +    let currentLength = this._loadRequests.length;
   1.664 +    for (let i = 0; i < currentLength; i++) {
   1.665 +      let request = this._loadRequests.shift();
   1.666 +
   1.667 +      // We must call ensureOverlayLoaded again for each request, to check if
   1.668 +      // the associated callback can be invoked now, or if we must still wait
   1.669 +      // for the associated overlay to load.
   1.670 +      this.ensureOverlayLoaded(request.overlay, request.callback);
   1.671 +    }
   1.672 +  }
   1.673 +};
   1.674 +
   1.675 +////////////////////////////////////////////////////////////////////////////////
   1.676 +//// DownloadsView
   1.677 +
   1.678 +/**
   1.679 + * Builds and updates the downloads list widget, responding to changes in the
   1.680 + * download state and real-time data.  In addition, handles part of the user
   1.681 + * interaction events raised by the downloads list widget.
   1.682 + */
   1.683 +const DownloadsView = {
   1.684 +  //////////////////////////////////////////////////////////////////////////////
   1.685 +  //// Functions handling download items in the list
   1.686 +
   1.687 +  /**
   1.688 +   * Maximum number of items shown by the list at any given time.
   1.689 +   */
   1.690 +  kItemCountLimit: 3,
   1.691 +
   1.692 +  /**
   1.693 +   * Indicates whether we are still loading downloads data asynchronously.
   1.694 +   */
   1.695 +  loading: false,
   1.696 +
   1.697 +  /**
   1.698 +   * Ordered array of all DownloadsDataItem objects.  We need to keep this array
   1.699 +   * because only a limited number of items are shown at once, and if an item
   1.700 +   * that is currently visible is removed from the list, we might need to take
   1.701 +   * another item from the array and make it appear at the bottom.
   1.702 +   */
   1.703 +  _dataItems: [],
   1.704 +
   1.705 +  /**
   1.706 +   * Object containing the available DownloadsViewItem objects, indexed by their
   1.707 +   * numeric download identifier.  There is a limited number of view items in
   1.708 +   * the panel at any given time.
   1.709 +   */
   1.710 +  _viewItems: {},
   1.711 +
   1.712 +  /**
   1.713 +   * Called when the number of items in the list changes.
   1.714 +   */
   1.715 +  _itemCountChanged: function DV_itemCountChanged()
   1.716 +  {
   1.717 +    DownloadsCommon.log("The downloads item count has changed - we are tracking",
   1.718 +                        this._dataItems.length, "downloads in total.");
   1.719 +    let count = this._dataItems.length;
   1.720 +    let hiddenCount = count - this.kItemCountLimit;
   1.721 +
   1.722 +    if (count > 0) {
   1.723 +      DownloadsCommon.log("Setting the panel's hasdownloads attribute to true.");
   1.724 +      DownloadsPanel.panel.setAttribute("hasdownloads", "true");
   1.725 +    } else {
   1.726 +      DownloadsCommon.log("Removing the panel's hasdownloads attribute.");
   1.727 +      DownloadsPanel.panel.removeAttribute("hasdownloads");
   1.728 +    }
   1.729 +
   1.730 +    // If we've got some hidden downloads, we should activate the
   1.731 +    // DownloadsSummary. The DownloadsSummary will determine whether or not
   1.732 +    // it's appropriate to actually display the summary.
   1.733 +    DownloadsSummary.active = hiddenCount > 0;
   1.734 +  },
   1.735 +
   1.736 +  /**
   1.737 +   * Element corresponding to the list of downloads.
   1.738 +   */
   1.739 +  get richListBox()
   1.740 +  {
   1.741 +    delete this.richListBox;
   1.742 +    return this.richListBox = document.getElementById("downloadsListBox");
   1.743 +  },
   1.744 +
   1.745 +  /**
   1.746 +   * Element corresponding to the button for showing more downloads.
   1.747 +   */
   1.748 +  get downloadsHistory()
   1.749 +  {
   1.750 +    delete this.downloadsHistory;
   1.751 +    return this.downloadsHistory = document.getElementById("downloadsHistory");
   1.752 +  },
   1.753 +
   1.754 +  //////////////////////////////////////////////////////////////////////////////
   1.755 +  //// Callback functions from DownloadsData
   1.756 +
   1.757 +  /**
   1.758 +   * Called before multiple downloads are about to be loaded.
   1.759 +   */
   1.760 +  onDataLoadStarting: function DV_onDataLoadStarting()
   1.761 +  {
   1.762 +    DownloadsCommon.log("onDataLoadStarting called for DownloadsView.");
   1.763 +    this.loading = true;
   1.764 +  },
   1.765 +
   1.766 +  /**
   1.767 +   * Called after data loading finished.
   1.768 +   */
   1.769 +  onDataLoadCompleted: function DV_onDataLoadCompleted()
   1.770 +  {
   1.771 +    DownloadsCommon.log("onDataLoadCompleted called for DownloadsView.");
   1.772 +
   1.773 +    this.loading = false;
   1.774 +
   1.775 +    // We suppressed item count change notifications during the batch load, at
   1.776 +    // this point we should just call the function once.
   1.777 +    this._itemCountChanged();
   1.778 +
   1.779 +    // Notify the panel that all the initially available downloads have been
   1.780 +    // loaded.  This ensures that the interface is visible, if still required.
   1.781 +    DownloadsPanel.onViewLoadCompleted();
   1.782 +  },
   1.783 +
   1.784 +  /**
   1.785 +   * Called when a new download data item is available, either during the
   1.786 +   * asynchronous data load or when a new download is started.
   1.787 +   *
   1.788 +   * @param aDataItem
   1.789 +   *        DownloadsDataItem object that was just added.
   1.790 +   * @param aNewest
   1.791 +   *        When true, indicates that this item is the most recent and should be
   1.792 +   *        added in the topmost position.  This happens when a new download is
   1.793 +   *        started.  When false, indicates that the item is the least recent
   1.794 +   *        and should be appended.  The latter generally happens during the
   1.795 +   *        asynchronous data load.
   1.796 +   */
   1.797 +  onDataItemAdded: function DV_onDataItemAdded(aDataItem, aNewest)
   1.798 +  {
   1.799 +    DownloadsCommon.log("A new download data item was added - aNewest =",
   1.800 +                        aNewest);
   1.801 +
   1.802 +    if (aNewest) {
   1.803 +      this._dataItems.unshift(aDataItem);
   1.804 +    } else {
   1.805 +      this._dataItems.push(aDataItem);
   1.806 +    }
   1.807 +
   1.808 +    let itemsNowOverflow = this._dataItems.length > this.kItemCountLimit;
   1.809 +    if (aNewest || !itemsNowOverflow) {
   1.810 +      // The newly added item is visible in the panel and we must add the
   1.811 +      // corresponding element.  This is either because it is the first item, or
   1.812 +      // because it was added at the bottom but the list still doesn't overflow.
   1.813 +      this._addViewItem(aDataItem, aNewest);
   1.814 +    }
   1.815 +    if (aNewest && itemsNowOverflow) {
   1.816 +      // If the list overflows, remove the last item from the panel to make room
   1.817 +      // for the new one that we just added at the top.
   1.818 +      this._removeViewItem(this._dataItems[this.kItemCountLimit]);
   1.819 +    }
   1.820 +
   1.821 +    // For better performance during batch loads, don't update the count for
   1.822 +    // every item, because the interface won't be visible until load finishes.
   1.823 +    if (!this.loading) {
   1.824 +      this._itemCountChanged();
   1.825 +    }
   1.826 +  },
   1.827 +
   1.828 +  /**
   1.829 +   * Called when a data item is removed.  Ensures that the widget associated
   1.830 +   * with the view item is removed from the user interface.
   1.831 +   *
   1.832 +   * @param aDataItem
   1.833 +   *        DownloadsDataItem object that is being removed.
   1.834 +   */
   1.835 +  onDataItemRemoved: function DV_onDataItemRemoved(aDataItem)
   1.836 +  {
   1.837 +    DownloadsCommon.log("A download data item was removed.");
   1.838 +
   1.839 +    let itemIndex = this._dataItems.indexOf(aDataItem);
   1.840 +    this._dataItems.splice(itemIndex, 1);
   1.841 +
   1.842 +    if (itemIndex < this.kItemCountLimit) {
   1.843 +      // The item to remove is visible in the panel.
   1.844 +      this._removeViewItem(aDataItem);
   1.845 +      if (this._dataItems.length >= this.kItemCountLimit) {
   1.846 +        // Reinsert the next item into the panel.
   1.847 +        this._addViewItem(this._dataItems[this.kItemCountLimit - 1], false);
   1.848 +      }
   1.849 +    }
   1.850 +
   1.851 +    this._itemCountChanged();
   1.852 +  },
   1.853 +
   1.854 +  /**
   1.855 +   * Returns the view item associated with the provided data item for this view.
   1.856 +   *
   1.857 +   * @param aDataItem
   1.858 +   *        DownloadsDataItem object for which the view item is requested.
   1.859 +   *
   1.860 +   * @return Object that can be used to notify item status events.
   1.861 +   */
   1.862 +  getViewItem: function DV_getViewItem(aDataItem)
   1.863 +  {
   1.864 +    // If the item is visible, just return it, otherwise return a mock object
   1.865 +    // that doesn't react to notifications.
   1.866 +    if (aDataItem.downloadGuid in this._viewItems) {
   1.867 +      return this._viewItems[aDataItem.downloadGuid];
   1.868 +    }
   1.869 +    return this._invisibleViewItem;
   1.870 +  },
   1.871 +
   1.872 +  /**
   1.873 +   * Mock DownloadsDataItem object that doesn't react to notifications.
   1.874 +   */
   1.875 +  _invisibleViewItem: Object.freeze({
   1.876 +    onStateChange: function () { },
   1.877 +    onProgressChange: function () { }
   1.878 +  }),
   1.879 +
   1.880 +  /**
   1.881 +   * Creates a new view item associated with the specified data item, and adds
   1.882 +   * it to the top or the bottom of the list.
   1.883 +   */
   1.884 +  _addViewItem: function DV_addViewItem(aDataItem, aNewest)
   1.885 +  {
   1.886 +    DownloadsCommon.log("Adding a new DownloadsViewItem to the downloads list.",
   1.887 +                        "aNewest =", aNewest);
   1.888 +
   1.889 +    let element = document.createElement("richlistitem");
   1.890 +    let viewItem = new DownloadsViewItem(aDataItem, element);
   1.891 +    this._viewItems[aDataItem.downloadGuid] = viewItem;
   1.892 +    if (aNewest) {
   1.893 +      this.richListBox.insertBefore(element, this.richListBox.firstChild);
   1.894 +    } else {
   1.895 +      this.richListBox.appendChild(element);
   1.896 +    }
   1.897 +  },
   1.898 +
   1.899 +  /**
   1.900 +   * Removes the view item associated with the specified data item.
   1.901 +   */
   1.902 +  _removeViewItem: function DV_removeViewItem(aDataItem)
   1.903 +  {
   1.904 +    DownloadsCommon.log("Removing a DownloadsViewItem from the downloads list.");
   1.905 +    let element = this.getViewItem(aDataItem)._element;
   1.906 +    let previousSelectedIndex = this.richListBox.selectedIndex;
   1.907 +    this.richListBox.removeChild(element);
   1.908 +    if (previousSelectedIndex != -1) {
   1.909 +      this.richListBox.selectedIndex = Math.min(previousSelectedIndex,
   1.910 +                                                this.richListBox.itemCount - 1);
   1.911 +    }
   1.912 +    delete this._viewItems[aDataItem.downloadGuid];
   1.913 +  },
   1.914 +
   1.915 +  //////////////////////////////////////////////////////////////////////////////
   1.916 +  //// User interface event functions
   1.917 +
   1.918 +  /**
   1.919 +   * Helper function to do commands on a specific download item.
   1.920 +   *
   1.921 +   * @param aEvent
   1.922 +   *        Event object for the event being handled.  If the event target is
   1.923 +   *        not a richlistitem that represents a download, this function will
   1.924 +   *        walk up the parent nodes until it finds a DOM node that is.
   1.925 +   * @param aCommand
   1.926 +   *        The command to be performed.
   1.927 +   */
   1.928 +  onDownloadCommand: function DV_onDownloadCommand(aEvent, aCommand)
   1.929 +  {
   1.930 +    let target = aEvent.target;
   1.931 +    while (target.nodeName != "richlistitem") {
   1.932 +      target = target.parentNode;
   1.933 +    }
   1.934 +    new DownloadsViewItemController(target).doCommand(aCommand);
   1.935 +  },
   1.936 +
   1.937 +  onDownloadClick: function DV_onDownloadClick(aEvent)
   1.938 +  {
   1.939 +    // Handle primary clicks only, and exclude the action button.
   1.940 +    if (aEvent.button == 0 &&
   1.941 +        !aEvent.originalTarget.hasAttribute("oncommand")) {
   1.942 +      goDoCommand("downloadsCmd_open");
   1.943 +    }
   1.944 +  },
   1.945 +
   1.946 +  /**
   1.947 +   * Handles keypress events on a download item.
   1.948 +   */
   1.949 +  onDownloadKeyPress: function DV_onDownloadKeyPress(aEvent)
   1.950 +  {
   1.951 +    // Pressing the key on buttons should not invoke the action because the
   1.952 +    // event has already been handled by the button itself.
   1.953 +    if (aEvent.originalTarget.hasAttribute("command") ||
   1.954 +        aEvent.originalTarget.hasAttribute("oncommand")) {
   1.955 +      return;
   1.956 +    }
   1.957 +
   1.958 +    if (aEvent.charCode == " ".charCodeAt(0)) {
   1.959 +      goDoCommand("downloadsCmd_pauseResume");
   1.960 +      return;
   1.961 +    }
   1.962 +
   1.963 +    if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
   1.964 +      goDoCommand("downloadsCmd_doDefault");
   1.965 +    }
   1.966 +  },
   1.967 +
   1.968 +
   1.969 +  /**
   1.970 +   * Mouse listeners to handle selection on hover.
   1.971 +   */
   1.972 +  onDownloadMouseOver: function DV_onDownloadMouseOver(aEvent)
   1.973 +  {
   1.974 +    if (aEvent.originalTarget.parentNode == this.richListBox)
   1.975 +      this.richListBox.selectedItem = aEvent.originalTarget;
   1.976 +  },
   1.977 +  onDownloadMouseOut: function DV_onDownloadMouseOut(aEvent)
   1.978 +  {
   1.979 +    if (aEvent.originalTarget.parentNode == this.richListBox) {
   1.980 +      // If the destination element is outside of the richlistitem, clear the
   1.981 +      // selection.
   1.982 +      let element = aEvent.relatedTarget;
   1.983 +      while (element && element != aEvent.originalTarget) {
   1.984 +        element = element.parentNode;
   1.985 +      }
   1.986 +      if (!element)
   1.987 +        this.richListBox.selectedIndex = -1;
   1.988 +    }
   1.989 +  },
   1.990 +
   1.991 +  onDownloadContextMenu: function DV_onDownloadContextMenu(aEvent)
   1.992 +  {
   1.993 +    let element = this.richListBox.selectedItem;
   1.994 +    if (!element) {
   1.995 +      return;
   1.996 +    }
   1.997 +
   1.998 +    DownloadsViewController.updateCommands();
   1.999 +
  1.1000 +    // Set the state attribute so that only the appropriate items are displayed.
  1.1001 +    let contextMenu = document.getElementById("downloadsContextMenu");
  1.1002 +    contextMenu.setAttribute("state", element.getAttribute("state"));
  1.1003 +  },
  1.1004 +
  1.1005 +  onDownloadDragStart: function DV_onDownloadDragStart(aEvent)
  1.1006 +  {
  1.1007 +    let element = this.richListBox.selectedItem;
  1.1008 +    if (!element) {
  1.1009 +      return;
  1.1010 +    }
  1.1011 +
  1.1012 +    let controller = new DownloadsViewItemController(element);
  1.1013 +    let localFile = controller.dataItem.localFile;
  1.1014 +    if (!localFile.exists()) {
  1.1015 +      return;
  1.1016 +    }
  1.1017 +
  1.1018 +    let dataTransfer = aEvent.dataTransfer;
  1.1019 +    dataTransfer.mozSetDataAt("application/x-moz-file", localFile, 0);
  1.1020 +    dataTransfer.effectAllowed = "copyMove";
  1.1021 +    var url = Services.io.newFileURI(localFile).spec;
  1.1022 +    dataTransfer.setData("text/uri-list", url);
  1.1023 +    dataTransfer.setData("text/plain", url);
  1.1024 +    dataTransfer.addElement(element);
  1.1025 +
  1.1026 +    aEvent.stopPropagation();
  1.1027 +  }
  1.1028 +}
  1.1029 +
  1.1030 +////////////////////////////////////////////////////////////////////////////////
  1.1031 +//// DownloadsViewItem
  1.1032 +
  1.1033 +/**
  1.1034 + * Builds and updates a single item in the downloads list widget, responding to
  1.1035 + * changes in the download state and real-time data.
  1.1036 + *
  1.1037 + * @param aDataItem
  1.1038 + *        DownloadsDataItem to be associated with the view item.
  1.1039 + * @param aElement
  1.1040 + *        XUL element corresponding to the single download item in the view.
  1.1041 + */
  1.1042 +function DownloadsViewItem(aDataItem, aElement)
  1.1043 +{
  1.1044 +  this._element = aElement;
  1.1045 +  this.dataItem = aDataItem;
  1.1046 +
  1.1047 +  this.lastEstimatedSecondsLeft = Infinity;
  1.1048 +
  1.1049 +  // Set the URI that represents the correct icon for the target file.  As soon
  1.1050 +  // as bug 239948 comment 12 is handled, the "file" property will be always a
  1.1051 +  // file URL rather than a file name.  At that point we should remove the "//"
  1.1052 +  // (double slash) from the icon URI specification (see test_moz_icon_uri.js).
  1.1053 +  this.image = "moz-icon://" + this.dataItem.file + "?size=32";
  1.1054 +
  1.1055 +  let attributes = {
  1.1056 +    "type": "download",
  1.1057 +    "class": "download-state",
  1.1058 +    "id": "downloadsItem_" + this.dataItem.downloadGuid,
  1.1059 +    "downloadGuid": this.dataItem.downloadGuid,
  1.1060 +    "state": this.dataItem.state,
  1.1061 +    "progress": this.dataItem.inProgress ? this.dataItem.percentComplete : 100,
  1.1062 +    "target": this.dataItem.target,
  1.1063 +    "image": this.image
  1.1064 +  };
  1.1065 +
  1.1066 +  for (let attributeName in attributes) {
  1.1067 +    this._element.setAttribute(attributeName, attributes[attributeName]);
  1.1068 +  }
  1.1069 +
  1.1070 +  // Initialize more complex attributes.
  1.1071 +  this._updateProgress();
  1.1072 +  this._updateStatusLine();
  1.1073 +  this.verifyTargetExists();
  1.1074 +}
  1.1075 +
  1.1076 +DownloadsViewItem.prototype = {
  1.1077 +  /**
  1.1078 +   * The DownloadDataItem associated with this view item.
  1.1079 +   */
  1.1080 +  dataItem: null,
  1.1081 +
  1.1082 +  /**
  1.1083 +   * The XUL element corresponding to the associated richlistbox item.
  1.1084 +   */
  1.1085 +  _element: null,
  1.1086 +
  1.1087 +  /**
  1.1088 +   * The inner XUL element for the progress bar, or null if not available.
  1.1089 +   */
  1.1090 +  _progressElement: null,
  1.1091 +
  1.1092 +  //////////////////////////////////////////////////////////////////////////////
  1.1093 +  //// Callback functions from DownloadsData
  1.1094 +
  1.1095 +  /**
  1.1096 +   * Called when the download state might have changed.  Sometimes the state of
  1.1097 +   * the download might be the same as before, if the data layer received
  1.1098 +   * multiple events for the same download.
  1.1099 +   */
  1.1100 +  onStateChange: function DVI_onStateChange(aOldState)
  1.1101 +  {
  1.1102 +    // If a download just finished successfully, it means that the target file
  1.1103 +    // now exists and we can extract its specific icon.  To ensure that the icon
  1.1104 +    // is reloaded, we must change the URI used by the XUL image element, for
  1.1105 +    // example by adding a query parameter.  Since this URI has a "moz-icon"
  1.1106 +    // scheme, this only works if we add one of the parameters explicitly
  1.1107 +    // supported by the nsIMozIconURI interface.
  1.1108 +    if (aOldState != Ci.nsIDownloadManager.DOWNLOAD_FINISHED &&
  1.1109 +        aOldState != this.dataItem.state) {
  1.1110 +      this._element.setAttribute("image", this.image + "&state=normal");
  1.1111 +
  1.1112 +      // We assume the existence of the target of a download that just completed
  1.1113 +      // successfully, without checking the condition in the background.  If the
  1.1114 +      // panel is already open, this will take effect immediately.  If the panel
  1.1115 +      // is opened later, a new background existence check will be performed.
  1.1116 +      this._element.setAttribute("exists", "true");
  1.1117 +    }
  1.1118 +
  1.1119 +    // Update the user interface after switching states.
  1.1120 +    this._element.setAttribute("state", this.dataItem.state);
  1.1121 +    this._updateProgress();
  1.1122 +    this._updateStatusLine();
  1.1123 +  },
  1.1124 +
  1.1125 +  /**
  1.1126 +   * Called when the download progress has changed.
  1.1127 +   */
  1.1128 +  onProgressChange: function DVI_onProgressChange() {
  1.1129 +    this._updateProgress();
  1.1130 +    this._updateStatusLine();
  1.1131 +  },
  1.1132 +
  1.1133 +  //////////////////////////////////////////////////////////////////////////////
  1.1134 +  //// Functions for updating the user interface
  1.1135 +
  1.1136 +  /**
  1.1137 +   * Updates the progress bar.
  1.1138 +   */
  1.1139 +  _updateProgress: function DVI_updateProgress() {
  1.1140 +    if (this.dataItem.starting) {
  1.1141 +      // Before the download starts, the progress meter has its initial value.
  1.1142 +      this._element.setAttribute("progressmode", "normal");
  1.1143 +      this._element.setAttribute("progress", "0");
  1.1144 +    } else if (this.dataItem.state == Ci.nsIDownloadManager.DOWNLOAD_SCANNING ||
  1.1145 +               this.dataItem.percentComplete == -1) {
  1.1146 +      // We might not know the progress of a running download, and we don't know
  1.1147 +      // the remaining time during the malware scanning phase.
  1.1148 +      this._element.setAttribute("progressmode", "undetermined");
  1.1149 +    } else {
  1.1150 +      // This is a running download of which we know the progress.
  1.1151 +      this._element.setAttribute("progressmode", "normal");
  1.1152 +      this._element.setAttribute("progress", this.dataItem.percentComplete);
  1.1153 +    }
  1.1154 +
  1.1155 +    // Find the progress element as soon as the download binding is accessible.
  1.1156 +    if (!this._progressElement) {
  1.1157 +      this._progressElement =
  1.1158 +           document.getAnonymousElementByAttribute(this._element, "anonid",
  1.1159 +                                                   "progressmeter");
  1.1160 +    }
  1.1161 +
  1.1162 +    // Dispatch the ValueChange event for accessibility, if possible.
  1.1163 +    if (this._progressElement) {
  1.1164 +      let event = document.createEvent("Events");
  1.1165 +      event.initEvent("ValueChange", true, true);
  1.1166 +      this._progressElement.dispatchEvent(event);
  1.1167 +    }
  1.1168 +  },
  1.1169 +
  1.1170 +  /**
  1.1171 +   * Updates the main status line, including bytes transferred, bytes total,
  1.1172 +   * download rate, and time remaining.
  1.1173 +   */
  1.1174 +  _updateStatusLine: function DVI_updateStatusLine() {
  1.1175 +    const nsIDM = Ci.nsIDownloadManager;
  1.1176 +
  1.1177 +    let status = "";
  1.1178 +    let statusTip = "";
  1.1179 +
  1.1180 +    if (this.dataItem.paused) {
  1.1181 +      let transfer = DownloadUtils.getTransferTotal(this.dataItem.currBytes,
  1.1182 +                                                    this.dataItem.maxBytes);
  1.1183 +
  1.1184 +      // We use the same XUL label to display both the state and the amount
  1.1185 +      // transferred, for example "Paused -  1.1 MB".
  1.1186 +      status = DownloadsCommon.strings.statusSeparatorBeforeNumber(
  1.1187 +                                            DownloadsCommon.strings.statePaused,
  1.1188 +                                            transfer);
  1.1189 +    } else if (this.dataItem.state == nsIDM.DOWNLOAD_DOWNLOADING) {
  1.1190 +      // We don't show the rate for each download in order to reduce clutter.
  1.1191 +      // The remaining time per download is likely enough information for the
  1.1192 +      // panel.
  1.1193 +      [status] =
  1.1194 +        DownloadUtils.getDownloadStatusNoRate(this.dataItem.currBytes,
  1.1195 +                                              this.dataItem.maxBytes,
  1.1196 +                                              this.dataItem.speed,
  1.1197 +                                              this.lastEstimatedSecondsLeft);
  1.1198 +
  1.1199 +      // We are, however, OK with displaying the rate in the tooltip.
  1.1200 +      let newEstimatedSecondsLeft;
  1.1201 +      [statusTip, newEstimatedSecondsLeft] =
  1.1202 +        DownloadUtils.getDownloadStatus(this.dataItem.currBytes,
  1.1203 +                                        this.dataItem.maxBytes,
  1.1204 +                                        this.dataItem.speed,
  1.1205 +                                        this.lastEstimatedSecondsLeft);
  1.1206 +      this.lastEstimatedSecondsLeft = newEstimatedSecondsLeft;
  1.1207 +    } else if (this.dataItem.starting) {
  1.1208 +      status = DownloadsCommon.strings.stateStarting;
  1.1209 +    } else if (this.dataItem.state == nsIDM.DOWNLOAD_SCANNING) {
  1.1210 +      status = DownloadsCommon.strings.stateScanning;
  1.1211 +    } else if (!this.dataItem.inProgress) {
  1.1212 +      let stateLabel = function () {
  1.1213 +        let s = DownloadsCommon.strings;
  1.1214 +        switch (this.dataItem.state) {
  1.1215 +          case nsIDM.DOWNLOAD_FAILED:           return s.stateFailed;
  1.1216 +          case nsIDM.DOWNLOAD_CANCELED:         return s.stateCanceled;
  1.1217 +          case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: return s.stateBlockedParentalControls;
  1.1218 +          case nsIDM.DOWNLOAD_BLOCKED_POLICY:   return s.stateBlockedPolicy;
  1.1219 +          case nsIDM.DOWNLOAD_DIRTY:            return s.stateDirty;
  1.1220 +          case nsIDM.DOWNLOAD_FINISHED:         return this._fileSizeText;
  1.1221 +        }
  1.1222 +        return null;
  1.1223 +      }.apply(this);
  1.1224 +
  1.1225 +      let [displayHost, fullHost] =
  1.1226 +        DownloadUtils.getURIHost(this.dataItem.referrer || this.dataItem.uri);
  1.1227 +
  1.1228 +      let end = new Date(this.dataItem.endTime);
  1.1229 +      let [displayDate, fullDate] = DownloadUtils.getReadableDates(end);
  1.1230 +
  1.1231 +      // We use the same XUL label to display the state, the host name, and the
  1.1232 +      // end time, for example "Canceled - 222.net - 11:15" or "1.1 MB -
  1.1233 +      // website2.com - Yesterday".  We show the full host and the complete date
  1.1234 +      // in the tooltip.
  1.1235 +      let firstPart = DownloadsCommon.strings.statusSeparator(stateLabel,
  1.1236 +                                                              displayHost);
  1.1237 +      status = DownloadsCommon.strings.statusSeparator(firstPart, displayDate);
  1.1238 +      statusTip = DownloadsCommon.strings.statusSeparator(fullHost, fullDate);
  1.1239 +    }
  1.1240 +
  1.1241 +    this._element.setAttribute("status", status);
  1.1242 +    this._element.setAttribute("statusTip", statusTip || status);
  1.1243 +  },
  1.1244 +
  1.1245 +  /**
  1.1246 +   * Localized string representing the total size of completed downloads, for
  1.1247 +   * example "1.5 MB" or "Unknown size".
  1.1248 +   */
  1.1249 +  get _fileSizeText()
  1.1250 +  {
  1.1251 +    // Display the file size, but show "Unknown" for negative sizes.
  1.1252 +    let fileSize = this.dataItem.maxBytes;
  1.1253 +    if (fileSize < 0) {
  1.1254 +      return DownloadsCommon.strings.sizeUnknown;
  1.1255 +    }
  1.1256 +    let [size, unit] = DownloadUtils.convertByteUnits(fileSize);
  1.1257 +    return DownloadsCommon.strings.sizeWithUnits(size, unit);
  1.1258 +  },
  1.1259 +
  1.1260 +  //////////////////////////////////////////////////////////////////////////////
  1.1261 +  //// Functions called by the panel
  1.1262 +
  1.1263 +  /**
  1.1264 +   * Starts checking whether the target file of a finished download is still
  1.1265 +   * available on disk, and sets an attribute that controls how the item is
  1.1266 +   * presented visually.
  1.1267 +   *
  1.1268 +   * The existence check is executed on a background thread.
  1.1269 +   */
  1.1270 +  verifyTargetExists: function DVI_verifyTargetExists() {
  1.1271 +    // We don't need to check if the download is not finished successfully.
  1.1272 +    if (!this.dataItem.openable) {
  1.1273 +      return;
  1.1274 +    }
  1.1275 +
  1.1276 +    OS.File.exists(this.dataItem.localFile.path).then(
  1.1277 +      function DVI_RTE_onSuccess(aExists) {
  1.1278 +        if (aExists) {
  1.1279 +          this._element.setAttribute("exists", "true");
  1.1280 +        } else {
  1.1281 +          this._element.removeAttribute("exists");
  1.1282 +        }
  1.1283 +      }.bind(this), Cu.reportError);
  1.1284 +  },
  1.1285 +};
  1.1286 +
  1.1287 +////////////////////////////////////////////////////////////////////////////////
  1.1288 +//// DownloadsViewController
  1.1289 +
  1.1290 +/**
  1.1291 + * Handles part of the user interaction events raised by the downloads list
  1.1292 + * widget, in particular the "commands" that apply to multiple items, and
  1.1293 + * dispatches the commands that apply to individual items.
  1.1294 + */
  1.1295 +const DownloadsViewController = {
  1.1296 +  //////////////////////////////////////////////////////////////////////////////
  1.1297 +  //// Initialization and termination
  1.1298 +
  1.1299 +  initialize: function DVC_initialize()
  1.1300 +  {
  1.1301 +    window.controllers.insertControllerAt(0, this);
  1.1302 +  },
  1.1303 +
  1.1304 +  terminate: function DVC_terminate()
  1.1305 +  {
  1.1306 +    window.controllers.removeController(this);
  1.1307 +  },
  1.1308 +
  1.1309 +  //////////////////////////////////////////////////////////////////////////////
  1.1310 +  //// nsIController
  1.1311 +
  1.1312 +  supportsCommand: function DVC_supportsCommand(aCommand)
  1.1313 +  {
  1.1314 +    // Firstly, determine if this is a command that we can handle.
  1.1315 +    if (!(aCommand in this.commands) &&
  1.1316 +        !(aCommand in DownloadsViewItemController.prototype.commands)) {
  1.1317 +      return false;
  1.1318 +    }
  1.1319 +    // Secondly, determine if focus is on a control in the downloads list.
  1.1320 +    let element = document.commandDispatcher.focusedElement;
  1.1321 +    while (element && element != DownloadsView.richListBox) {
  1.1322 +      element = element.parentNode;
  1.1323 +    }
  1.1324 +    // We should handle the command only if the downloads list is among the
  1.1325 +    // ancestors of the focused element.
  1.1326 +    return !!element;
  1.1327 +  },
  1.1328 +
  1.1329 +  isCommandEnabled: function DVC_isCommandEnabled(aCommand)
  1.1330 +  {
  1.1331 +    // Handle commands that are not selection-specific.
  1.1332 +    if (aCommand == "downloadsCmd_clearList") {
  1.1333 +      return DownloadsCommon.getData(window).canRemoveFinished;
  1.1334 +    }
  1.1335 +
  1.1336 +    // Other commands are selection-specific.
  1.1337 +    let element = DownloadsView.richListBox.selectedItem;
  1.1338 +    return element &&
  1.1339 +           new DownloadsViewItemController(element).isCommandEnabled(aCommand);
  1.1340 +  },
  1.1341 +
  1.1342 +  doCommand: function DVC_doCommand(aCommand)
  1.1343 +  {
  1.1344 +    // If this command is not selection-specific, execute it.
  1.1345 +    if (aCommand in this.commands) {
  1.1346 +      this.commands[aCommand].apply(this);
  1.1347 +      return;
  1.1348 +    }
  1.1349 +
  1.1350 +    // Other commands are selection-specific.
  1.1351 +    let element = DownloadsView.richListBox.selectedItem;
  1.1352 +    if (element) {
  1.1353 +      // The doCommand function also checks if the command is enabled.
  1.1354 +      new DownloadsViewItemController(element).doCommand(aCommand);
  1.1355 +    }
  1.1356 +  },
  1.1357 +
  1.1358 +  onEvent: function () { },
  1.1359 +
  1.1360 +  //////////////////////////////////////////////////////////////////////////////
  1.1361 +  //// Other functions
  1.1362 +
  1.1363 +  updateCommands: function DVC_updateCommands()
  1.1364 +  {
  1.1365 +    Object.keys(this.commands).forEach(goUpdateCommand);
  1.1366 +    Object.keys(DownloadsViewItemController.prototype.commands)
  1.1367 +          .forEach(goUpdateCommand);
  1.1368 +  },
  1.1369 +
  1.1370 +  //////////////////////////////////////////////////////////////////////////////
  1.1371 +  //// Selection-independent commands
  1.1372 +
  1.1373 +  /**
  1.1374 +   * This object contains one key for each command that operates regardless of
  1.1375 +   * the currently selected item in the list.
  1.1376 +   */
  1.1377 +  commands: {
  1.1378 +    downloadsCmd_clearList: function DVC_downloadsCmd_clearList()
  1.1379 +    {
  1.1380 +      DownloadsCommon.getData(window).removeFinished();
  1.1381 +    }
  1.1382 +  }
  1.1383 +};
  1.1384 +
  1.1385 +////////////////////////////////////////////////////////////////////////////////
  1.1386 +//// DownloadsViewItemController
  1.1387 +
  1.1388 +/**
  1.1389 + * Handles all the user interaction events, in particular the "commands",
  1.1390 + * related to a single item in the downloads list widgets.
  1.1391 + */
  1.1392 +function DownloadsViewItemController(aElement) {
  1.1393 +  let downloadGuid = aElement.getAttribute("downloadGuid");
  1.1394 +  this.dataItem = DownloadsCommon.getData(window).dataItems[downloadGuid];
  1.1395 +}
  1.1396 +
  1.1397 +DownloadsViewItemController.prototype = {
  1.1398 +  //////////////////////////////////////////////////////////////////////////////
  1.1399 +  //// Command dispatching
  1.1400 +
  1.1401 +  /**
  1.1402 +   * The DownloadDataItem controlled by this object.
  1.1403 +   */
  1.1404 +  dataItem: null,
  1.1405 +
  1.1406 +  isCommandEnabled: function DVIC_isCommandEnabled(aCommand)
  1.1407 +  {
  1.1408 +    switch (aCommand) {
  1.1409 +      case "downloadsCmd_open": {
  1.1410 +        return this.dataItem.openable && this.dataItem.localFile.exists();
  1.1411 +      }
  1.1412 +      case "downloadsCmd_show": {
  1.1413 +        return this.dataItem.localFile.exists() ||
  1.1414 +               this.dataItem.partFile.exists();
  1.1415 +      }
  1.1416 +      case "downloadsCmd_pauseResume":
  1.1417 +        return this.dataItem.inProgress && this.dataItem.resumable;
  1.1418 +      case "downloadsCmd_retry":
  1.1419 +        return this.dataItem.canRetry;
  1.1420 +      case "downloadsCmd_openReferrer":
  1.1421 +        return !!this.dataItem.referrer;
  1.1422 +      case "cmd_delete":
  1.1423 +      case "downloadsCmd_cancel":
  1.1424 +      case "downloadsCmd_copyLocation":
  1.1425 +      case "downloadsCmd_doDefault":
  1.1426 +        return true;
  1.1427 +    }
  1.1428 +    return false;
  1.1429 +  },
  1.1430 +
  1.1431 +  doCommand: function DVIC_doCommand(aCommand)
  1.1432 +  {
  1.1433 +    if (this.isCommandEnabled(aCommand)) {
  1.1434 +      this.commands[aCommand].apply(this);
  1.1435 +    }
  1.1436 +  },
  1.1437 +
  1.1438 +  //////////////////////////////////////////////////////////////////////////////
  1.1439 +  //// Item commands
  1.1440 +
  1.1441 +  /**
  1.1442 +   * This object contains one key for each command that operates on this item.
  1.1443 +   *
  1.1444 +   * In commands, the "this" identifier points to the controller item.
  1.1445 +   */
  1.1446 +  commands: {
  1.1447 +    cmd_delete: function DVIC_cmd_delete()
  1.1448 +    {
  1.1449 +      this.dataItem.remove();
  1.1450 +      PlacesUtils.bhistory.removePage(NetUtil.newURI(this.dataItem.uri));
  1.1451 +    },
  1.1452 +
  1.1453 +    downloadsCmd_cancel: function DVIC_downloadsCmd_cancel()
  1.1454 +    {
  1.1455 +      this.dataItem.cancel();
  1.1456 +    },
  1.1457 +
  1.1458 +    downloadsCmd_open: function DVIC_downloadsCmd_open()
  1.1459 +    {
  1.1460 +      this.dataItem.openLocalFile();
  1.1461 +
  1.1462 +      // We explicitly close the panel here to give the user the feedback that
  1.1463 +      // their click has been received, and we're handling the action.
  1.1464 +      // Otherwise, we'd have to wait for the file-type handler to execute
  1.1465 +      // before the panel would close. This also helps to prevent the user from
  1.1466 +      // accidentally opening a file several times.
  1.1467 +      DownloadsPanel.hidePanel();
  1.1468 +    },
  1.1469 +
  1.1470 +    downloadsCmd_show: function DVIC_downloadsCmd_show()
  1.1471 +    {
  1.1472 +      this.dataItem.showLocalFile();
  1.1473 +
  1.1474 +      // We explicitly close the panel here to give the user the feedback that
  1.1475 +      // their click has been received, and we're handling the action.
  1.1476 +      // Otherwise, we'd have to wait for the operating system file manager
  1.1477 +      // window to open before the panel closed. This also helps to prevent the
  1.1478 +      // user from opening the containing folder several times.
  1.1479 +      DownloadsPanel.hidePanel();
  1.1480 +    },
  1.1481 +
  1.1482 +    downloadsCmd_pauseResume: function DVIC_downloadsCmd_pauseResume()
  1.1483 +    {
  1.1484 +      this.dataItem.togglePauseResume();
  1.1485 +    },
  1.1486 +
  1.1487 +    downloadsCmd_retry: function DVIC_downloadsCmd_retry()
  1.1488 +    {
  1.1489 +      this.dataItem.retry();
  1.1490 +    },
  1.1491 +
  1.1492 +    downloadsCmd_openReferrer: function DVIC_downloadsCmd_openReferrer()
  1.1493 +    {
  1.1494 +      openURL(this.dataItem.referrer);
  1.1495 +    },
  1.1496 +
  1.1497 +    downloadsCmd_copyLocation: function DVIC_downloadsCmd_copyLocation()
  1.1498 +    {
  1.1499 +      let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
  1.1500 +                      .getService(Ci.nsIClipboardHelper);
  1.1501 +      clipboard.copyString(this.dataItem.uri, document);
  1.1502 +    },
  1.1503 +
  1.1504 +    downloadsCmd_doDefault: function DVIC_downloadsCmd_doDefault()
  1.1505 +    {
  1.1506 +      const nsIDM = Ci.nsIDownloadManager;
  1.1507 +
  1.1508 +      // Determine the default command for the current item.
  1.1509 +      let defaultCommand = function () {
  1.1510 +        switch (this.dataItem.state) {
  1.1511 +          case nsIDM.DOWNLOAD_NOTSTARTED:       return "downloadsCmd_cancel";
  1.1512 +          case nsIDM.DOWNLOAD_FINISHED:         return "downloadsCmd_open";
  1.1513 +          case nsIDM.DOWNLOAD_FAILED:           return "downloadsCmd_retry";
  1.1514 +          case nsIDM.DOWNLOAD_CANCELED:         return "downloadsCmd_retry";
  1.1515 +          case nsIDM.DOWNLOAD_PAUSED:           return "downloadsCmd_pauseResume";
  1.1516 +          case nsIDM.DOWNLOAD_QUEUED:           return "downloadsCmd_cancel";
  1.1517 +          case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: return "downloadsCmd_openReferrer";
  1.1518 +          case nsIDM.DOWNLOAD_SCANNING:         return "downloadsCmd_show";
  1.1519 +          case nsIDM.DOWNLOAD_DIRTY:            return "downloadsCmd_openReferrer";
  1.1520 +          case nsIDM.DOWNLOAD_BLOCKED_POLICY:   return "downloadsCmd_openReferrer";
  1.1521 +        }
  1.1522 +        return "";
  1.1523 +      }.apply(this);
  1.1524 +      if (defaultCommand && this.isCommandEnabled(defaultCommand))
  1.1525 +        this.doCommand(defaultCommand);
  1.1526 +    }
  1.1527 +  }
  1.1528 +};
  1.1529 +
  1.1530 +
  1.1531 +////////////////////////////////////////////////////////////////////////////////
  1.1532 +//// DownloadsSummary
  1.1533 +
  1.1534 +/**
  1.1535 + * Manages the summary at the bottom of the downloads panel list if the number
  1.1536 + * of items in the list exceeds the panels limit.
  1.1537 + */
  1.1538 +const DownloadsSummary = {
  1.1539 +
  1.1540 +  /**
  1.1541 +   * Sets the active state of the summary. When active, the summary subscribes
  1.1542 +   * to the DownloadsCommon DownloadsSummaryData singleton.
  1.1543 +   *
  1.1544 +   * @param aActive
  1.1545 +   *        Set to true to activate the summary.
  1.1546 +   */
  1.1547 +  set active(aActive)
  1.1548 +  {
  1.1549 +    if (aActive == this._active || !this._summaryNode) {
  1.1550 +      return this._active;
  1.1551 +    }
  1.1552 +    if (aActive) {
  1.1553 +      DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit)
  1.1554 +                     .refreshView(this);
  1.1555 +    } else {
  1.1556 +      DownloadsFooter.showingSummary = false;
  1.1557 +    }
  1.1558 +
  1.1559 +    return this._active = aActive;
  1.1560 +  },
  1.1561 +
  1.1562 +  /**
  1.1563 +   * Returns the active state of the downloads summary.
  1.1564 +   */
  1.1565 +  get active() this._active,
  1.1566 +
  1.1567 +  _active: false,
  1.1568 +
  1.1569 +  /**
  1.1570 +   * Sets whether or not we show the progress bar.
  1.1571 +   *
  1.1572 +   * @param aShowingProgress
  1.1573 +   *        True if we should show the progress bar.
  1.1574 +   */
  1.1575 +  set showingProgress(aShowingProgress)
  1.1576 +  {
  1.1577 +    if (aShowingProgress) {
  1.1578 +      this._summaryNode.setAttribute("inprogress", "true");
  1.1579 +    } else {
  1.1580 +      this._summaryNode.removeAttribute("inprogress");
  1.1581 +    }
  1.1582 +    // If progress isn't being shown, then we simply do not show the summary.
  1.1583 +    return DownloadsFooter.showingSummary = aShowingProgress;
  1.1584 +  },
  1.1585 +
  1.1586 +  /**
  1.1587 +   * Sets the amount of progress that is visible in the progress bar.
  1.1588 +   *
  1.1589 +   * @param aValue
  1.1590 +   *        A value between 0 and 100 to represent the progress of the
  1.1591 +   *        summarized downloads.
  1.1592 +   */
  1.1593 +  set percentComplete(aValue)
  1.1594 +  {
  1.1595 +    if (this._progressNode) {
  1.1596 +      this._progressNode.setAttribute("value", aValue);
  1.1597 +    }
  1.1598 +    return aValue;
  1.1599 +  },
  1.1600 +
  1.1601 +  /**
  1.1602 +   * Sets the description for the download summary.
  1.1603 +   *
  1.1604 +   * @param aValue
  1.1605 +   *        A string representing the description of the summarized
  1.1606 +   *        downloads.
  1.1607 +   */
  1.1608 +  set description(aValue)
  1.1609 +  {
  1.1610 +    if (this._descriptionNode) {
  1.1611 +      this._descriptionNode.setAttribute("value", aValue);
  1.1612 +      this._descriptionNode.setAttribute("tooltiptext", aValue);
  1.1613 +    }
  1.1614 +    return aValue;
  1.1615 +  },
  1.1616 +
  1.1617 +  /**
  1.1618 +   * Sets the details for the download summary, such as the time remaining,
  1.1619 +   * the amount of bytes transferred, etc.
  1.1620 +   *
  1.1621 +   * @param aValue
  1.1622 +   *        A string representing the details of the summarized
  1.1623 +   *        downloads.
  1.1624 +   */
  1.1625 +  set details(aValue)
  1.1626 +  {
  1.1627 +    if (this._detailsNode) {
  1.1628 +      this._detailsNode.setAttribute("value", aValue);
  1.1629 +      this._detailsNode.setAttribute("tooltiptext", aValue);
  1.1630 +    }
  1.1631 +    return aValue;
  1.1632 +  },
  1.1633 +
  1.1634 +  /**
  1.1635 +   * Focuses the root element of the summary.
  1.1636 +   */
  1.1637 +  focus: function()
  1.1638 +  {
  1.1639 +    if (this._summaryNode) {
  1.1640 +      this._summaryNode.focus();
  1.1641 +    }
  1.1642 +  },
  1.1643 +
  1.1644 +  /**
  1.1645 +   * Respond to keydown events on the Downloads Summary node.
  1.1646 +   *
  1.1647 +   * @param aEvent
  1.1648 +   *        The keydown event being handled.
  1.1649 +   */
  1.1650 +  onKeyDown: function DS_onKeyDown(aEvent)
  1.1651 +  {
  1.1652 +    if (aEvent.charCode == " ".charCodeAt(0) ||
  1.1653 +        aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
  1.1654 +      DownloadsPanel.showDownloadsHistory();
  1.1655 +    }
  1.1656 +  },
  1.1657 +
  1.1658 +  /**
  1.1659 +   * Respond to click events on the Downloads Summary node.
  1.1660 +   *
  1.1661 +   * @param aEvent
  1.1662 +   *        The click event being handled.
  1.1663 +   */
  1.1664 +  onClick: function DS_onClick(aEvent)
  1.1665 +  {
  1.1666 +    DownloadsPanel.showDownloadsHistory();
  1.1667 +  },
  1.1668 +
  1.1669 +  /**
  1.1670 +   * Element corresponding to the root of the downloads summary.
  1.1671 +   */
  1.1672 +  get _summaryNode()
  1.1673 +  {
  1.1674 +    let node = document.getElementById("downloadsSummary");
  1.1675 +    if (!node) {
  1.1676 +      return null;
  1.1677 +    }
  1.1678 +    delete this._summaryNode;
  1.1679 +    return this._summaryNode = node;
  1.1680 +  },
  1.1681 +
  1.1682 +  /**
  1.1683 +   * Element corresponding to the progress bar in the downloads summary.
  1.1684 +   */
  1.1685 +  get _progressNode()
  1.1686 +  {
  1.1687 +    let node = document.getElementById("downloadsSummaryProgress");
  1.1688 +    if (!node) {
  1.1689 +      return null;
  1.1690 +    }
  1.1691 +    delete this._progressNode;
  1.1692 +    return this._progressNode = node;
  1.1693 +  },
  1.1694 +
  1.1695 +  /**
  1.1696 +   * Element corresponding to the main description of the downloads
  1.1697 +   * summary.
  1.1698 +   */
  1.1699 +  get _descriptionNode()
  1.1700 +  {
  1.1701 +    let node = document.getElementById("downloadsSummaryDescription");
  1.1702 +    if (!node) {
  1.1703 +      return null;
  1.1704 +    }
  1.1705 +    delete this._descriptionNode;
  1.1706 +    return this._descriptionNode = node;
  1.1707 +  },
  1.1708 +
  1.1709 +  /**
  1.1710 +   * Element corresponding to the secondary description of the downloads
  1.1711 +   * summary.
  1.1712 +   */
  1.1713 +  get _detailsNode()
  1.1714 +  {
  1.1715 +    let node = document.getElementById("downloadsSummaryDetails");
  1.1716 +    if (!node) {
  1.1717 +      return null;
  1.1718 +    }
  1.1719 +    delete this._detailsNode;
  1.1720 +    return this._detailsNode = node;
  1.1721 +  }
  1.1722 +}
  1.1723 +
  1.1724 +////////////////////////////////////////////////////////////////////////////////
  1.1725 +//// DownloadsFooter
  1.1726 +
  1.1727 +/**
  1.1728 + * Manages events sent to to the footer vbox, which contains both the
  1.1729 + * DownloadsSummary as well as the "Show All Downloads" button.
  1.1730 + */
  1.1731 +const DownloadsFooter = {
  1.1732 +
  1.1733 +  /**
  1.1734 +   * Focuses the appropriate element within the footer. If the summary
  1.1735 +   * is visible, focus it. If not, focus the "Show All Downloads"
  1.1736 +   * button.
  1.1737 +   */
  1.1738 +  focus: function DF_focus()
  1.1739 +  {
  1.1740 +    if (this._showingSummary) {
  1.1741 +      DownloadsSummary.focus();
  1.1742 +    } else {
  1.1743 +      DownloadsView.downloadsHistory.focus();
  1.1744 +    }
  1.1745 +  },
  1.1746 +
  1.1747 +  _showingSummary: false,
  1.1748 +
  1.1749 +  /**
  1.1750 +   * Sets whether or not the Downloads Summary should be displayed in the
  1.1751 +   * footer. If not, the "Show All Downloads" button is shown instead.
  1.1752 +   */
  1.1753 +  set showingSummary(aValue)
  1.1754 +  {
  1.1755 +    if (this._footerNode) {
  1.1756 +      if (aValue) {
  1.1757 +        this._footerNode.setAttribute("showingsummary", "true");
  1.1758 +      } else {
  1.1759 +        this._footerNode.removeAttribute("showingsummary");
  1.1760 +      }
  1.1761 +      this._showingSummary = aValue;
  1.1762 +    }
  1.1763 +    return aValue;
  1.1764 +  },
  1.1765 +
  1.1766 +  /**
  1.1767 +   * Element corresponding to the footer of the downloads panel.
  1.1768 +   */
  1.1769 +  get _footerNode()
  1.1770 +  {
  1.1771 +    let node = document.getElementById("downloadsFooter");
  1.1772 +    if (!node) {
  1.1773 +      return null;
  1.1774 +    }
  1.1775 +    delete this._footerNode;
  1.1776 +    return this._footerNode = node;
  1.1777 +  }
  1.1778 +};

mercurial