browser/components/downloads/content/allDownloadsViewOverlay.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/components/downloads/content/allDownloadsViewOverlay.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1594 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +/**
     1.9 + * THE PLACES VIEW IMPLEMENTED IN THIS FILE HAS A VERY PARTICULAR USE CASE.
    1.10 + * IT IS HIGHLY RECOMMENDED NOT TO EXTEND IT FOR ANY OTHER USE CASES OR RELY
    1.11 + * ON IT AS AN API.
    1.12 + */
    1.13 +
    1.14 +let Cu = Components.utils;
    1.15 +let Ci = Components.interfaces;
    1.16 +let Cc = Components.classes;
    1.17 +
    1.18 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.19 +Cu.import("resource://gre/modules/Services.jsm");
    1.20 +Cu.import("resource://gre/modules/NetUtil.jsm");
    1.21 +Cu.import("resource://gre/modules/DownloadUtils.jsm");
    1.22 +Cu.import("resource:///modules/DownloadsCommon.jsm");
    1.23 +Cu.import("resource://gre/modules/PlacesUtils.jsm");
    1.24 +Cu.import("resource://gre/modules/osfile.jsm");
    1.25 +
    1.26 +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
    1.27 +                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
    1.28 +XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
    1.29 +                                  "resource:///modules/RecentWindow.jsm");
    1.30 +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
    1.31 +                                  "resource://gre/modules/FileUtils.jsm");
    1.32 +
    1.33 +const nsIDM = Ci.nsIDownloadManager;
    1.34 +
    1.35 +const DESTINATION_FILE_URI_ANNO  = "downloads/destinationFileURI";
    1.36 +const DOWNLOAD_META_DATA_ANNO    = "downloads/metaData";
    1.37 +
    1.38 +const DOWNLOAD_VIEW_SUPPORTED_COMMANDS =
    1.39 + ["cmd_delete", "cmd_copy", "cmd_paste", "cmd_selectAll",
    1.40 +  "downloadsCmd_pauseResume", "downloadsCmd_cancel",
    1.41 +  "downloadsCmd_open", "downloadsCmd_show", "downloadsCmd_retry",
    1.42 +  "downloadsCmd_openReferrer", "downloadsCmd_clearDownloads"];
    1.43 +
    1.44 +const NOT_AVAILABLE = Number.MAX_VALUE;
    1.45 +
    1.46 +/**
    1.47 + * A download element shell is responsible for handling the commands and the
    1.48 + * displayed data for a single download view element. The download element
    1.49 + * could represent either a past download (for which we get data from places)  or
    1.50 + * a "session" download (using a data-item object. See DownloadsCommon.jsm), or both.
    1.51 + *
    1.52 + * Once initialized with either a data item or a places node, the created richlistitem
    1.53 + * can be accessed through the |element| getter, and can then be inserted/removed from
    1.54 + * a richlistbox.
    1.55 + *
    1.56 + * The shell doesn't take care of inserting the item, or removing it when it's no longer
    1.57 + * valid. That's the caller (a DownloadsPlacesView object) responsibility.
    1.58 + *
    1.59 + * The caller is also responsible for "passing over" notification from both the
    1.60 + * download-view and the places-result-observer, in the following manner:
    1.61 + *  - The DownloadsPlacesView object implements getViewItem of the download-view
    1.62 + *    pseudo interface.  It returns this object (therefore we implement
    1.63 + *    onStateChangea and onProgressChange here).
    1.64 + *  - The DownloadsPlacesView object adds itself as a places result observer and
    1.65 + *    calls this object's placesNodeIconChanged, placesNodeTitleChanged and
    1.66 + *    placeNodeAnnotationChanged from its callbacks.
    1.67 + *
    1.68 + * @param [optional] aDataItem
    1.69 + *        The data item of a the session download. Required if aPlacesNode is not set
    1.70 + * @param [optional] aPlacesNode
    1.71 + *        The places node for a past download. Required if aDataItem is not set.
    1.72 + * @param [optional] aAnnotations
    1.73 + *        Map containing annotations values, to speed up the initial loading.
    1.74 + */
    1.75 +function DownloadElementShell(aDataItem, aPlacesNode, aAnnotations) {
    1.76 +  this._element = document.createElement("richlistitem");
    1.77 +  this._element._shell = this;
    1.78 +
    1.79 +  this._element.classList.add("download");
    1.80 +  this._element.classList.add("download-state");
    1.81 +
    1.82 +  if (aAnnotations)
    1.83 +    this._annotations = aAnnotations;
    1.84 +  if (aDataItem)
    1.85 +    this.dataItem = aDataItem;
    1.86 +  if (aPlacesNode)
    1.87 +    this.placesNode = aPlacesNode;
    1.88 +}
    1.89 +
    1.90 +DownloadElementShell.prototype = {
    1.91 +  // The richlistitem for the download
    1.92 +  get element() this._element,
    1.93 +
    1.94 +  /**
    1.95 +   * Manages the "active" state of the shell.  By default all the shells
    1.96 +   * without a dataItem are inactive, thus their UI is not updated.  They must
    1.97 +   * be activated when entering the visible area.  Session downloads are
    1.98 +   * always active since they always have a dataItem.
    1.99 +   */
   1.100 +  ensureActive: function DES_ensureActive() {
   1.101 +    if (!this._active) {
   1.102 +      this._active = true;
   1.103 +      this._element.setAttribute("active", true);
   1.104 +      this._updateUI();
   1.105 +    }
   1.106 +  },
   1.107 +  get active() !!this._active,
   1.108 +
   1.109 +  // The data item for the download
   1.110 +  _dataItem: null,
   1.111 +  get dataItem() this._dataItem,
   1.112 +
   1.113 +  set dataItem(aValue) {
   1.114 +    if (this._dataItem != aValue) {
   1.115 +      if (!aValue && !this._placesNode)
   1.116 +        throw new Error("Should always have either a dataItem or a placesNode");
   1.117 +
   1.118 +      this._dataItem = aValue;
   1.119 +      if (!this.active)
   1.120 +        this.ensureActive();
   1.121 +      else
   1.122 +        this._updateUI();
   1.123 +    }
   1.124 +    return aValue;
   1.125 +  },
   1.126 +
   1.127 +  _placesNode: null,
   1.128 +  get placesNode() this._placesNode,
   1.129 +  set placesNode(aValue) {
   1.130 +    if (this._placesNode != aValue) {
   1.131 +      if (!aValue && !this._dataItem)
   1.132 +        throw new Error("Should always have either a dataItem or a placesNode");
   1.133 +
   1.134 +      // Preserve the annotations map if this is the first loading and we got
   1.135 +      // cached values.
   1.136 +      if (this._placesNode || !this._annotations) {
   1.137 +        this._annotations = new Map();
   1.138 +      }
   1.139 +
   1.140 +      this._placesNode = aValue;
   1.141 +
   1.142 +      // We don't need to update the UI if we had a data item, because
   1.143 +      // the places information isn't used in this case.
   1.144 +      if (!this._dataItem && this.active)
   1.145 +        this._updateUI();
   1.146 +    }
   1.147 +    return aValue;
   1.148 +  },
   1.149 +
   1.150 +  // The download uri (as a string)
   1.151 +  get downloadURI() {
   1.152 +    if (this._dataItem)
   1.153 +     return this._dataItem.uri;
   1.154 +    if (this._placesNode)
   1.155 +      return this._placesNode.uri;
   1.156 +    throw new Error("Unexpected download element state");
   1.157 +  },
   1.158 +
   1.159 +  get _downloadURIObj() {
   1.160 +    if (!("__downloadURIObj" in this))
   1.161 +      this.__downloadURIObj = NetUtil.newURI(this.downloadURI);
   1.162 +    return this.__downloadURIObj;
   1.163 +  },
   1.164 +
   1.165 +  _getIcon: function DES__getIcon() {
   1.166 +    let metaData = this.getDownloadMetaData();
   1.167 +    if ("filePath" in metaData)
   1.168 +      return "moz-icon://" + metaData.filePath + "?size=32";
   1.169 +
   1.170 +    if (this._placesNode) {
   1.171 +      // Try to extract an extension from the uri.
   1.172 +      let ext = this._downloadURIObj.QueryInterface(Ci.nsIURL).fileExtension;
   1.173 +      if (ext)
   1.174 +        return "moz-icon://." + ext + "?size=32";
   1.175 +      return this._placesNode.icon || "moz-icon://.unknown?size=32";
   1.176 +    }
   1.177 +    if (this._dataItem)
   1.178 +      throw new Error("Session-download items should always have a target file uri");
   1.179 +
   1.180 +    throw new Error("Unexpected download element state");
   1.181 +  },
   1.182 +
   1.183 +  // Helper for getting a places annotation set for the download.
   1.184 +  _getAnnotation: function DES__getAnnotation(aAnnotation, aDefaultValue) {
   1.185 +    let value;
   1.186 +    if (this._annotations.has(aAnnotation))
   1.187 +      value = this._annotations.get(aAnnotation);
   1.188 +
   1.189 +    // If the value is cached, or we know it doesn't exist, avoid a database
   1.190 +    // lookup.
   1.191 +    if (value === undefined) {
   1.192 +      try {
   1.193 +        value = PlacesUtils.annotations.getPageAnnotation(
   1.194 +          this._downloadURIObj, aAnnotation);
   1.195 +      }
   1.196 +      catch(ex) {
   1.197 +        value = NOT_AVAILABLE;
   1.198 +      }
   1.199 +    }
   1.200 +
   1.201 +    if (value === NOT_AVAILABLE) {
   1.202 +      if (aDefaultValue === undefined) {
   1.203 +        throw new Error("Could not get required annotation '" + aAnnotation +
   1.204 +                        "' for download with url '" + this.downloadURI + "'");
   1.205 +      }
   1.206 +      value = aDefaultValue;
   1.207 +    }
   1.208 +
   1.209 +    this._annotations.set(aAnnotation, value);
   1.210 +    return value;
   1.211 +  },
   1.212 +
   1.213 +  _fetchTargetFileInfo: function DES__fetchTargetFileInfo(aUpdateMetaDataAndStatusUI = false) {
   1.214 +    if (this._targetFileInfoFetched)
   1.215 +      throw new Error("_fetchTargetFileInfo should not be called if the information was already fetched");
   1.216 +    if (!this.active)
   1.217 +      throw new Error("Trying to _fetchTargetFileInfo on an inactive download shell");
   1.218 +
   1.219 +    let path = this.getDownloadMetaData().filePath;
   1.220 +
   1.221 +    // In previous version, the target file annotations were not set,
   1.222 +    // so we cannot tell where is the file.
   1.223 +    if (path === undefined) {
   1.224 +      this._targetFileInfoFetched = true;
   1.225 +      this._targetFileExists = false;
   1.226 +      if (aUpdateMetaDataAndStatusUI) {
   1.227 +        this._metaData = null;
   1.228 +        this._updateDownloadStatusUI();
   1.229 +      }
   1.230 +      // Here we don't need to update the download commands,
   1.231 +      // as the state is unknown as it was.
   1.232 +      return;
   1.233 +    }
   1.234 +
   1.235 +    OS.File.stat(path).then(
   1.236 +      function onSuccess(fileInfo) {
   1.237 +        this._targetFileInfoFetched = true;
   1.238 +        this._targetFileExists = true;
   1.239 +        this._targetFileSize = fileInfo.size;
   1.240 +        if (aUpdateMetaDataAndStatusUI) {
   1.241 +          this._metaData = null;
   1.242 +          this._updateDownloadStatusUI();
   1.243 +        }
   1.244 +        if (this._element.selected)
   1.245 +          goUpdateDownloadCommands();
   1.246 +      }.bind(this),
   1.247 +
   1.248 +      function onFailure(aReason) {
   1.249 +        if (aReason instanceof OS.File.Error && aReason.becauseNoSuchFile) {
   1.250 +          this._targetFileInfoFetched = true;
   1.251 +          this._targetFileExists = false;
   1.252 +        }
   1.253 +        else {
   1.254 +          Cu.reportError("Could not fetch info for target file (reason: " +
   1.255 +                         aReason + ")");
   1.256 +        }
   1.257 +
   1.258 +        if (aUpdateMetaDataAndStatusUI) {
   1.259 +          this._metaData = null;
   1.260 +          this._updateDownloadStatusUI();
   1.261 +        }
   1.262 +
   1.263 +        if (this._element.selected)
   1.264 +          goUpdateDownloadCommands();
   1.265 +      }.bind(this)
   1.266 +    );
   1.267 +  },
   1.268 +
   1.269 +  _getAnnotatedMetaData: function DES__getAnnotatedMetaData()
   1.270 +    JSON.parse(this._getAnnotation(DOWNLOAD_META_DATA_ANNO)),
   1.271 +
   1.272 +  _extractFilePathAndNameFromFileURI:
   1.273 +  function DES__extractFilePathAndNameFromFileURI(aFileURI) {
   1.274 +    let file = Cc["@mozilla.org/network/protocol;1?name=file"]
   1.275 +                .getService(Ci.nsIFileProtocolHandler)
   1.276 +                .getFileFromURLSpec(aFileURI);
   1.277 +    return [file.path, file.leafName];
   1.278 +  },
   1.279 +
   1.280 +  /**
   1.281 +   * Retrieve the meta data object for the download.  The following fields
   1.282 +   * may be set.
   1.283 +   *
   1.284 +   * - state - any download state defined in nsIDownloadManager.  If this field
   1.285 +   *   is not set, the download state is unknown.
   1.286 +   * - endTime: the end time of the download.
   1.287 +   * - filePath: the downloaded file path on the file system, when it
   1.288 +   *   was downloaded.  The file may not exist.  This is set for session
   1.289 +   *   downloads that have a local file set, and for history downloads done
   1.290 +   *   after the landing of bug 591289.
   1.291 +   * - fileName: the downloaded file name on the file system. Set if filePath
   1.292 +   *   is set.
   1.293 +   * - displayName: the user-facing label for the download.  This is always
   1.294 +   *   set.  If available, it's set to the downloaded file name.  If not,
   1.295 +   *   the places title for the download uri is used it's set.  As a last
   1.296 +   *   resort, we fallback to the download uri.
   1.297 +   * - fileSize (only set for downloads which completed succesfully):
   1.298 +   *   the downloaded file size.  For downloads done after the landing of
   1.299 +   *   bug 826991, this value is "static" - that is, it does not necessarily
   1.300 +   *   mean that the file is in place and has this size.
   1.301 +   */
   1.302 +  getDownloadMetaData: function DES_getDownloadMetaData() {
   1.303 +    if (!this._metaData) {
   1.304 +      if (this._dataItem) {
   1.305 +        this._metaData = {
   1.306 +          state:       this._dataItem.state,
   1.307 +          endTime:     this._dataItem.endTime,
   1.308 +          fileName:    this._dataItem.target,
   1.309 +          displayName: this._dataItem.target
   1.310 +        };
   1.311 +        if (this._dataItem.done)
   1.312 +          this._metaData.fileSize = this._dataItem.maxBytes;
   1.313 +        if (this._dataItem.localFile)
   1.314 +          this._metaData.filePath = this._dataItem.localFile.path;
   1.315 +      }
   1.316 +      else {
   1.317 +        try {
   1.318 +          this._metaData = this._getAnnotatedMetaData();
   1.319 +        }
   1.320 +        catch(ex) {
   1.321 +          this._metaData = { };
   1.322 +          if (this._targetFileInfoFetched && this._targetFileExists) {
   1.323 +            this._metaData.state = this._targetFileSize > 0 ?
   1.324 +              nsIDM.DOWNLOAD_FINISHED : nsIDM.DOWNLOAD_FAILED;
   1.325 +            this._metaData.fileSize = this._targetFileSize;
   1.326 +          }
   1.327 +
   1.328 +          // This is actually the start-time, but it's the best we can get.
   1.329 +          this._metaData.endTime = this._placesNode.time / 1000;
   1.330 +        }
   1.331 +
   1.332 +        try {
   1.333 +          let targetFileURI = this._getAnnotation(DESTINATION_FILE_URI_ANNO);
   1.334 +          [this._metaData.filePath, this._metaData.fileName] =
   1.335 +            this._extractFilePathAndNameFromFileURI(targetFileURI);
   1.336 +          this._metaData.displayName = this._metaData.fileName;
   1.337 +        }
   1.338 +        catch(ex) {
   1.339 +          this._metaData.displayName = this._placesNode.title || this.downloadURI;
   1.340 +        }
   1.341 +      }
   1.342 +    }
   1.343 +    return this._metaData;
   1.344 +  },
   1.345 +
   1.346 +  // The status text for the download
   1.347 +  _getStatusText: function DES__getStatusText() {
   1.348 +    let s = DownloadsCommon.strings;
   1.349 +    if (this._dataItem && this._dataItem.inProgress) {
   1.350 +      if (this._dataItem.paused) {
   1.351 +        let transfer =
   1.352 +          DownloadUtils.getTransferTotal(this._dataItem.currBytes,
   1.353 +                                         this._dataItem.maxBytes);
   1.354 +
   1.355 +         // We use the same XUL label to display both the state and the amount
   1.356 +         // transferred, for example "Paused -  1.1 MB".
   1.357 +         return s.statusSeparatorBeforeNumber(s.statePaused, transfer);
   1.358 +      }
   1.359 +      if (this._dataItem.state == nsIDM.DOWNLOAD_DOWNLOADING) {
   1.360 +        let [status, newEstimatedSecondsLeft] =
   1.361 +          DownloadUtils.getDownloadStatus(this.dataItem.currBytes,
   1.362 +                                          this.dataItem.maxBytes,
   1.363 +                                          this.dataItem.speed,
   1.364 +                                          this._lastEstimatedSecondsLeft || Infinity);
   1.365 +        this._lastEstimatedSecondsLeft = newEstimatedSecondsLeft;
   1.366 +        return status;
   1.367 +      }
   1.368 +      if (this._dataItem.starting) {
   1.369 +        return s.stateStarting;
   1.370 +      }
   1.371 +      if (this._dataItem.state == nsIDM.DOWNLOAD_SCANNING) {
   1.372 +        return s.stateScanning;
   1.373 +      }
   1.374 +
   1.375 +      throw new Error("_getStatusText called with a bogus download state");
   1.376 +    }
   1.377 +
   1.378 +    // This is a not-in-progress or history download.
   1.379 +    let stateLabel = "";
   1.380 +    let state = this.getDownloadMetaData().state;
   1.381 +    switch (state) {
   1.382 +      case nsIDM.DOWNLOAD_FAILED:
   1.383 +        stateLabel = s.stateFailed;
   1.384 +        break;
   1.385 +      case nsIDM.DOWNLOAD_CANCELED:
   1.386 +        stateLabel = s.stateCanceled;
   1.387 +        break;
   1.388 +      case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
   1.389 +        stateLabel = s.stateBlockedParentalControls;
   1.390 +        break;
   1.391 +      case nsIDM.DOWNLOAD_BLOCKED_POLICY:
   1.392 +        stateLabel = s.stateBlockedPolicy;
   1.393 +        break;
   1.394 +      case nsIDM.DOWNLOAD_DIRTY:
   1.395 +        stateLabel = s.stateDirty;
   1.396 +        break;
   1.397 +      case nsIDM.DOWNLOAD_FINISHED:{
   1.398 +        // For completed downloads, show the file size (e.g. "1.5 MB")
   1.399 +        let metaData = this.getDownloadMetaData();
   1.400 +        if ("fileSize" in metaData) {
   1.401 +          let [size, unit] = DownloadUtils.convertByteUnits(metaData.fileSize);
   1.402 +          stateLabel = s.sizeWithUnits(size, unit);
   1.403 +          break;
   1.404 +        }
   1.405 +        // Fallback to default unknown state.
   1.406 +      }
   1.407 +      default:
   1.408 +        stateLabel = s.sizeUnknown;
   1.409 +        break;
   1.410 +    }
   1.411 +
   1.412 +    // TODO (bug 829201): history downloads should get the referrer from Places.
   1.413 +    let referrer = this._dataItem && this._dataItem.referrer ||
   1.414 +                   this.downloadURI;
   1.415 +    let [displayHost, fullHost] = DownloadUtils.getURIHost(referrer);
   1.416 +
   1.417 +    let date = new Date(this.getDownloadMetaData().endTime);
   1.418 +    let [displayDate, fullDate] = DownloadUtils.getReadableDates(date);
   1.419 +
   1.420 +    // We use the same XUL label to display the state, the host name, and the
   1.421 +    // end time.
   1.422 +    let firstPart = s.statusSeparator(stateLabel, displayHost);
   1.423 +    return s.statusSeparator(firstPart, displayDate);
   1.424 +  },
   1.425 +
   1.426 +  // The progressmeter element for the download
   1.427 +  get _progressElement() {
   1.428 +    if (!("__progressElement" in this)) {
   1.429 +      this.__progressElement =
   1.430 +        document.getAnonymousElementByAttribute(this._element, "anonid",
   1.431 +                                                "progressmeter");
   1.432 +    }
   1.433 +    return this.__progressElement;
   1.434 +  },
   1.435 +
   1.436 +  // Updates the download state attribute (and by that hide/unhide the
   1.437 +  // appropriate buttons and context menu items), the status text label,
   1.438 +  // and the progress meter.
   1.439 +  _updateDownloadStatusUI: function  DES__updateDownloadStatusUI() {
   1.440 +    if (!this.active)
   1.441 +      throw new Error("_updateDownloadStatusUI called for an inactive item.");
   1.442 +
   1.443 +    let state = this.getDownloadMetaData().state;
   1.444 +    if (state !== undefined)
   1.445 +      this._element.setAttribute("state", state);
   1.446 +
   1.447 +    this._element.setAttribute("status", this._getStatusText());
   1.448 +
   1.449 +    // For past-downloads, we're done. For session-downloads, we may also need
   1.450 +    // to update the progress-meter.
   1.451 +    if (!this._dataItem)
   1.452 +      return;
   1.453 +
   1.454 +    // Copied from updateProgress in downloads.js.
   1.455 +    if (this._dataItem.starting) {
   1.456 +      // Before the download starts, the progress meter has its initial value.
   1.457 +      this._element.setAttribute("progressmode", "normal");
   1.458 +      this._element.setAttribute("progress", "0");
   1.459 +    }
   1.460 +    else if (this._dataItem.state == nsIDM.DOWNLOAD_SCANNING ||
   1.461 +             this._dataItem.percentComplete == -1) {
   1.462 +      // We might not know the progress of a running download, and we don't know
   1.463 +      // the remaining time during the malware scanning phase.
   1.464 +      this._element.setAttribute("progressmode", "undetermined");
   1.465 +    }
   1.466 +    else {
   1.467 +      // This is a running download of which we know the progress.
   1.468 +      this._element.setAttribute("progressmode", "normal");
   1.469 +      this._element.setAttribute("progress", this._dataItem.percentComplete);
   1.470 +    }
   1.471 +
   1.472 +    // Dispatch the ValueChange event for accessibility, if possible.
   1.473 +    if (this._progressElement) {
   1.474 +      let event = document.createEvent("Events");
   1.475 +      event.initEvent("ValueChange", true, true);
   1.476 +      this._progressElement.dispatchEvent(event);
   1.477 +    }
   1.478 +  },
   1.479 +
   1.480 +  _updateDisplayNameAndIcon: function DES__updateDisplayNameAndIcon() {
   1.481 +    let metaData = this.getDownloadMetaData();
   1.482 +    this._element.setAttribute("displayName", metaData.displayName);
   1.483 +    this._element.setAttribute("image", this._getIcon());
   1.484 +  },
   1.485 +
   1.486 +  _updateUI: function DES__updateUI() {
   1.487 +    if (!this.active)
   1.488 +      throw new Error("Trying to _updateUI on an inactive download shell");
   1.489 +
   1.490 +    this._metaData = null;
   1.491 +    this._targetFileInfoFetched = false;
   1.492 +
   1.493 +    this._updateDisplayNameAndIcon();
   1.494 +
   1.495 +    // For history downloads done in past releases, the downloads/metaData
   1.496 +    // annotation is not set, and therefore we cannot tell the download
   1.497 +    // state without the target file information.
   1.498 +    if (this._dataItem || this.getDownloadMetaData().state !== undefined)
   1.499 +      this._updateDownloadStatusUI();
   1.500 +    else
   1.501 +      this._fetchTargetFileInfo(true);
   1.502 +  },
   1.503 +
   1.504 +  placesNodeIconChanged: function DES_placesNodeIconChanged() {
   1.505 +    if (!this._dataItem)
   1.506 +      this._element.setAttribute("image", this._getIcon());
   1.507 +  },
   1.508 +
   1.509 +  placesNodeTitleChanged: function DES_placesNodeTitleChanged() {
   1.510 +    // If there's a file path, we use the leaf name for the title.
   1.511 +    if (!this._dataItem && this.active && !this.getDownloadMetaData().filePath) {
   1.512 +      this._metaData = null;
   1.513 +      this._updateDisplayNameAndIcon();
   1.514 +    }
   1.515 +  },
   1.516 +
   1.517 +  placesNodeAnnotationChanged: function DES_placesNodeAnnotationChanged(aAnnoName) {
   1.518 +    this._annotations.delete(aAnnoName);
   1.519 +    if (!this._dataItem && this.active) {
   1.520 +      if (aAnnoName == DOWNLOAD_META_DATA_ANNO) {
   1.521 +        let metaData = this.getDownloadMetaData();
   1.522 +        let annotatedMetaData = this._getAnnotatedMetaData();
   1.523 +        metaData.endTme = annotatedMetaData.endTime;
   1.524 +        if ("fileSize" in annotatedMetaData)
   1.525 +          metaData.fileSize = annotatedMetaData.fileSize;
   1.526 +        else
   1.527 +          delete metaData.fileSize;
   1.528 +
   1.529 +        if (metaData.state != annotatedMetaData.state) {
   1.530 +          metaData.state = annotatedMetaData.state;
   1.531 +          if (this._element.selected)
   1.532 +            goUpdateDownloadCommands();
   1.533 +        }
   1.534 +
   1.535 +        this._updateDownloadStatusUI();
   1.536 +      }
   1.537 +      else if (aAnnoName == DESTINATION_FILE_URI_ANNO) {
   1.538 +        let metaData = this.getDownloadMetaData();
   1.539 +        let targetFileURI = this._getAnnotation(DESTINATION_FILE_URI_ANNO);
   1.540 +        [metaData.filePath, metaData.fileName] =
   1.541 +            this._extractFilePathAndNameFromFileURI(targetFileURI);
   1.542 +        metaData.displayName = metaData.fileName;
   1.543 +        this._updateDisplayNameAndIcon();
   1.544 +
   1.545 +        if (this._targetFileInfoFetched) {
   1.546 +          // This will also update the download commands if necessary.
   1.547 +          this._targetFileInfoFetched = false;
   1.548 +          this._fetchTargetFileInfo();
   1.549 +        }
   1.550 +      }
   1.551 +    }
   1.552 +  },
   1.553 +
   1.554 +  /* DownloadView */
   1.555 +  onStateChange: function DES_onStateChange(aOldState) {
   1.556 +    let metaData = this.getDownloadMetaData();
   1.557 +    metaData.state = this.dataItem.state;
   1.558 +    if (aOldState != nsIDM.DOWNLOAD_FINISHED && aOldState != metaData.state) {
   1.559 +      // See comment in DVI_onStateChange in downloads.js (the panel-view)
   1.560 +      this._element.setAttribute("image", this._getIcon() + "&state=normal");
   1.561 +      metaData.fileSize = this._dataItem.maxBytes;
   1.562 +      if (this._targetFileInfoFetched) {
   1.563 +        this._targetFileInfoFetched = false;
   1.564 +        this._fetchTargetFileInfo();
   1.565 +      }
   1.566 +    }
   1.567 +
   1.568 +    this._updateDownloadStatusUI();
   1.569 +    if (this._element.selected)
   1.570 +      goUpdateDownloadCommands();
   1.571 +    else
   1.572 +      goUpdateCommand("downloadsCmd_clearDownloads");
   1.573 +  },
   1.574 +
   1.575 +  /* DownloadView */
   1.576 +  onProgressChange: function DES_onProgressChange() {
   1.577 +    this._updateDownloadStatusUI();
   1.578 +  },
   1.579 +
   1.580 +  /* nsIController */
   1.581 +  isCommandEnabled: function DES_isCommandEnabled(aCommand) {
   1.582 +    // The only valid command for inactive elements is cmd_delete.
   1.583 +    if (!this.active && aCommand != "cmd_delete")
   1.584 +      return false;
   1.585 +    switch (aCommand) {
   1.586 +      case "downloadsCmd_open": {
   1.587 +        // We cannot open a session dowload file unless it's done ("openable").
   1.588 +        // If it's finished, we need to make sure the file was not removed,
   1.589 +        // as we do for past downloads.
   1.590 +        if (this._dataItem && !this._dataItem.openable)
   1.591 +          return false;
   1.592 +
   1.593 +        if (this._targetFileInfoFetched)
   1.594 +          return this._targetFileExists;
   1.595 +
   1.596 +        // If the target file information is not yet fetched,
   1.597 +        // temporarily assume that the file is in place.
   1.598 +        return this.getDownloadMetaData().state == nsIDM.DOWNLOAD_FINISHED;
   1.599 +      }
   1.600 +      case "downloadsCmd_show": {
   1.601 +        // TODO: Bug 827010 - Handle part-file asynchronously.
   1.602 +        if (this._dataItem &&
   1.603 +            this._dataItem.partFile && this._dataItem.partFile.exists())
   1.604 +          return true;
   1.605 +
   1.606 +        if (this._targetFileInfoFetched)
   1.607 +          return this._targetFileExists;
   1.608 +
   1.609 +        // If the target file information is not yet fetched,
   1.610 +        // temporarily assume that the file is in place.
   1.611 +        return this.getDownloadMetaData().state == nsIDM.DOWNLOAD_FINISHED;
   1.612 +      }
   1.613 +      case "downloadsCmd_pauseResume":
   1.614 +        return this._dataItem && this._dataItem.inProgress && this._dataItem.resumable;
   1.615 +      case "downloadsCmd_retry":
   1.616 +        // An history download can always be retried.
   1.617 +        return !this._dataItem || this._dataItem.canRetry;
   1.618 +      case "downloadsCmd_openReferrer":
   1.619 +        return this._dataItem && !!this._dataItem.referrer;
   1.620 +      case "cmd_delete":
   1.621 +        // The behavior in this case is somewhat unexpected, so we disallow that.
   1.622 +        if (this._placesNode && this._dataItem && this._dataItem.inProgress)
   1.623 +          return false;
   1.624 +        return true;
   1.625 +      case "downloadsCmd_cancel":
   1.626 +        return this._dataItem != null;
   1.627 +    }
   1.628 +    return false;
   1.629 +  },
   1.630 +
   1.631 +  _retryAsHistoryDownload: function DES__retryAsHistoryDownload() {
   1.632 +    // In future we may try to download into the same original target uri, when
   1.633 +    // we have it.  Though that requires verifying the path is still valid and
   1.634 +    // may surprise the user if he wants to be requested every time.
   1.635 +    let browserWin = RecentWindow.getMostRecentBrowserWindow();
   1.636 +    let initiatingDoc = browserWin ? browserWin.document : document;
   1.637 +    DownloadURL(this.downloadURI, this.getDownloadMetaData().fileName,
   1.638 +                initiatingDoc);
   1.639 +  },
   1.640 +
   1.641 +  /* nsIController */
   1.642 +  doCommand: function DES_doCommand(aCommand) {
   1.643 +    switch (aCommand) {
   1.644 +      case "downloadsCmd_open": {
   1.645 +        let file = this._dataItem ?
   1.646 +          this.dataItem.localFile :
   1.647 +          new FileUtils.File(this.getDownloadMetaData().filePath);
   1.648 +
   1.649 +        DownloadsCommon.openDownloadedFile(file, null, window);
   1.650 +        break;
   1.651 +      }
   1.652 +      case "downloadsCmd_show": {
   1.653 +        if (this._dataItem) {
   1.654 +          this._dataItem.showLocalFile();
   1.655 +        }
   1.656 +        else {
   1.657 +          let file = new FileUtils.File(this.getDownloadMetaData().filePath);
   1.658 +          DownloadsCommon.showDownloadedFile(file);
   1.659 +        }
   1.660 +        break;
   1.661 +      }
   1.662 +      case "downloadsCmd_openReferrer": {
   1.663 +        openURL(this._dataItem.referrer);
   1.664 +        break;
   1.665 +      }
   1.666 +      case "downloadsCmd_cancel": {
   1.667 +        this._dataItem.cancel();
   1.668 +        break;
   1.669 +      }
   1.670 +      case "cmd_delete": {
   1.671 +        if (this._dataItem)
   1.672 +          this._dataItem.remove();
   1.673 +        if (this._placesNode)
   1.674 +          PlacesUtils.bhistory.removePage(this._downloadURIObj);
   1.675 +        break;
   1.676 +       }
   1.677 +      case "downloadsCmd_retry": {
   1.678 +        if (this._dataItem)
   1.679 +          this._dataItem.retry();
   1.680 +        else
   1.681 +          this._retryAsHistoryDownload();
   1.682 +        break;
   1.683 +      }
   1.684 +      case "downloadsCmd_pauseResume": {
   1.685 +        this._dataItem.togglePauseResume();
   1.686 +        break;
   1.687 +      }
   1.688 +    }
   1.689 +  },
   1.690 +
   1.691 +  // Returns whether or not the download handled by this shell should
   1.692 +  // show up in the search results for the given term.  Both the display
   1.693 +  // name for the download and the url are searched.
   1.694 +  matchesSearchTerm: function DES_matchesSearchTerm(aTerm) {
   1.695 +    if (!aTerm)
   1.696 +      return true;
   1.697 +    aTerm = aTerm.toLowerCase();
   1.698 +    return this.getDownloadMetaData().displayName.toLowerCase().contains(aTerm) ||
   1.699 +           this.downloadURI.toLowerCase().contains(aTerm);
   1.700 +  },
   1.701 +
   1.702 +  // Handles return kepress on the element (the keypress listener is
   1.703 +  // set in the DownloadsPlacesView object).
   1.704 +  doDefaultCommand: function DES_doDefaultCommand() {
   1.705 +    function getDefaultCommandForState(aState) {
   1.706 +      switch (aState) {
   1.707 +        case nsIDM.DOWNLOAD_FINISHED:
   1.708 +          return "downloadsCmd_open";
   1.709 +        case nsIDM.DOWNLOAD_PAUSED:
   1.710 +          return "downloadsCmd_pauseResume";
   1.711 +        case nsIDM.DOWNLOAD_NOTSTARTED:
   1.712 +        case nsIDM.DOWNLOAD_QUEUED:
   1.713 +          return "downloadsCmd_cancel";
   1.714 +        case nsIDM.DOWNLOAD_FAILED:
   1.715 +        case nsIDM.DOWNLOAD_CANCELED:
   1.716 +          return "downloadsCmd_retry";
   1.717 +        case nsIDM.DOWNLOAD_SCANNING:
   1.718 +          return "downloadsCmd_show";
   1.719 +        case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
   1.720 +        case nsIDM.DOWNLOAD_DIRTY:
   1.721 +        case nsIDM.DOWNLOAD_BLOCKED_POLICY:
   1.722 +          return "downloadsCmd_openReferrer";
   1.723 +      }
   1.724 +      return "";
   1.725 +    }
   1.726 +    let command = getDefaultCommandForState(this.getDownloadMetaData().state);
   1.727 +    if (command && this.isCommandEnabled(command))
   1.728 +      this.doCommand(command);
   1.729 +  },
   1.730 +
   1.731 +  /**
   1.732 +   * At the first time an item is selected, we don't yet have
   1.733 +   * the target file information.  Thus the call to goUpdateDownloadCommands
   1.734 +   * in DPV_onSelect would result in best-guess enabled/disabled result.
   1.735 +   * That way we let the user perform command immediately. However, once
   1.736 +   * we have the target file information, we can update the commands
   1.737 +   * appropriately (_fetchTargetFileInfo() calls goUpdateDownloadCommands).
   1.738 +   */
   1.739 +  onSelect: function DES_onSelect() {
   1.740 +    if (!this.active)
   1.741 +      return;
   1.742 +    if (!this._targetFileInfoFetched)
   1.743 +      this._fetchTargetFileInfo();
   1.744 +  }
   1.745 +};
   1.746 +
   1.747 +/**
   1.748 + * A Downloads Places View is a places view designed to show a places query
   1.749 + * for history donwloads alongside the current "session"-downloads.
   1.750 + *
   1.751 + * As we don't use the places controller, some methods implemented by other
   1.752 + * places views are not implemented by this view.
   1.753 + *
   1.754 + * A richlistitem in this view can represent either a past download or a session
   1.755 + * download, or both. Session downloads are shown first in the view, and as long
   1.756 + * as they exist they "collapses" their history "counterpart" (So we don't show two
   1.757 + * items for every download).
   1.758 + */
   1.759 +function DownloadsPlacesView(aRichListBox, aActive = true) {
   1.760 +  this._richlistbox = aRichListBox;
   1.761 +  this._richlistbox._placesView = this;
   1.762 +  window.controllers.insertControllerAt(0, this);
   1.763 +
   1.764 +  // Map download URLs to download element shells regardless of their type
   1.765 +  this._downloadElementsShellsForURI = new Map();
   1.766 +
   1.767 +  // Map download data items to their element shells.
   1.768 +  this._viewItemsForDataItems = new WeakMap();
   1.769 +
   1.770 +  // Points to the last session download element. We keep track of this
   1.771 +  // in order to keep all session downloads above past downloads.
   1.772 +  this._lastSessionDownloadElement = null;
   1.773 +
   1.774 +  this._searchTerm = "";
   1.775 +
   1.776 +  this._active = aActive;
   1.777 +
   1.778 +  // Register as a downloads view. The places data will be initialized by
   1.779 +  // the places setter.
   1.780 +  this._initiallySelectedElement = null;
   1.781 +  this._downloadsData = DownloadsCommon.getData(window.opener || window);
   1.782 +  this._downloadsData.addView(this);
   1.783 +
   1.784 +  // Get the Download button out of the attention state since we're about to
   1.785 +  // view all downloads.
   1.786 +  DownloadsCommon.getIndicatorData(window).attention = false;
   1.787 +
   1.788 +  // Make sure to unregister the view if the window is closed.
   1.789 +  window.addEventListener("unload", function() {
   1.790 +    window.controllers.removeController(this);
   1.791 +    this._downloadsData.removeView(this);
   1.792 +    this.result = null;
   1.793 +  }.bind(this), true);
   1.794 +  // Resizing the window may change items visibility.
   1.795 +  window.addEventListener("resize", function() {
   1.796 +    this._ensureVisibleElementsAreActive();
   1.797 +  }.bind(this), true);
   1.798 +}
   1.799 +
   1.800 +DownloadsPlacesView.prototype = {
   1.801 +  get associatedElement() this._richlistbox,
   1.802 +
   1.803 +  get active() this._active,
   1.804 +  set active(val) {
   1.805 +    this._active = val;
   1.806 +    if (this._active)
   1.807 +      this._ensureVisibleElementsAreActive();
   1.808 +    return this._active;
   1.809 +  },
   1.810 +
   1.811 +  _forEachDownloadElementShellForURI:
   1.812 +  function DPV__forEachDownloadElementShellForURI(aURI, aCallback) {
   1.813 +    if (this._downloadElementsShellsForURI.has(aURI)) {
   1.814 +      let downloadElementShells = this._downloadElementsShellsForURI.get(aURI);
   1.815 +      for (let des of downloadElementShells) {
   1.816 +        aCallback(des);
   1.817 +      }
   1.818 +    }
   1.819 +  },
   1.820 +
   1.821 +  _getAnnotationsFor: function DPV_getAnnotationsFor(aURI) {
   1.822 +    if (!this._cachedAnnotations) {
   1.823 +      this._cachedAnnotations = new Map();
   1.824 +      for (let name of [ DESTINATION_FILE_URI_ANNO,
   1.825 +                         DOWNLOAD_META_DATA_ANNO ]) {
   1.826 +        let results = PlacesUtils.annotations.getAnnotationsWithName(name);
   1.827 +        for (let result of results) {
   1.828 +          let url = result.uri.spec;
   1.829 +          if (!this._cachedAnnotations.has(url))
   1.830 +            this._cachedAnnotations.set(url, new Map());
   1.831 +          let m = this._cachedAnnotations.get(url);
   1.832 +          m.set(result.annotationName, result.annotationValue);
   1.833 +        }
   1.834 +      }
   1.835 +    }
   1.836 +
   1.837 +    let annotations = this._cachedAnnotations.get(aURI);
   1.838 +    if (!annotations) {
   1.839 +      // There are no annotations for this entry, that means it is quite old.
   1.840 +      // Make up a fake annotations entry with default values.
   1.841 +      annotations = new Map();
   1.842 +      annotations.set(DESTINATION_FILE_URI_ANNO, NOT_AVAILABLE);
   1.843 +    }
   1.844 +    // The meta-data annotation has been added recently, so it's likely missing.
   1.845 +    if (!annotations.has(DOWNLOAD_META_DATA_ANNO)) {
   1.846 +      annotations.set(DOWNLOAD_META_DATA_ANNO, NOT_AVAILABLE);
   1.847 +    }
   1.848 +    return annotations;
   1.849 +  },
   1.850 +
   1.851 +  /**
   1.852 +   * Given a data item for a session download, or a places node for a past
   1.853 +   * download, updates the view as necessary.
   1.854 +   *  1. If the given data is a places node, we check whether there are any
   1.855 +   *     elements for the same download url. If there are, then we just reset
   1.856 +   *     their places node. Otherwise we add a new download element.
   1.857 +   *  2. If the given data is a data item, we first check if there's a history
   1.858 +   *     download in the list that is not associated with a data item. If we
   1.859 +   *     found one, we use it for the data item as well and reposition it
   1.860 +   *     alongside the other session downloads. If we don't, then we go ahead
   1.861 +   *     and create a new element for the download.
   1.862 +   *
   1.863 +   * @param aDataItem
   1.864 +   *        The data item of a session download. Set to null for history
   1.865 +   *        downloads data.
   1.866 +   * @param [optional] aPlacesNode
   1.867 +   *        The places node for a history download. Required if there's no data
   1.868 +   *        item.
   1.869 +   * @param [optional] aNewest
   1.870 +   *        @see onDataItemAdded. Ignored for history downlods.
   1.871 +   * @param [optional] aDocumentFragment
   1.872 +   *        To speed up the appending of multiple elements to the end of the
   1.873 +   *        list which are coming in a single batch (i.e. invalidateContainer),
   1.874 +   *        a document fragment may be passed to which the new elements would
   1.875 +   *        be appended. It's the caller's job to ensure the fragment is merged
   1.876 +   *        to the richlistbox at the end.
   1.877 +   */
   1.878 +  _addDownloadData:
   1.879 +  function DPV_addDownloadData(aDataItem, aPlacesNode, aNewest = false,
   1.880 +                               aDocumentFragment = null) {
   1.881 +    let downloadURI = aPlacesNode ? aPlacesNode.uri : aDataItem.uri;
   1.882 +    let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI);
   1.883 +    if (!shellsForURI) {
   1.884 +      shellsForURI = new Set();
   1.885 +      this._downloadElementsShellsForURI.set(downloadURI, shellsForURI);
   1.886 +    }
   1.887 +
   1.888 +    let newOrUpdatedShell = null;
   1.889 +
   1.890 +    // Trivial: if there are no shells for this download URI, we always
   1.891 +    // need to create one.
   1.892 +    let shouldCreateShell = shellsForURI.size == 0;
   1.893 +
   1.894 +    // However, if we do have shells for this download uri, there are
   1.895 +    // few options:
   1.896 +    // 1) There's only one shell and it's for a history download (it has
   1.897 +    //    no data item). In this case, we update this shell and move it
   1.898 +    //    if necessary
   1.899 +    // 2) There are multiple shells, indicicating multiple downloads for
   1.900 +    //    the same download uri are running. In this case we create
   1.901 +    //    anoter shell for the download (so we have one shell for each data
   1.902 +    //    item).
   1.903 +    //
   1.904 +    // Note: If a cancelled session download is already in the list, and the
   1.905 +    // download is retired, onDataItemAdded is called again for the same
   1.906 +    // data item. Thus, we also check that we make sure we don't have a view item
   1.907 +    // already.
   1.908 +    if (!shouldCreateShell &&
   1.909 +        aDataItem && this.getViewItem(aDataItem) == null) {
   1.910 +      // If there's a past-download-only shell for this download-uri with no
   1.911 +      // associated data item, use it for the new data item. Otherwise, go ahead
   1.912 +      // and create another shell.
   1.913 +      shouldCreateShell = true;
   1.914 +      for (let shell of shellsForURI) {
   1.915 +        if (!shell.dataItem) {
   1.916 +          shouldCreateShell = false;
   1.917 +          shell.dataItem = aDataItem;
   1.918 +          newOrUpdatedShell = shell;
   1.919 +          this._viewItemsForDataItems.set(aDataItem, shell);
   1.920 +          break;
   1.921 +        }
   1.922 +      }
   1.923 +    }
   1.924 +
   1.925 +    if (shouldCreateShell) {
   1.926 +      // Bug 836271: The annotations for a url should be cached only when the
   1.927 +      // places node is available, i.e. when we know we we'd be notified for
   1.928 +      // annoation changes. 
   1.929 +      // Otherwise we may cache NOT_AVILABLE values first for a given session
   1.930 +      // download, and later use these NOT_AVILABLE values when a history
   1.931 +      // download for the same URL is added.
   1.932 +      let cachedAnnotations = aPlacesNode ? this._getAnnotationsFor(downloadURI) : null;
   1.933 +      let shell = new DownloadElementShell(aDataItem, aPlacesNode, cachedAnnotations);
   1.934 +      newOrUpdatedShell = shell;
   1.935 +      shellsForURI.add(shell);
   1.936 +      if (aDataItem)
   1.937 +        this._viewItemsForDataItems.set(aDataItem, shell);
   1.938 +    }
   1.939 +    else if (aPlacesNode) {
   1.940 +      for (let shell of shellsForURI) {
   1.941 +        if (shell.placesNode != aPlacesNode)
   1.942 +          shell.placesNode = aPlacesNode;
   1.943 +      }
   1.944 +    }
   1.945 +
   1.946 +    if (newOrUpdatedShell) {
   1.947 +      if (aNewest) {
   1.948 +        this._richlistbox.insertBefore(newOrUpdatedShell.element,
   1.949 +                                       this._richlistbox.firstChild);
   1.950 +        if (!this._lastSessionDownloadElement) {
   1.951 +          this._lastSessionDownloadElement = newOrUpdatedShell.element;
   1.952 +        }
   1.953 +        // Some operations like retrying an history download move an element to
   1.954 +        // the top of the richlistbox, along with other session downloads.
   1.955 +        // More generally, if a new download is added, should be made visible.
   1.956 +        this._richlistbox.ensureElementIsVisible(newOrUpdatedShell.element);
   1.957 +      }
   1.958 +      else if (aDataItem) {
   1.959 +        let before = this._lastSessionDownloadElement ?
   1.960 +          this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild;
   1.961 +        this._richlistbox.insertBefore(newOrUpdatedShell.element, before);
   1.962 +        this._lastSessionDownloadElement = newOrUpdatedShell.element;
   1.963 +      }
   1.964 +      else {
   1.965 +        let appendTo = aDocumentFragment || this._richlistbox;
   1.966 +        appendTo.appendChild(newOrUpdatedShell.element);
   1.967 +      }
   1.968 +
   1.969 +      if (this.searchTerm) {
   1.970 +        newOrUpdatedShell.element.hidden =
   1.971 +          !newOrUpdatedShell.element._shell.matchesSearchTerm(this.searchTerm);
   1.972 +      }
   1.973 +    }
   1.974 +
   1.975 +    // If aDocumentFragment is defined this is a batch change, so it's up to
   1.976 +    // the caller to append the fragment and activate the visible shells.
   1.977 +    if (!aDocumentFragment) {
   1.978 +      this._ensureVisibleElementsAreActive();
   1.979 +      goUpdateCommand("downloadsCmd_clearDownloads");
   1.980 +    }
   1.981 +  },
   1.982 +
   1.983 +  _removeElement: function DPV__removeElement(aElement) {
   1.984 +    // If the element was selected exclusively, select its next
   1.985 +    // sibling first, if not, try for previous sibling, if any.
   1.986 +    if ((aElement.nextSibling || aElement.previousSibling) &&
   1.987 +        this._richlistbox.selectedItems &&
   1.988 +        this._richlistbox.selectedItems.length == 1 &&
   1.989 +        this._richlistbox.selectedItems[0] == aElement) {
   1.990 +      this._richlistbox.selectItem(aElement.nextSibling ||
   1.991 +                                   aElement.previousSibling);
   1.992 +    }
   1.993 +
   1.994 +    if (this._lastSessionDownloadElement == aElement)
   1.995 +      this._lastSessionDownloadElement = aElement.previousSibling;
   1.996 +
   1.997 +    this._richlistbox.removeItemFromSelection(aElement);
   1.998 +    this._richlistbox.removeChild(aElement);
   1.999 +    this._ensureVisibleElementsAreActive();
  1.1000 +    goUpdateCommand("downloadsCmd_clearDownloads");
  1.1001 +  },
  1.1002 +
  1.1003 +  _removeHistoryDownloadFromView:
  1.1004 +  function DPV__removeHistoryDownloadFromView(aPlacesNode) {
  1.1005 +    let downloadURI = aPlacesNode.uri;
  1.1006 +    let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI);
  1.1007 +    if (shellsForURI) {
  1.1008 +      for (let shell of shellsForURI) {
  1.1009 +        if (shell.dataItem) {
  1.1010 +          shell.placesNode = null;
  1.1011 +        }
  1.1012 +        else {
  1.1013 +          this._removeElement(shell.element);
  1.1014 +          shellsForURI.delete(shell);
  1.1015 +          if (shellsForURI.size == 0)
  1.1016 +            this._downloadElementsShellsForURI.delete(downloadURI);
  1.1017 +        }
  1.1018 +      }
  1.1019 +    }
  1.1020 +  },
  1.1021 +
  1.1022 +  _removeSessionDownloadFromView:
  1.1023 +  function DPV__removeSessionDownloadFromView(aDataItem) {
  1.1024 +    let shells = this._downloadElementsShellsForURI.get(aDataItem.uri);
  1.1025 +    if (shells.size == 0)
  1.1026 +      throw new Error("Should have had at leaat one shell for this uri");
  1.1027 +
  1.1028 +    let shell = this.getViewItem(aDataItem);
  1.1029 +    if (!shells.has(shell))
  1.1030 +      throw new Error("Missing download element shell in shells list for url");
  1.1031 +
  1.1032 +    // If there's more than one item for this download uri, we can let the
  1.1033 +    // view item for this this particular data item go away.
  1.1034 +    // If there's only one item for this download uri, we should only
  1.1035 +    // keep it if it is associated with a history download.
  1.1036 +    if (shells.size > 1 || !shell.placesNode) {
  1.1037 +      this._removeElement(shell.element);
  1.1038 +      shells.delete(shell);
  1.1039 +      if (shells.size == 0)
  1.1040 +        this._downloadElementsShellsForURI.delete(aDataItem.uri);
  1.1041 +    }
  1.1042 +    else {
  1.1043 +      shell.dataItem = null;
  1.1044 +      // Move it below the session-download items;
  1.1045 +      if (this._lastSessionDownloadElement == shell.element) {
  1.1046 +        this._lastSessionDownloadElement = shell.element.previousSibling;
  1.1047 +      }
  1.1048 +      else {
  1.1049 +        let before = this._lastSessionDownloadElement ?
  1.1050 +          this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild;
  1.1051 +        this._richlistbox.insertBefore(shell.element, before);
  1.1052 +      }
  1.1053 +    }
  1.1054 +  },
  1.1055 +
  1.1056 +  _ensureVisibleElementsAreActive:
  1.1057 +  function DPV__ensureVisibleElementsAreActive() {
  1.1058 +    if (!this.active || this._ensureVisibleTimer || !this._richlistbox.firstChild)
  1.1059 +      return;
  1.1060 +
  1.1061 +    this._ensureVisibleTimer = setTimeout(function() {
  1.1062 +      delete this._ensureVisibleTimer;
  1.1063 +      if (!this._richlistbox.firstChild)
  1.1064 +        return;
  1.1065 +
  1.1066 +      let rlbRect = this._richlistbox.getBoundingClientRect();
  1.1067 +      let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
  1.1068 +                           .getInterface(Ci.nsIDOMWindowUtils);
  1.1069 +      let nodes = winUtils.nodesFromRect(rlbRect.left, rlbRect.top,
  1.1070 +                                         0, rlbRect.width, rlbRect.height, 0,
  1.1071 +                                         true, false);
  1.1072 +      // nodesFromRect returns nodes in z-index order, and for the same z-index
  1.1073 +      // sorts them in inverted DOM order, thus starting from the one that would
  1.1074 +      // be on top.
  1.1075 +      let firstVisibleNode, lastVisibleNode;
  1.1076 +      for (let node of nodes) {
  1.1077 +        if (node.localName === "richlistitem" && node._shell) {
  1.1078 +          node._shell.ensureActive();
  1.1079 +          // The first visible node is the last match.
  1.1080 +          firstVisibleNode = node;
  1.1081 +          // While the last visible node is the first match.
  1.1082 +          if (!lastVisibleNode)
  1.1083 +            lastVisibleNode = node;
  1.1084 +        }
  1.1085 +      }
  1.1086 +
  1.1087 +      // Also activate the first invisible nodes in both boundaries (that is,
  1.1088 +      // above and below the visible area) to ensure proper keyboard navigation
  1.1089 +      // in both directions.
  1.1090 +      let nodeBelowVisibleArea = lastVisibleNode && lastVisibleNode.nextSibling;
  1.1091 +      if (nodeBelowVisibleArea && nodeBelowVisibleArea._shell)
  1.1092 +        nodeBelowVisibleArea._shell.ensureActive();
  1.1093 +
  1.1094 +      let nodeABoveVisibleArea =
  1.1095 +        firstVisibleNode && firstVisibleNode.previousSibling;
  1.1096 +      if (nodeABoveVisibleArea && nodeABoveVisibleArea._shell)
  1.1097 +        nodeABoveVisibleArea._shell.ensureActive();
  1.1098 +    }.bind(this), 10);
  1.1099 +  },
  1.1100 +
  1.1101 +  _place: "",
  1.1102 +  get place() this._place,
  1.1103 +  set place(val) {
  1.1104 +    // Don't reload everything if we don't have to.
  1.1105 +    if (this._place == val) {
  1.1106 +      // XXXmano: places.js relies on this behavior (see Bug 822203).
  1.1107 +      this.searchTerm = "";
  1.1108 +      return val;
  1.1109 +    }
  1.1110 +
  1.1111 +    this._place = val;
  1.1112 +
  1.1113 +    let history = PlacesUtils.history;
  1.1114 +    let queries = { }, options = { };
  1.1115 +    history.queryStringToQueries(val, queries, { }, options);
  1.1116 +    if (!queries.value.length)
  1.1117 +      queries.value = [history.getNewQuery()];
  1.1118 +
  1.1119 +    let result = history.executeQueries(queries.value, queries.value.length,
  1.1120 +                                        options.value);
  1.1121 +    result.addObserver(this, false);
  1.1122 +    return val;
  1.1123 +  },
  1.1124 +
  1.1125 +  _result: null,
  1.1126 +  get result() this._result,
  1.1127 +  set result(val) {
  1.1128 +    if (this._result == val)
  1.1129 +      return val;
  1.1130 +
  1.1131 +    if (this._result) {
  1.1132 +      this._result.removeObserver(this);
  1.1133 +      this._resultNode.containerOpen = false;
  1.1134 +    }
  1.1135 +
  1.1136 +    if (val) {
  1.1137 +      this._result = val;
  1.1138 +      this._resultNode = val.root;
  1.1139 +      this._resultNode.containerOpen = true;
  1.1140 +      this._ensureInitialSelection();
  1.1141 +    }
  1.1142 +    else {
  1.1143 +      delete this._resultNode;
  1.1144 +      delete this._result;
  1.1145 +    }
  1.1146 +
  1.1147 +    return val;
  1.1148 +  },
  1.1149 +
  1.1150 +  get selectedNodes() {
  1.1151 +    let placesNodes = [];
  1.1152 +    let selectedElements = this._richlistbox.selectedItems;
  1.1153 +    for (let elt of selectedElements) {
  1.1154 +      if (elt._shell.placesNode)
  1.1155 +        placesNodes.push(elt._shell.placesNode);
  1.1156 +    }
  1.1157 +    return placesNodes;
  1.1158 +  },
  1.1159 +
  1.1160 +  get selectedNode() {
  1.1161 +    let selectedNodes = this.selectedNodes;
  1.1162 +    return selectedNodes.length == 1 ? selectedNodes[0] : null;
  1.1163 +  },
  1.1164 +
  1.1165 +  get hasSelection() this.selectedNodes.length > 0,
  1.1166 +
  1.1167 +  containerStateChanged:
  1.1168 +  function DPV_containerStateChanged(aNode, aOldState, aNewState) {
  1.1169 +    this.invalidateContainer(aNode)
  1.1170 +  },
  1.1171 +
  1.1172 +  invalidateContainer:
  1.1173 +  function DPV_invalidateContainer(aContainer) {
  1.1174 +    if (aContainer != this._resultNode)
  1.1175 +      throw new Error("Unexpected container node");
  1.1176 +    if (!aContainer.containerOpen)
  1.1177 +      throw new Error("Root container for the downloads query cannot be closed");
  1.1178 +
  1.1179 +    let suppressOnSelect = this._richlistbox.suppressOnSelect;
  1.1180 +    this._richlistbox.suppressOnSelect = true;
  1.1181 +    try {
  1.1182 +      // Remove the invalidated history downloads from the list and unset the
  1.1183 +      // places node for data downloads.
  1.1184 +      // Loop backwards since _removeHistoryDownloadFromView may removeChild().
  1.1185 +      for (let i = this._richlistbox.childNodes.length - 1; i >= 0; --i) {
  1.1186 +        let element = this._richlistbox.childNodes[i];
  1.1187 +        if (element._shell.placesNode)
  1.1188 +          this._removeHistoryDownloadFromView(element._shell.placesNode);
  1.1189 +      }
  1.1190 +    }
  1.1191 +    finally {
  1.1192 +      this._richlistbox.suppressOnSelect = suppressOnSelect;
  1.1193 +    }
  1.1194 +
  1.1195 +    if (aContainer.childCount > 0) {
  1.1196 +      let elementsToAppendFragment = document.createDocumentFragment();
  1.1197 +      for (let i = 0; i < aContainer.childCount; i++) {
  1.1198 +        try {
  1.1199 +          this._addDownloadData(null, aContainer.getChild(i), false,
  1.1200 +                                elementsToAppendFragment);
  1.1201 +        }
  1.1202 +        catch(ex) {
  1.1203 +          Cu.reportError(ex);
  1.1204 +        }
  1.1205 +      }
  1.1206 +
  1.1207 +      // _addDownloadData may not add new elements if there were already
  1.1208 +      // data items in place.
  1.1209 +      if (elementsToAppendFragment.firstChild) {
  1.1210 +        this._appendDownloadsFragment(elementsToAppendFragment);
  1.1211 +        this._ensureVisibleElementsAreActive();
  1.1212 +      }
  1.1213 +    }
  1.1214 +
  1.1215 +    goUpdateDownloadCommands();
  1.1216 +  },
  1.1217 +
  1.1218 +  _appendDownloadsFragment: function DPV__appendDownloadsFragment(aDOMFragment) {
  1.1219 +    // Workaround multiple reflows hang by removing the richlistbox
  1.1220 +    // and adding it back when we're done.
  1.1221 +
  1.1222 +    // Hack for bug 836283: reset xbl fields to their old values after the
  1.1223 +    // binding is reattached to avoid breaking the selection state
  1.1224 +    let xblFields = new Map();
  1.1225 +    for (let [key, value] in Iterator(this._richlistbox)) {
  1.1226 +      xblFields.set(key, value);
  1.1227 +    }
  1.1228 +
  1.1229 +    let parentNode = this._richlistbox.parentNode;
  1.1230 +    let nextSibling = this._richlistbox.nextSibling;
  1.1231 +    parentNode.removeChild(this._richlistbox);
  1.1232 +    this._richlistbox.appendChild(aDOMFragment);
  1.1233 +    parentNode.insertBefore(this._richlistbox, nextSibling);
  1.1234 +
  1.1235 +    for (let [key, value] of xblFields) {
  1.1236 +      this._richlistbox[key] = value;
  1.1237 +    }
  1.1238 +  },
  1.1239 +
  1.1240 +  nodeInserted: function DPV_nodeInserted(aParent, aPlacesNode) {
  1.1241 +    this._addDownloadData(null, aPlacesNode);
  1.1242 +  },
  1.1243 +
  1.1244 +  nodeRemoved: function DPV_nodeRemoved(aParent, aPlacesNode, aOldIndex) {
  1.1245 +    this._removeHistoryDownloadFromView(aPlacesNode);
  1.1246 +  },
  1.1247 +
  1.1248 +  nodeIconChanged: function DPV_nodeIconChanged(aNode) {
  1.1249 +    this._forEachDownloadElementShellForURI(aNode.uri, function(aDownloadElementShell) {
  1.1250 +      aDownloadElementShell.placesNodeIconChanged();
  1.1251 +    });
  1.1252 +  },
  1.1253 +
  1.1254 +  nodeAnnotationChanged: function DPV_nodeAnnotationChanged(aNode, aAnnoName) {
  1.1255 +    this._forEachDownloadElementShellForURI(aNode.uri, function(aDownloadElementShell) {
  1.1256 +      aDownloadElementShell.placesNodeAnnotationChanged(aAnnoName);
  1.1257 +    });
  1.1258 +  },
  1.1259 +
  1.1260 +  nodeTitleChanged: function DPV_nodeTitleChanged(aNode, aNewTitle) {
  1.1261 +    this._forEachDownloadElementShellForURI(aNode.uri, function(aDownloadElementShell) {
  1.1262 +      aDownloadElementShell.placesNodeTitleChanged();
  1.1263 +    });
  1.1264 +  },
  1.1265 +
  1.1266 +  nodeKeywordChanged: function() {},
  1.1267 +  nodeDateAddedChanged: function() {},
  1.1268 +  nodeLastModifiedChanged: function() {},
  1.1269 +  nodeHistoryDetailsChanged: function() {},
  1.1270 +  nodeTagsChanged: function() {},
  1.1271 +  sortingChanged: function() {},
  1.1272 +  nodeMoved: function() {},
  1.1273 +  nodeURIChanged: function() {},
  1.1274 +  batching: function() {},
  1.1275 +
  1.1276 +  get controller() this._richlistbox.controller,
  1.1277 +
  1.1278 +  get searchTerm() this._searchTerm,
  1.1279 +  set searchTerm(aValue) {
  1.1280 +    if (this._searchTerm != aValue) {
  1.1281 +      for (let element of this._richlistbox.childNodes) {
  1.1282 +        element.hidden = !element._shell.matchesSearchTerm(aValue);
  1.1283 +      }
  1.1284 +      this._ensureVisibleElementsAreActive();
  1.1285 +    }
  1.1286 +    return this._searchTerm = aValue;
  1.1287 +  },
  1.1288 +
  1.1289 +  /**
  1.1290 +   * When the view loads, we want to select the first item.
  1.1291 +   * However, because session downloads, for which the data is loaded
  1.1292 +   * asynchronously, always come first in the list, and because the list
  1.1293 +   * may (or may not) already contain history downloads at that point, it
  1.1294 +   * turns out that by the time we can select the first item, the user may
  1.1295 +   * have already started using the view.
  1.1296 +   * To make things even more complicated, in other cases, the places data
  1.1297 +   * may be loaded after the session downloads data.  Thus we cannot rely on
  1.1298 +   * the order in which the data comes in.
  1.1299 +   * We work around this by attempting to select the first element twice,
  1.1300 +   * once after the places data is loaded and once when the session downloads
  1.1301 +   * data is done loading.  However, if the selection has changed in-between,
  1.1302 +   * we assume the user has already started using the view and give up.
  1.1303 +   */
  1.1304 +  _ensureInitialSelection: function DPV__ensureInitialSelection() {
  1.1305 +    // Either they're both null, or the selection has not changed in between.
  1.1306 +    if (this._richlistbox.selectedItem == this._initiallySelectedElement) {
  1.1307 +      let firstDownloadElement = this._richlistbox.firstChild;
  1.1308 +      if (firstDownloadElement != this._initiallySelectedElement) {
  1.1309 +        // We may be called before _ensureVisibleElementsAreActive,
  1.1310 +        // or before the download binding is attached. Therefore, ensure the
  1.1311 +        // first item is activated, and pass the item to the richlistbox
  1.1312 +        // setters only at a point we know for sure the binding is attached.
  1.1313 +        firstDownloadElement._shell.ensureActive();
  1.1314 +        Services.tm.mainThread.dispatch(function() {
  1.1315 +          this._richlistbox.selectedItem = firstDownloadElement;
  1.1316 +          this._richlistbox.currentItem = firstDownloadElement;
  1.1317 +          this._initiallySelectedElement = firstDownloadElement;
  1.1318 +        }.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
  1.1319 +      }
  1.1320 +    }
  1.1321 +  },
  1.1322 +
  1.1323 +  onDataLoadStarting: function() { },
  1.1324 +  onDataLoadCompleted: function DPV_onDataLoadCompleted() {
  1.1325 +    this._ensureInitialSelection();
  1.1326 +  },
  1.1327 +
  1.1328 +  onDataItemAdded: function DPV_onDataItemAdded(aDataItem, aNewest) {
  1.1329 +    this._addDownloadData(aDataItem, null, aNewest);
  1.1330 +  },
  1.1331 +
  1.1332 +  onDataItemRemoved: function DPV_onDataItemRemoved(aDataItem) {
  1.1333 +    this._removeSessionDownloadFromView(aDataItem);
  1.1334 +  },
  1.1335 +
  1.1336 +  getViewItem: function(aDataItem)
  1.1337 +    this._viewItemsForDataItems.get(aDataItem, null),
  1.1338 +
  1.1339 +  supportsCommand: function DPV_supportsCommand(aCommand) {
  1.1340 +    if (DOWNLOAD_VIEW_SUPPORTED_COMMANDS.indexOf(aCommand) != -1) {
  1.1341 +      // The clear-downloads command may be performed by the toolbar-button,
  1.1342 +      // which can be focused on OS X.  Thus enable this command even if the
  1.1343 +      // richlistbox is not focused.
  1.1344 +      // For other commands, be prudent and disable them unless the richlistview
  1.1345 +      // is focused. It's important to make the decision here rather than in
  1.1346 +      // isCommandEnabled.  Otherwise our controller may "steal" commands from
  1.1347 +      // other controls in the window (see goUpdateCommand &
  1.1348 +      // getControllerForCommand).
  1.1349 +      if (document.activeElement == this._richlistbox ||
  1.1350 +          aCommand == "downloadsCmd_clearDownloads") {
  1.1351 +        return true;
  1.1352 +      }
  1.1353 +    }
  1.1354 +    return false;
  1.1355 +  },
  1.1356 +
  1.1357 +  isCommandEnabled: function DPV_isCommandEnabled(aCommand) {
  1.1358 +    switch (aCommand) {
  1.1359 +      case "cmd_copy":
  1.1360 +        return this._richlistbox.selectedItems.length > 0;
  1.1361 +      case "cmd_selectAll":
  1.1362 +        return true;
  1.1363 +      case "cmd_paste":
  1.1364 +        return this._canDownloadClipboardURL();
  1.1365 +      case "downloadsCmd_clearDownloads":
  1.1366 +        return this._canClearDownloads();
  1.1367 +      default:
  1.1368 +        return Array.every(this._richlistbox.selectedItems, function(element) {
  1.1369 +          return element._shell.isCommandEnabled(aCommand);
  1.1370 +        });
  1.1371 +    }
  1.1372 +  },
  1.1373 +
  1.1374 +  _canClearDownloads: function DPV__canClearDownloads() {
  1.1375 +    // Downloads can be cleared if there's at least one removeable download in
  1.1376 +    // the list (either a history download or a completed session download).
  1.1377 +    // Because history downloads are always removable and are listed after the
  1.1378 +    // session downloads, check from bottom to top.
  1.1379 +    for (let elt = this._richlistbox.lastChild; elt; elt = elt.previousSibling) {
  1.1380 +      if (elt._shell.placesNode || !elt._shell.dataItem.inProgress)
  1.1381 +        return true;
  1.1382 +    }
  1.1383 +    return false;
  1.1384 +  },
  1.1385 +
  1.1386 +  _copySelectedDownloadsToClipboard:
  1.1387 +  function DPV__copySelectedDownloadsToClipboard() {
  1.1388 +    let selectedElements = this._richlistbox.selectedItems;
  1.1389 +    let urls = [e._shell.downloadURI for each (e in selectedElements)];
  1.1390 +
  1.1391 +    Cc["@mozilla.org/widget/clipboardhelper;1"].
  1.1392 +    getService(Ci.nsIClipboardHelper).copyString(urls.join("\n"), document);
  1.1393 +  },
  1.1394 +
  1.1395 +  _getURLFromClipboardData: function DPV__getURLFromClipboardData() {
  1.1396 +    let trans = Cc["@mozilla.org/widget/transferable;1"].
  1.1397 +                createInstance(Ci.nsITransferable);
  1.1398 +    trans.init(null);
  1.1399 +
  1.1400 +    let flavors = ["text/x-moz-url", "text/unicode"];
  1.1401 +    flavors.forEach(trans.addDataFlavor);
  1.1402 +
  1.1403 +    Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard);
  1.1404 +
  1.1405 +    // Getting the data or creating the nsIURI might fail.
  1.1406 +    try {
  1.1407 +      let data = {};
  1.1408 +      trans.getAnyTransferData({}, data, {});
  1.1409 +      let [url, name] = data.value.QueryInterface(Ci.nsISupportsString)
  1.1410 +                            .data.split("\n");
  1.1411 +      if (url)
  1.1412 +        return [NetUtil.newURI(url, null, null).spec, name];
  1.1413 +    }
  1.1414 +    catch(ex) { }
  1.1415 +
  1.1416 +    return ["", ""];
  1.1417 +  },
  1.1418 +
  1.1419 +  _canDownloadClipboardURL: function DPV__canDownloadClipboardURL() {
  1.1420 +    let [url, name] = this._getURLFromClipboardData();
  1.1421 +    return url != "";
  1.1422 +  },
  1.1423 +
  1.1424 +  _downloadURLFromClipboard: function DPV__downloadURLFromClipboard() {
  1.1425 +    let [url, name] = this._getURLFromClipboardData();
  1.1426 +    let browserWin = RecentWindow.getMostRecentBrowserWindow();
  1.1427 +    let initiatingDoc = browserWin ? browserWin.document : document;
  1.1428 +    DownloadURL(url, name, initiatingDoc);
  1.1429 +  },
  1.1430 +
  1.1431 +  doCommand: function DPV_doCommand(aCommand) {
  1.1432 +    switch (aCommand) {
  1.1433 +      case "cmd_copy":
  1.1434 +        this._copySelectedDownloadsToClipboard();
  1.1435 +        break;
  1.1436 +      case "cmd_selectAll":
  1.1437 +        this._richlistbox.selectAll();
  1.1438 +        break;
  1.1439 +      case "cmd_paste":
  1.1440 +        this._downloadURLFromClipboard();
  1.1441 +        break;
  1.1442 +      case "downloadsCmd_clearDownloads":
  1.1443 +        this._downloadsData.removeFinished();
  1.1444 +        if (this.result) {
  1.1445 +          Cc["@mozilla.org/browser/download-history;1"]
  1.1446 +            .getService(Ci.nsIDownloadHistory)
  1.1447 +            .removeAllDownloads();
  1.1448 +        }
  1.1449 +        // There may be no selection or focus change as a result
  1.1450 +        // of these change, and we want the command updated immediately.
  1.1451 +        goUpdateCommand("downloadsCmd_clearDownloads");
  1.1452 +        break;
  1.1453 +      default: {
  1.1454 +        // Slicing the array to get a freezed list of selected items. Otherwise,
  1.1455 +        // the selectedItems array is live and doCommand may alter the selection
  1.1456 +        // while we are trying to do one particular action, like removing items
  1.1457 +        // from history.
  1.1458 +        let selectedElements = this._richlistbox.selectedItems.slice();
  1.1459 +        for (let element of selectedElements) {
  1.1460 +          element._shell.doCommand(aCommand);
  1.1461 +        }
  1.1462 +      }
  1.1463 +    }
  1.1464 +  },
  1.1465 +
  1.1466 +  onEvent: function() { },
  1.1467 +
  1.1468 +  onContextMenu: function DPV_onContextMenu(aEvent)
  1.1469 +  {
  1.1470 +    let element = this._richlistbox.selectedItem;
  1.1471 +    if (!element || !element._shell)
  1.1472 +      return false;
  1.1473 +
  1.1474 +    // Set the state attribute so that only the appropriate items are displayed.
  1.1475 +    let contextMenu = document.getElementById("downloadsContextMenu");
  1.1476 +    let state = element._shell.getDownloadMetaData().state;
  1.1477 +    if (state !== undefined)
  1.1478 +      contextMenu.setAttribute("state", state);
  1.1479 +    else
  1.1480 +      contextMenu.removeAttribute("state");
  1.1481 +
  1.1482 +    if (state == nsIDM.DOWNLOAD_DOWNLOADING) {
  1.1483 +      // The resumable property of a download may change at any time, so
  1.1484 +      // ensure we update the related command now.
  1.1485 +      goUpdateCommand("downloadsCmd_pauseResume");
  1.1486 +    }
  1.1487 +    return true;
  1.1488 +  },
  1.1489 +
  1.1490 +  onKeyPress: function DPV_onKeyPress(aEvent) {
  1.1491 +    let selectedElements = this._richlistbox.selectedItems;
  1.1492 +    if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
  1.1493 +      // In the content tree, opening bookmarks by pressing return is only
  1.1494 +      // supported when a single item is selected. To be consistent, do the
  1.1495 +      // same here.
  1.1496 +      if (selectedElements.length == 1) {
  1.1497 +        let element = selectedElements[0];
  1.1498 +        if (element._shell)
  1.1499 +          element._shell.doDefaultCommand();
  1.1500 +      }
  1.1501 +    }
  1.1502 +    else if (aEvent.charCode == " ".charCodeAt(0)) {
  1.1503 +      // Pausue/Resume every selected download
  1.1504 +      for (let element of selectedElements) {
  1.1505 +        if (element._shell.isCommandEnabled("downloadsCmd_pauseResume"))
  1.1506 +          element._shell.doCommand("downloadsCmd_pauseResume");
  1.1507 +      }
  1.1508 +    }
  1.1509 +  },
  1.1510 +
  1.1511 +  onDoubleClick: function DPV_onDoubleClick(aEvent) {
  1.1512 +    if (aEvent.button != 0)
  1.1513 +      return;
  1.1514 +
  1.1515 +    let selectedElements = this._richlistbox.selectedItems;
  1.1516 +    if (selectedElements.length != 1)
  1.1517 +      return;
  1.1518 +
  1.1519 +    let element = selectedElements[0];
  1.1520 +    if (element._shell)
  1.1521 +      element._shell.doDefaultCommand();
  1.1522 +  },
  1.1523 +
  1.1524 +  onScroll: function DPV_onScroll() {
  1.1525 +    this._ensureVisibleElementsAreActive();
  1.1526 +  },
  1.1527 +
  1.1528 +  onSelect: function DPV_onSelect() {
  1.1529 +    goUpdateDownloadCommands();
  1.1530 +
  1.1531 +    let selectedElements = this._richlistbox.selectedItems;
  1.1532 +    for (let elt of selectedElements) {
  1.1533 +      if (elt._shell)
  1.1534 +        elt._shell.onSelect();
  1.1535 +    }
  1.1536 +  },
  1.1537 +
  1.1538 +  onDragStart: function DPV_onDragStart(aEvent) {
  1.1539 +    // TODO Bug 831358: Support d&d for multiple selection.
  1.1540 +    // For now, we just drag the first element.
  1.1541 +    let selectedItem = this._richlistbox.selectedItem;
  1.1542 +    if (!selectedItem)
  1.1543 +      return;
  1.1544 +
  1.1545 +    let metaData = selectedItem._shell.getDownloadMetaData();
  1.1546 +    if (!("filePath" in metaData))
  1.1547 +      return;
  1.1548 +    let file = new FileUtils.File(metaData.filePath);
  1.1549 +    if (!file.exists())
  1.1550 +      return;
  1.1551 +
  1.1552 +    let dt = aEvent.dataTransfer;
  1.1553 +    dt.mozSetDataAt("application/x-moz-file", file, 0);
  1.1554 +    let url = Services.io.newFileURI(file).spec;
  1.1555 +    dt.setData("text/uri-list", url);
  1.1556 +    dt.setData("text/plain", url);
  1.1557 +    dt.effectAllowed = "copyMove";
  1.1558 +    dt.addElement(selectedItem);
  1.1559 +  },
  1.1560 +
  1.1561 +  onDragOver: function DPV_onDragOver(aEvent) {
  1.1562 +    let types = aEvent.dataTransfer.types;
  1.1563 +    if (types.contains("text/uri-list") ||
  1.1564 +        types.contains("text/x-moz-url") ||
  1.1565 +        types.contains("text/plain")) {
  1.1566 +      aEvent.preventDefault();
  1.1567 +    }
  1.1568 +  },
  1.1569 +
  1.1570 +  onDrop: function DPV_onDrop(aEvent) {
  1.1571 +    let dt = aEvent.dataTransfer;
  1.1572 +    // If dragged item is from our source, do not try to
  1.1573 +    // redownload already downloaded file.
  1.1574 +    if (dt.mozGetDataAt("application/x-moz-file", 0))
  1.1575 +      return;
  1.1576 +
  1.1577 +    let name = { };
  1.1578 +    let url = Services.droppedLinkHandler.dropLink(aEvent, name);
  1.1579 +    if (url) {
  1.1580 +      let browserWin = RecentWindow.getMostRecentBrowserWindow();
  1.1581 +      let initiatingDoc = browserWin ? browserWin.document : document;
  1.1582 +      DownloadURL(url, name.value, initiatingDoc);
  1.1583 +    }
  1.1584 +  }
  1.1585 +};
  1.1586 +
  1.1587 +for (let methodName of ["load", "applyFilter", "selectNode", "selectItems"]) {
  1.1588 +  DownloadsPlacesView.prototype[methodName] = function() {
  1.1589 +    throw new Error("|" + methodName + "| is not implemented by the downloads view.");
  1.1590 +  }
  1.1591 +}
  1.1592 +
  1.1593 +function goUpdateDownloadCommands() {
  1.1594 +  for (let command of DOWNLOAD_VIEW_SUPPORTED_COMMANDS) {
  1.1595 +    goUpdateCommand(command);
  1.1596 +  }
  1.1597 +}

mercurial