browser/components/downloads/content/allDownloadsViewOverlay.js

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 /**
michael@0 6 * THE PLACES VIEW IMPLEMENTED IN THIS FILE HAS A VERY PARTICULAR USE CASE.
michael@0 7 * IT IS HIGHLY RECOMMENDED NOT TO EXTEND IT FOR ANY OTHER USE CASES OR RELY
michael@0 8 * ON IT AS AN API.
michael@0 9 */
michael@0 10
michael@0 11 let Cu = Components.utils;
michael@0 12 let Ci = Components.interfaces;
michael@0 13 let Cc = Components.classes;
michael@0 14
michael@0 15 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 16 Cu.import("resource://gre/modules/Services.jsm");
michael@0 17 Cu.import("resource://gre/modules/NetUtil.jsm");
michael@0 18 Cu.import("resource://gre/modules/DownloadUtils.jsm");
michael@0 19 Cu.import("resource:///modules/DownloadsCommon.jsm");
michael@0 20 Cu.import("resource://gre/modules/PlacesUtils.jsm");
michael@0 21 Cu.import("resource://gre/modules/osfile.jsm");
michael@0 22
michael@0 23 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
michael@0 24 "resource://gre/modules/PrivateBrowsingUtils.jsm");
michael@0 25 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
michael@0 26 "resource:///modules/RecentWindow.jsm");
michael@0 27 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
michael@0 28 "resource://gre/modules/FileUtils.jsm");
michael@0 29
michael@0 30 const nsIDM = Ci.nsIDownloadManager;
michael@0 31
michael@0 32 const DESTINATION_FILE_URI_ANNO = "downloads/destinationFileURI";
michael@0 33 const DOWNLOAD_META_DATA_ANNO = "downloads/metaData";
michael@0 34
michael@0 35 const DOWNLOAD_VIEW_SUPPORTED_COMMANDS =
michael@0 36 ["cmd_delete", "cmd_copy", "cmd_paste", "cmd_selectAll",
michael@0 37 "downloadsCmd_pauseResume", "downloadsCmd_cancel",
michael@0 38 "downloadsCmd_open", "downloadsCmd_show", "downloadsCmd_retry",
michael@0 39 "downloadsCmd_openReferrer", "downloadsCmd_clearDownloads"];
michael@0 40
michael@0 41 const NOT_AVAILABLE = Number.MAX_VALUE;
michael@0 42
michael@0 43 /**
michael@0 44 * A download element shell is responsible for handling the commands and the
michael@0 45 * displayed data for a single download view element. The download element
michael@0 46 * could represent either a past download (for which we get data from places) or
michael@0 47 * a "session" download (using a data-item object. See DownloadsCommon.jsm), or both.
michael@0 48 *
michael@0 49 * Once initialized with either a data item or a places node, the created richlistitem
michael@0 50 * can be accessed through the |element| getter, and can then be inserted/removed from
michael@0 51 * a richlistbox.
michael@0 52 *
michael@0 53 * The shell doesn't take care of inserting the item, or removing it when it's no longer
michael@0 54 * valid. That's the caller (a DownloadsPlacesView object) responsibility.
michael@0 55 *
michael@0 56 * The caller is also responsible for "passing over" notification from both the
michael@0 57 * download-view and the places-result-observer, in the following manner:
michael@0 58 * - The DownloadsPlacesView object implements getViewItem of the download-view
michael@0 59 * pseudo interface. It returns this object (therefore we implement
michael@0 60 * onStateChangea and onProgressChange here).
michael@0 61 * - The DownloadsPlacesView object adds itself as a places result observer and
michael@0 62 * calls this object's placesNodeIconChanged, placesNodeTitleChanged and
michael@0 63 * placeNodeAnnotationChanged from its callbacks.
michael@0 64 *
michael@0 65 * @param [optional] aDataItem
michael@0 66 * The data item of a the session download. Required if aPlacesNode is not set
michael@0 67 * @param [optional] aPlacesNode
michael@0 68 * The places node for a past download. Required if aDataItem is not set.
michael@0 69 * @param [optional] aAnnotations
michael@0 70 * Map containing annotations values, to speed up the initial loading.
michael@0 71 */
michael@0 72 function DownloadElementShell(aDataItem, aPlacesNode, aAnnotations) {
michael@0 73 this._element = document.createElement("richlistitem");
michael@0 74 this._element._shell = this;
michael@0 75
michael@0 76 this._element.classList.add("download");
michael@0 77 this._element.classList.add("download-state");
michael@0 78
michael@0 79 if (aAnnotations)
michael@0 80 this._annotations = aAnnotations;
michael@0 81 if (aDataItem)
michael@0 82 this.dataItem = aDataItem;
michael@0 83 if (aPlacesNode)
michael@0 84 this.placesNode = aPlacesNode;
michael@0 85 }
michael@0 86
michael@0 87 DownloadElementShell.prototype = {
michael@0 88 // The richlistitem for the download
michael@0 89 get element() this._element,
michael@0 90
michael@0 91 /**
michael@0 92 * Manages the "active" state of the shell. By default all the shells
michael@0 93 * without a dataItem are inactive, thus their UI is not updated. They must
michael@0 94 * be activated when entering the visible area. Session downloads are
michael@0 95 * always active since they always have a dataItem.
michael@0 96 */
michael@0 97 ensureActive: function DES_ensureActive() {
michael@0 98 if (!this._active) {
michael@0 99 this._active = true;
michael@0 100 this._element.setAttribute("active", true);
michael@0 101 this._updateUI();
michael@0 102 }
michael@0 103 },
michael@0 104 get active() !!this._active,
michael@0 105
michael@0 106 // The data item for the download
michael@0 107 _dataItem: null,
michael@0 108 get dataItem() this._dataItem,
michael@0 109
michael@0 110 set dataItem(aValue) {
michael@0 111 if (this._dataItem != aValue) {
michael@0 112 if (!aValue && !this._placesNode)
michael@0 113 throw new Error("Should always have either a dataItem or a placesNode");
michael@0 114
michael@0 115 this._dataItem = aValue;
michael@0 116 if (!this.active)
michael@0 117 this.ensureActive();
michael@0 118 else
michael@0 119 this._updateUI();
michael@0 120 }
michael@0 121 return aValue;
michael@0 122 },
michael@0 123
michael@0 124 _placesNode: null,
michael@0 125 get placesNode() this._placesNode,
michael@0 126 set placesNode(aValue) {
michael@0 127 if (this._placesNode != aValue) {
michael@0 128 if (!aValue && !this._dataItem)
michael@0 129 throw new Error("Should always have either a dataItem or a placesNode");
michael@0 130
michael@0 131 // Preserve the annotations map if this is the first loading and we got
michael@0 132 // cached values.
michael@0 133 if (this._placesNode || !this._annotations) {
michael@0 134 this._annotations = new Map();
michael@0 135 }
michael@0 136
michael@0 137 this._placesNode = aValue;
michael@0 138
michael@0 139 // We don't need to update the UI if we had a data item, because
michael@0 140 // the places information isn't used in this case.
michael@0 141 if (!this._dataItem && this.active)
michael@0 142 this._updateUI();
michael@0 143 }
michael@0 144 return aValue;
michael@0 145 },
michael@0 146
michael@0 147 // The download uri (as a string)
michael@0 148 get downloadURI() {
michael@0 149 if (this._dataItem)
michael@0 150 return this._dataItem.uri;
michael@0 151 if (this._placesNode)
michael@0 152 return this._placesNode.uri;
michael@0 153 throw new Error("Unexpected download element state");
michael@0 154 },
michael@0 155
michael@0 156 get _downloadURIObj() {
michael@0 157 if (!("__downloadURIObj" in this))
michael@0 158 this.__downloadURIObj = NetUtil.newURI(this.downloadURI);
michael@0 159 return this.__downloadURIObj;
michael@0 160 },
michael@0 161
michael@0 162 _getIcon: function DES__getIcon() {
michael@0 163 let metaData = this.getDownloadMetaData();
michael@0 164 if ("filePath" in metaData)
michael@0 165 return "moz-icon://" + metaData.filePath + "?size=32";
michael@0 166
michael@0 167 if (this._placesNode) {
michael@0 168 // Try to extract an extension from the uri.
michael@0 169 let ext = this._downloadURIObj.QueryInterface(Ci.nsIURL).fileExtension;
michael@0 170 if (ext)
michael@0 171 return "moz-icon://." + ext + "?size=32";
michael@0 172 return this._placesNode.icon || "moz-icon://.unknown?size=32";
michael@0 173 }
michael@0 174 if (this._dataItem)
michael@0 175 throw new Error("Session-download items should always have a target file uri");
michael@0 176
michael@0 177 throw new Error("Unexpected download element state");
michael@0 178 },
michael@0 179
michael@0 180 // Helper for getting a places annotation set for the download.
michael@0 181 _getAnnotation: function DES__getAnnotation(aAnnotation, aDefaultValue) {
michael@0 182 let value;
michael@0 183 if (this._annotations.has(aAnnotation))
michael@0 184 value = this._annotations.get(aAnnotation);
michael@0 185
michael@0 186 // If the value is cached, or we know it doesn't exist, avoid a database
michael@0 187 // lookup.
michael@0 188 if (value === undefined) {
michael@0 189 try {
michael@0 190 value = PlacesUtils.annotations.getPageAnnotation(
michael@0 191 this._downloadURIObj, aAnnotation);
michael@0 192 }
michael@0 193 catch(ex) {
michael@0 194 value = NOT_AVAILABLE;
michael@0 195 }
michael@0 196 }
michael@0 197
michael@0 198 if (value === NOT_AVAILABLE) {
michael@0 199 if (aDefaultValue === undefined) {
michael@0 200 throw new Error("Could not get required annotation '" + aAnnotation +
michael@0 201 "' for download with url '" + this.downloadURI + "'");
michael@0 202 }
michael@0 203 value = aDefaultValue;
michael@0 204 }
michael@0 205
michael@0 206 this._annotations.set(aAnnotation, value);
michael@0 207 return value;
michael@0 208 },
michael@0 209
michael@0 210 _fetchTargetFileInfo: function DES__fetchTargetFileInfo(aUpdateMetaDataAndStatusUI = false) {
michael@0 211 if (this._targetFileInfoFetched)
michael@0 212 throw new Error("_fetchTargetFileInfo should not be called if the information was already fetched");
michael@0 213 if (!this.active)
michael@0 214 throw new Error("Trying to _fetchTargetFileInfo on an inactive download shell");
michael@0 215
michael@0 216 let path = this.getDownloadMetaData().filePath;
michael@0 217
michael@0 218 // In previous version, the target file annotations were not set,
michael@0 219 // so we cannot tell where is the file.
michael@0 220 if (path === undefined) {
michael@0 221 this._targetFileInfoFetched = true;
michael@0 222 this._targetFileExists = false;
michael@0 223 if (aUpdateMetaDataAndStatusUI) {
michael@0 224 this._metaData = null;
michael@0 225 this._updateDownloadStatusUI();
michael@0 226 }
michael@0 227 // Here we don't need to update the download commands,
michael@0 228 // as the state is unknown as it was.
michael@0 229 return;
michael@0 230 }
michael@0 231
michael@0 232 OS.File.stat(path).then(
michael@0 233 function onSuccess(fileInfo) {
michael@0 234 this._targetFileInfoFetched = true;
michael@0 235 this._targetFileExists = true;
michael@0 236 this._targetFileSize = fileInfo.size;
michael@0 237 if (aUpdateMetaDataAndStatusUI) {
michael@0 238 this._metaData = null;
michael@0 239 this._updateDownloadStatusUI();
michael@0 240 }
michael@0 241 if (this._element.selected)
michael@0 242 goUpdateDownloadCommands();
michael@0 243 }.bind(this),
michael@0 244
michael@0 245 function onFailure(aReason) {
michael@0 246 if (aReason instanceof OS.File.Error && aReason.becauseNoSuchFile) {
michael@0 247 this._targetFileInfoFetched = true;
michael@0 248 this._targetFileExists = false;
michael@0 249 }
michael@0 250 else {
michael@0 251 Cu.reportError("Could not fetch info for target file (reason: " +
michael@0 252 aReason + ")");
michael@0 253 }
michael@0 254
michael@0 255 if (aUpdateMetaDataAndStatusUI) {
michael@0 256 this._metaData = null;
michael@0 257 this._updateDownloadStatusUI();
michael@0 258 }
michael@0 259
michael@0 260 if (this._element.selected)
michael@0 261 goUpdateDownloadCommands();
michael@0 262 }.bind(this)
michael@0 263 );
michael@0 264 },
michael@0 265
michael@0 266 _getAnnotatedMetaData: function DES__getAnnotatedMetaData()
michael@0 267 JSON.parse(this._getAnnotation(DOWNLOAD_META_DATA_ANNO)),
michael@0 268
michael@0 269 _extractFilePathAndNameFromFileURI:
michael@0 270 function DES__extractFilePathAndNameFromFileURI(aFileURI) {
michael@0 271 let file = Cc["@mozilla.org/network/protocol;1?name=file"]
michael@0 272 .getService(Ci.nsIFileProtocolHandler)
michael@0 273 .getFileFromURLSpec(aFileURI);
michael@0 274 return [file.path, file.leafName];
michael@0 275 },
michael@0 276
michael@0 277 /**
michael@0 278 * Retrieve the meta data object for the download. The following fields
michael@0 279 * may be set.
michael@0 280 *
michael@0 281 * - state - any download state defined in nsIDownloadManager. If this field
michael@0 282 * is not set, the download state is unknown.
michael@0 283 * - endTime: the end time of the download.
michael@0 284 * - filePath: the downloaded file path on the file system, when it
michael@0 285 * was downloaded. The file may not exist. This is set for session
michael@0 286 * downloads that have a local file set, and for history downloads done
michael@0 287 * after the landing of bug 591289.
michael@0 288 * - fileName: the downloaded file name on the file system. Set if filePath
michael@0 289 * is set.
michael@0 290 * - displayName: the user-facing label for the download. This is always
michael@0 291 * set. If available, it's set to the downloaded file name. If not,
michael@0 292 * the places title for the download uri is used it's set. As a last
michael@0 293 * resort, we fallback to the download uri.
michael@0 294 * - fileSize (only set for downloads which completed succesfully):
michael@0 295 * the downloaded file size. For downloads done after the landing of
michael@0 296 * bug 826991, this value is "static" - that is, it does not necessarily
michael@0 297 * mean that the file is in place and has this size.
michael@0 298 */
michael@0 299 getDownloadMetaData: function DES_getDownloadMetaData() {
michael@0 300 if (!this._metaData) {
michael@0 301 if (this._dataItem) {
michael@0 302 this._metaData = {
michael@0 303 state: this._dataItem.state,
michael@0 304 endTime: this._dataItem.endTime,
michael@0 305 fileName: this._dataItem.target,
michael@0 306 displayName: this._dataItem.target
michael@0 307 };
michael@0 308 if (this._dataItem.done)
michael@0 309 this._metaData.fileSize = this._dataItem.maxBytes;
michael@0 310 if (this._dataItem.localFile)
michael@0 311 this._metaData.filePath = this._dataItem.localFile.path;
michael@0 312 }
michael@0 313 else {
michael@0 314 try {
michael@0 315 this._metaData = this._getAnnotatedMetaData();
michael@0 316 }
michael@0 317 catch(ex) {
michael@0 318 this._metaData = { };
michael@0 319 if (this._targetFileInfoFetched && this._targetFileExists) {
michael@0 320 this._metaData.state = this._targetFileSize > 0 ?
michael@0 321 nsIDM.DOWNLOAD_FINISHED : nsIDM.DOWNLOAD_FAILED;
michael@0 322 this._metaData.fileSize = this._targetFileSize;
michael@0 323 }
michael@0 324
michael@0 325 // This is actually the start-time, but it's the best we can get.
michael@0 326 this._metaData.endTime = this._placesNode.time / 1000;
michael@0 327 }
michael@0 328
michael@0 329 try {
michael@0 330 let targetFileURI = this._getAnnotation(DESTINATION_FILE_URI_ANNO);
michael@0 331 [this._metaData.filePath, this._metaData.fileName] =
michael@0 332 this._extractFilePathAndNameFromFileURI(targetFileURI);
michael@0 333 this._metaData.displayName = this._metaData.fileName;
michael@0 334 }
michael@0 335 catch(ex) {
michael@0 336 this._metaData.displayName = this._placesNode.title || this.downloadURI;
michael@0 337 }
michael@0 338 }
michael@0 339 }
michael@0 340 return this._metaData;
michael@0 341 },
michael@0 342
michael@0 343 // The status text for the download
michael@0 344 _getStatusText: function DES__getStatusText() {
michael@0 345 let s = DownloadsCommon.strings;
michael@0 346 if (this._dataItem && this._dataItem.inProgress) {
michael@0 347 if (this._dataItem.paused) {
michael@0 348 let transfer =
michael@0 349 DownloadUtils.getTransferTotal(this._dataItem.currBytes,
michael@0 350 this._dataItem.maxBytes);
michael@0 351
michael@0 352 // We use the same XUL label to display both the state and the amount
michael@0 353 // transferred, for example "Paused - 1.1 MB".
michael@0 354 return s.statusSeparatorBeforeNumber(s.statePaused, transfer);
michael@0 355 }
michael@0 356 if (this._dataItem.state == nsIDM.DOWNLOAD_DOWNLOADING) {
michael@0 357 let [status, newEstimatedSecondsLeft] =
michael@0 358 DownloadUtils.getDownloadStatus(this.dataItem.currBytes,
michael@0 359 this.dataItem.maxBytes,
michael@0 360 this.dataItem.speed,
michael@0 361 this._lastEstimatedSecondsLeft || Infinity);
michael@0 362 this._lastEstimatedSecondsLeft = newEstimatedSecondsLeft;
michael@0 363 return status;
michael@0 364 }
michael@0 365 if (this._dataItem.starting) {
michael@0 366 return s.stateStarting;
michael@0 367 }
michael@0 368 if (this._dataItem.state == nsIDM.DOWNLOAD_SCANNING) {
michael@0 369 return s.stateScanning;
michael@0 370 }
michael@0 371
michael@0 372 throw new Error("_getStatusText called with a bogus download state");
michael@0 373 }
michael@0 374
michael@0 375 // This is a not-in-progress or history download.
michael@0 376 let stateLabel = "";
michael@0 377 let state = this.getDownloadMetaData().state;
michael@0 378 switch (state) {
michael@0 379 case nsIDM.DOWNLOAD_FAILED:
michael@0 380 stateLabel = s.stateFailed;
michael@0 381 break;
michael@0 382 case nsIDM.DOWNLOAD_CANCELED:
michael@0 383 stateLabel = s.stateCanceled;
michael@0 384 break;
michael@0 385 case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
michael@0 386 stateLabel = s.stateBlockedParentalControls;
michael@0 387 break;
michael@0 388 case nsIDM.DOWNLOAD_BLOCKED_POLICY:
michael@0 389 stateLabel = s.stateBlockedPolicy;
michael@0 390 break;
michael@0 391 case nsIDM.DOWNLOAD_DIRTY:
michael@0 392 stateLabel = s.stateDirty;
michael@0 393 break;
michael@0 394 case nsIDM.DOWNLOAD_FINISHED:{
michael@0 395 // For completed downloads, show the file size (e.g. "1.5 MB")
michael@0 396 let metaData = this.getDownloadMetaData();
michael@0 397 if ("fileSize" in metaData) {
michael@0 398 let [size, unit] = DownloadUtils.convertByteUnits(metaData.fileSize);
michael@0 399 stateLabel = s.sizeWithUnits(size, unit);
michael@0 400 break;
michael@0 401 }
michael@0 402 // Fallback to default unknown state.
michael@0 403 }
michael@0 404 default:
michael@0 405 stateLabel = s.sizeUnknown;
michael@0 406 break;
michael@0 407 }
michael@0 408
michael@0 409 // TODO (bug 829201): history downloads should get the referrer from Places.
michael@0 410 let referrer = this._dataItem && this._dataItem.referrer ||
michael@0 411 this.downloadURI;
michael@0 412 let [displayHost, fullHost] = DownloadUtils.getURIHost(referrer);
michael@0 413
michael@0 414 let date = new Date(this.getDownloadMetaData().endTime);
michael@0 415 let [displayDate, fullDate] = DownloadUtils.getReadableDates(date);
michael@0 416
michael@0 417 // We use the same XUL label to display the state, the host name, and the
michael@0 418 // end time.
michael@0 419 let firstPart = s.statusSeparator(stateLabel, displayHost);
michael@0 420 return s.statusSeparator(firstPart, displayDate);
michael@0 421 },
michael@0 422
michael@0 423 // The progressmeter element for the download
michael@0 424 get _progressElement() {
michael@0 425 if (!("__progressElement" in this)) {
michael@0 426 this.__progressElement =
michael@0 427 document.getAnonymousElementByAttribute(this._element, "anonid",
michael@0 428 "progressmeter");
michael@0 429 }
michael@0 430 return this.__progressElement;
michael@0 431 },
michael@0 432
michael@0 433 // Updates the download state attribute (and by that hide/unhide the
michael@0 434 // appropriate buttons and context menu items), the status text label,
michael@0 435 // and the progress meter.
michael@0 436 _updateDownloadStatusUI: function DES__updateDownloadStatusUI() {
michael@0 437 if (!this.active)
michael@0 438 throw new Error("_updateDownloadStatusUI called for an inactive item.");
michael@0 439
michael@0 440 let state = this.getDownloadMetaData().state;
michael@0 441 if (state !== undefined)
michael@0 442 this._element.setAttribute("state", state);
michael@0 443
michael@0 444 this._element.setAttribute("status", this._getStatusText());
michael@0 445
michael@0 446 // For past-downloads, we're done. For session-downloads, we may also need
michael@0 447 // to update the progress-meter.
michael@0 448 if (!this._dataItem)
michael@0 449 return;
michael@0 450
michael@0 451 // Copied from updateProgress in downloads.js.
michael@0 452 if (this._dataItem.starting) {
michael@0 453 // Before the download starts, the progress meter has its initial value.
michael@0 454 this._element.setAttribute("progressmode", "normal");
michael@0 455 this._element.setAttribute("progress", "0");
michael@0 456 }
michael@0 457 else if (this._dataItem.state == nsIDM.DOWNLOAD_SCANNING ||
michael@0 458 this._dataItem.percentComplete == -1) {
michael@0 459 // We might not know the progress of a running download, and we don't know
michael@0 460 // the remaining time during the malware scanning phase.
michael@0 461 this._element.setAttribute("progressmode", "undetermined");
michael@0 462 }
michael@0 463 else {
michael@0 464 // This is a running download of which we know the progress.
michael@0 465 this._element.setAttribute("progressmode", "normal");
michael@0 466 this._element.setAttribute("progress", this._dataItem.percentComplete);
michael@0 467 }
michael@0 468
michael@0 469 // Dispatch the ValueChange event for accessibility, if possible.
michael@0 470 if (this._progressElement) {
michael@0 471 let event = document.createEvent("Events");
michael@0 472 event.initEvent("ValueChange", true, true);
michael@0 473 this._progressElement.dispatchEvent(event);
michael@0 474 }
michael@0 475 },
michael@0 476
michael@0 477 _updateDisplayNameAndIcon: function DES__updateDisplayNameAndIcon() {
michael@0 478 let metaData = this.getDownloadMetaData();
michael@0 479 this._element.setAttribute("displayName", metaData.displayName);
michael@0 480 this._element.setAttribute("image", this._getIcon());
michael@0 481 },
michael@0 482
michael@0 483 _updateUI: function DES__updateUI() {
michael@0 484 if (!this.active)
michael@0 485 throw new Error("Trying to _updateUI on an inactive download shell");
michael@0 486
michael@0 487 this._metaData = null;
michael@0 488 this._targetFileInfoFetched = false;
michael@0 489
michael@0 490 this._updateDisplayNameAndIcon();
michael@0 491
michael@0 492 // For history downloads done in past releases, the downloads/metaData
michael@0 493 // annotation is not set, and therefore we cannot tell the download
michael@0 494 // state without the target file information.
michael@0 495 if (this._dataItem || this.getDownloadMetaData().state !== undefined)
michael@0 496 this._updateDownloadStatusUI();
michael@0 497 else
michael@0 498 this._fetchTargetFileInfo(true);
michael@0 499 },
michael@0 500
michael@0 501 placesNodeIconChanged: function DES_placesNodeIconChanged() {
michael@0 502 if (!this._dataItem)
michael@0 503 this._element.setAttribute("image", this._getIcon());
michael@0 504 },
michael@0 505
michael@0 506 placesNodeTitleChanged: function DES_placesNodeTitleChanged() {
michael@0 507 // If there's a file path, we use the leaf name for the title.
michael@0 508 if (!this._dataItem && this.active && !this.getDownloadMetaData().filePath) {
michael@0 509 this._metaData = null;
michael@0 510 this._updateDisplayNameAndIcon();
michael@0 511 }
michael@0 512 },
michael@0 513
michael@0 514 placesNodeAnnotationChanged: function DES_placesNodeAnnotationChanged(aAnnoName) {
michael@0 515 this._annotations.delete(aAnnoName);
michael@0 516 if (!this._dataItem && this.active) {
michael@0 517 if (aAnnoName == DOWNLOAD_META_DATA_ANNO) {
michael@0 518 let metaData = this.getDownloadMetaData();
michael@0 519 let annotatedMetaData = this._getAnnotatedMetaData();
michael@0 520 metaData.endTme = annotatedMetaData.endTime;
michael@0 521 if ("fileSize" in annotatedMetaData)
michael@0 522 metaData.fileSize = annotatedMetaData.fileSize;
michael@0 523 else
michael@0 524 delete metaData.fileSize;
michael@0 525
michael@0 526 if (metaData.state != annotatedMetaData.state) {
michael@0 527 metaData.state = annotatedMetaData.state;
michael@0 528 if (this._element.selected)
michael@0 529 goUpdateDownloadCommands();
michael@0 530 }
michael@0 531
michael@0 532 this._updateDownloadStatusUI();
michael@0 533 }
michael@0 534 else if (aAnnoName == DESTINATION_FILE_URI_ANNO) {
michael@0 535 let metaData = this.getDownloadMetaData();
michael@0 536 let targetFileURI = this._getAnnotation(DESTINATION_FILE_URI_ANNO);
michael@0 537 [metaData.filePath, metaData.fileName] =
michael@0 538 this._extractFilePathAndNameFromFileURI(targetFileURI);
michael@0 539 metaData.displayName = metaData.fileName;
michael@0 540 this._updateDisplayNameAndIcon();
michael@0 541
michael@0 542 if (this._targetFileInfoFetched) {
michael@0 543 // This will also update the download commands if necessary.
michael@0 544 this._targetFileInfoFetched = false;
michael@0 545 this._fetchTargetFileInfo();
michael@0 546 }
michael@0 547 }
michael@0 548 }
michael@0 549 },
michael@0 550
michael@0 551 /* DownloadView */
michael@0 552 onStateChange: function DES_onStateChange(aOldState) {
michael@0 553 let metaData = this.getDownloadMetaData();
michael@0 554 metaData.state = this.dataItem.state;
michael@0 555 if (aOldState != nsIDM.DOWNLOAD_FINISHED && aOldState != metaData.state) {
michael@0 556 // See comment in DVI_onStateChange in downloads.js (the panel-view)
michael@0 557 this._element.setAttribute("image", this._getIcon() + "&state=normal");
michael@0 558 metaData.fileSize = this._dataItem.maxBytes;
michael@0 559 if (this._targetFileInfoFetched) {
michael@0 560 this._targetFileInfoFetched = false;
michael@0 561 this._fetchTargetFileInfo();
michael@0 562 }
michael@0 563 }
michael@0 564
michael@0 565 this._updateDownloadStatusUI();
michael@0 566 if (this._element.selected)
michael@0 567 goUpdateDownloadCommands();
michael@0 568 else
michael@0 569 goUpdateCommand("downloadsCmd_clearDownloads");
michael@0 570 },
michael@0 571
michael@0 572 /* DownloadView */
michael@0 573 onProgressChange: function DES_onProgressChange() {
michael@0 574 this._updateDownloadStatusUI();
michael@0 575 },
michael@0 576
michael@0 577 /* nsIController */
michael@0 578 isCommandEnabled: function DES_isCommandEnabled(aCommand) {
michael@0 579 // The only valid command for inactive elements is cmd_delete.
michael@0 580 if (!this.active && aCommand != "cmd_delete")
michael@0 581 return false;
michael@0 582 switch (aCommand) {
michael@0 583 case "downloadsCmd_open": {
michael@0 584 // We cannot open a session dowload file unless it's done ("openable").
michael@0 585 // If it's finished, we need to make sure the file was not removed,
michael@0 586 // as we do for past downloads.
michael@0 587 if (this._dataItem && !this._dataItem.openable)
michael@0 588 return false;
michael@0 589
michael@0 590 if (this._targetFileInfoFetched)
michael@0 591 return this._targetFileExists;
michael@0 592
michael@0 593 // If the target file information is not yet fetched,
michael@0 594 // temporarily assume that the file is in place.
michael@0 595 return this.getDownloadMetaData().state == nsIDM.DOWNLOAD_FINISHED;
michael@0 596 }
michael@0 597 case "downloadsCmd_show": {
michael@0 598 // TODO: Bug 827010 - Handle part-file asynchronously.
michael@0 599 if (this._dataItem &&
michael@0 600 this._dataItem.partFile && this._dataItem.partFile.exists())
michael@0 601 return true;
michael@0 602
michael@0 603 if (this._targetFileInfoFetched)
michael@0 604 return this._targetFileExists;
michael@0 605
michael@0 606 // If the target file information is not yet fetched,
michael@0 607 // temporarily assume that the file is in place.
michael@0 608 return this.getDownloadMetaData().state == nsIDM.DOWNLOAD_FINISHED;
michael@0 609 }
michael@0 610 case "downloadsCmd_pauseResume":
michael@0 611 return this._dataItem && this._dataItem.inProgress && this._dataItem.resumable;
michael@0 612 case "downloadsCmd_retry":
michael@0 613 // An history download can always be retried.
michael@0 614 return !this._dataItem || this._dataItem.canRetry;
michael@0 615 case "downloadsCmd_openReferrer":
michael@0 616 return this._dataItem && !!this._dataItem.referrer;
michael@0 617 case "cmd_delete":
michael@0 618 // The behavior in this case is somewhat unexpected, so we disallow that.
michael@0 619 if (this._placesNode && this._dataItem && this._dataItem.inProgress)
michael@0 620 return false;
michael@0 621 return true;
michael@0 622 case "downloadsCmd_cancel":
michael@0 623 return this._dataItem != null;
michael@0 624 }
michael@0 625 return false;
michael@0 626 },
michael@0 627
michael@0 628 _retryAsHistoryDownload: function DES__retryAsHistoryDownload() {
michael@0 629 // In future we may try to download into the same original target uri, when
michael@0 630 // we have it. Though that requires verifying the path is still valid and
michael@0 631 // may surprise the user if he wants to be requested every time.
michael@0 632 let browserWin = RecentWindow.getMostRecentBrowserWindow();
michael@0 633 let initiatingDoc = browserWin ? browserWin.document : document;
michael@0 634 DownloadURL(this.downloadURI, this.getDownloadMetaData().fileName,
michael@0 635 initiatingDoc);
michael@0 636 },
michael@0 637
michael@0 638 /* nsIController */
michael@0 639 doCommand: function DES_doCommand(aCommand) {
michael@0 640 switch (aCommand) {
michael@0 641 case "downloadsCmd_open": {
michael@0 642 let file = this._dataItem ?
michael@0 643 this.dataItem.localFile :
michael@0 644 new FileUtils.File(this.getDownloadMetaData().filePath);
michael@0 645
michael@0 646 DownloadsCommon.openDownloadedFile(file, null, window);
michael@0 647 break;
michael@0 648 }
michael@0 649 case "downloadsCmd_show": {
michael@0 650 if (this._dataItem) {
michael@0 651 this._dataItem.showLocalFile();
michael@0 652 }
michael@0 653 else {
michael@0 654 let file = new FileUtils.File(this.getDownloadMetaData().filePath);
michael@0 655 DownloadsCommon.showDownloadedFile(file);
michael@0 656 }
michael@0 657 break;
michael@0 658 }
michael@0 659 case "downloadsCmd_openReferrer": {
michael@0 660 openURL(this._dataItem.referrer);
michael@0 661 break;
michael@0 662 }
michael@0 663 case "downloadsCmd_cancel": {
michael@0 664 this._dataItem.cancel();
michael@0 665 break;
michael@0 666 }
michael@0 667 case "cmd_delete": {
michael@0 668 if (this._dataItem)
michael@0 669 this._dataItem.remove();
michael@0 670 if (this._placesNode)
michael@0 671 PlacesUtils.bhistory.removePage(this._downloadURIObj);
michael@0 672 break;
michael@0 673 }
michael@0 674 case "downloadsCmd_retry": {
michael@0 675 if (this._dataItem)
michael@0 676 this._dataItem.retry();
michael@0 677 else
michael@0 678 this._retryAsHistoryDownload();
michael@0 679 break;
michael@0 680 }
michael@0 681 case "downloadsCmd_pauseResume": {
michael@0 682 this._dataItem.togglePauseResume();
michael@0 683 break;
michael@0 684 }
michael@0 685 }
michael@0 686 },
michael@0 687
michael@0 688 // Returns whether or not the download handled by this shell should
michael@0 689 // show up in the search results for the given term. Both the display
michael@0 690 // name for the download and the url are searched.
michael@0 691 matchesSearchTerm: function DES_matchesSearchTerm(aTerm) {
michael@0 692 if (!aTerm)
michael@0 693 return true;
michael@0 694 aTerm = aTerm.toLowerCase();
michael@0 695 return this.getDownloadMetaData().displayName.toLowerCase().contains(aTerm) ||
michael@0 696 this.downloadURI.toLowerCase().contains(aTerm);
michael@0 697 },
michael@0 698
michael@0 699 // Handles return kepress on the element (the keypress listener is
michael@0 700 // set in the DownloadsPlacesView object).
michael@0 701 doDefaultCommand: function DES_doDefaultCommand() {
michael@0 702 function getDefaultCommandForState(aState) {
michael@0 703 switch (aState) {
michael@0 704 case nsIDM.DOWNLOAD_FINISHED:
michael@0 705 return "downloadsCmd_open";
michael@0 706 case nsIDM.DOWNLOAD_PAUSED:
michael@0 707 return "downloadsCmd_pauseResume";
michael@0 708 case nsIDM.DOWNLOAD_NOTSTARTED:
michael@0 709 case nsIDM.DOWNLOAD_QUEUED:
michael@0 710 return "downloadsCmd_cancel";
michael@0 711 case nsIDM.DOWNLOAD_FAILED:
michael@0 712 case nsIDM.DOWNLOAD_CANCELED:
michael@0 713 return "downloadsCmd_retry";
michael@0 714 case nsIDM.DOWNLOAD_SCANNING:
michael@0 715 return "downloadsCmd_show";
michael@0 716 case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
michael@0 717 case nsIDM.DOWNLOAD_DIRTY:
michael@0 718 case nsIDM.DOWNLOAD_BLOCKED_POLICY:
michael@0 719 return "downloadsCmd_openReferrer";
michael@0 720 }
michael@0 721 return "";
michael@0 722 }
michael@0 723 let command = getDefaultCommandForState(this.getDownloadMetaData().state);
michael@0 724 if (command && this.isCommandEnabled(command))
michael@0 725 this.doCommand(command);
michael@0 726 },
michael@0 727
michael@0 728 /**
michael@0 729 * At the first time an item is selected, we don't yet have
michael@0 730 * the target file information. Thus the call to goUpdateDownloadCommands
michael@0 731 * in DPV_onSelect would result in best-guess enabled/disabled result.
michael@0 732 * That way we let the user perform command immediately. However, once
michael@0 733 * we have the target file information, we can update the commands
michael@0 734 * appropriately (_fetchTargetFileInfo() calls goUpdateDownloadCommands).
michael@0 735 */
michael@0 736 onSelect: function DES_onSelect() {
michael@0 737 if (!this.active)
michael@0 738 return;
michael@0 739 if (!this._targetFileInfoFetched)
michael@0 740 this._fetchTargetFileInfo();
michael@0 741 }
michael@0 742 };
michael@0 743
michael@0 744 /**
michael@0 745 * A Downloads Places View is a places view designed to show a places query
michael@0 746 * for history donwloads alongside the current "session"-downloads.
michael@0 747 *
michael@0 748 * As we don't use the places controller, some methods implemented by other
michael@0 749 * places views are not implemented by this view.
michael@0 750 *
michael@0 751 * A richlistitem in this view can represent either a past download or a session
michael@0 752 * download, or both. Session downloads are shown first in the view, and as long
michael@0 753 * as they exist they "collapses" their history "counterpart" (So we don't show two
michael@0 754 * items for every download).
michael@0 755 */
michael@0 756 function DownloadsPlacesView(aRichListBox, aActive = true) {
michael@0 757 this._richlistbox = aRichListBox;
michael@0 758 this._richlistbox._placesView = this;
michael@0 759 window.controllers.insertControllerAt(0, this);
michael@0 760
michael@0 761 // Map download URLs to download element shells regardless of their type
michael@0 762 this._downloadElementsShellsForURI = new Map();
michael@0 763
michael@0 764 // Map download data items to their element shells.
michael@0 765 this._viewItemsForDataItems = new WeakMap();
michael@0 766
michael@0 767 // Points to the last session download element. We keep track of this
michael@0 768 // in order to keep all session downloads above past downloads.
michael@0 769 this._lastSessionDownloadElement = null;
michael@0 770
michael@0 771 this._searchTerm = "";
michael@0 772
michael@0 773 this._active = aActive;
michael@0 774
michael@0 775 // Register as a downloads view. The places data will be initialized by
michael@0 776 // the places setter.
michael@0 777 this._initiallySelectedElement = null;
michael@0 778 this._downloadsData = DownloadsCommon.getData(window.opener || window);
michael@0 779 this._downloadsData.addView(this);
michael@0 780
michael@0 781 // Get the Download button out of the attention state since we're about to
michael@0 782 // view all downloads.
michael@0 783 DownloadsCommon.getIndicatorData(window).attention = false;
michael@0 784
michael@0 785 // Make sure to unregister the view if the window is closed.
michael@0 786 window.addEventListener("unload", function() {
michael@0 787 window.controllers.removeController(this);
michael@0 788 this._downloadsData.removeView(this);
michael@0 789 this.result = null;
michael@0 790 }.bind(this), true);
michael@0 791 // Resizing the window may change items visibility.
michael@0 792 window.addEventListener("resize", function() {
michael@0 793 this._ensureVisibleElementsAreActive();
michael@0 794 }.bind(this), true);
michael@0 795 }
michael@0 796
michael@0 797 DownloadsPlacesView.prototype = {
michael@0 798 get associatedElement() this._richlistbox,
michael@0 799
michael@0 800 get active() this._active,
michael@0 801 set active(val) {
michael@0 802 this._active = val;
michael@0 803 if (this._active)
michael@0 804 this._ensureVisibleElementsAreActive();
michael@0 805 return this._active;
michael@0 806 },
michael@0 807
michael@0 808 _forEachDownloadElementShellForURI:
michael@0 809 function DPV__forEachDownloadElementShellForURI(aURI, aCallback) {
michael@0 810 if (this._downloadElementsShellsForURI.has(aURI)) {
michael@0 811 let downloadElementShells = this._downloadElementsShellsForURI.get(aURI);
michael@0 812 for (let des of downloadElementShells) {
michael@0 813 aCallback(des);
michael@0 814 }
michael@0 815 }
michael@0 816 },
michael@0 817
michael@0 818 _getAnnotationsFor: function DPV_getAnnotationsFor(aURI) {
michael@0 819 if (!this._cachedAnnotations) {
michael@0 820 this._cachedAnnotations = new Map();
michael@0 821 for (let name of [ DESTINATION_FILE_URI_ANNO,
michael@0 822 DOWNLOAD_META_DATA_ANNO ]) {
michael@0 823 let results = PlacesUtils.annotations.getAnnotationsWithName(name);
michael@0 824 for (let result of results) {
michael@0 825 let url = result.uri.spec;
michael@0 826 if (!this._cachedAnnotations.has(url))
michael@0 827 this._cachedAnnotations.set(url, new Map());
michael@0 828 let m = this._cachedAnnotations.get(url);
michael@0 829 m.set(result.annotationName, result.annotationValue);
michael@0 830 }
michael@0 831 }
michael@0 832 }
michael@0 833
michael@0 834 let annotations = this._cachedAnnotations.get(aURI);
michael@0 835 if (!annotations) {
michael@0 836 // There are no annotations for this entry, that means it is quite old.
michael@0 837 // Make up a fake annotations entry with default values.
michael@0 838 annotations = new Map();
michael@0 839 annotations.set(DESTINATION_FILE_URI_ANNO, NOT_AVAILABLE);
michael@0 840 }
michael@0 841 // The meta-data annotation has been added recently, so it's likely missing.
michael@0 842 if (!annotations.has(DOWNLOAD_META_DATA_ANNO)) {
michael@0 843 annotations.set(DOWNLOAD_META_DATA_ANNO, NOT_AVAILABLE);
michael@0 844 }
michael@0 845 return annotations;
michael@0 846 },
michael@0 847
michael@0 848 /**
michael@0 849 * Given a data item for a session download, or a places node for a past
michael@0 850 * download, updates the view as necessary.
michael@0 851 * 1. If the given data is a places node, we check whether there are any
michael@0 852 * elements for the same download url. If there are, then we just reset
michael@0 853 * their places node. Otherwise we add a new download element.
michael@0 854 * 2. If the given data is a data item, we first check if there's a history
michael@0 855 * download in the list that is not associated with a data item. If we
michael@0 856 * found one, we use it for the data item as well and reposition it
michael@0 857 * alongside the other session downloads. If we don't, then we go ahead
michael@0 858 * and create a new element for the download.
michael@0 859 *
michael@0 860 * @param aDataItem
michael@0 861 * The data item of a session download. Set to null for history
michael@0 862 * downloads data.
michael@0 863 * @param [optional] aPlacesNode
michael@0 864 * The places node for a history download. Required if there's no data
michael@0 865 * item.
michael@0 866 * @param [optional] aNewest
michael@0 867 * @see onDataItemAdded. Ignored for history downlods.
michael@0 868 * @param [optional] aDocumentFragment
michael@0 869 * To speed up the appending of multiple elements to the end of the
michael@0 870 * list which are coming in a single batch (i.e. invalidateContainer),
michael@0 871 * a document fragment may be passed to which the new elements would
michael@0 872 * be appended. It's the caller's job to ensure the fragment is merged
michael@0 873 * to the richlistbox at the end.
michael@0 874 */
michael@0 875 _addDownloadData:
michael@0 876 function DPV_addDownloadData(aDataItem, aPlacesNode, aNewest = false,
michael@0 877 aDocumentFragment = null) {
michael@0 878 let downloadURI = aPlacesNode ? aPlacesNode.uri : aDataItem.uri;
michael@0 879 let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI);
michael@0 880 if (!shellsForURI) {
michael@0 881 shellsForURI = new Set();
michael@0 882 this._downloadElementsShellsForURI.set(downloadURI, shellsForURI);
michael@0 883 }
michael@0 884
michael@0 885 let newOrUpdatedShell = null;
michael@0 886
michael@0 887 // Trivial: if there are no shells for this download URI, we always
michael@0 888 // need to create one.
michael@0 889 let shouldCreateShell = shellsForURI.size == 0;
michael@0 890
michael@0 891 // However, if we do have shells for this download uri, there are
michael@0 892 // few options:
michael@0 893 // 1) There's only one shell and it's for a history download (it has
michael@0 894 // no data item). In this case, we update this shell and move it
michael@0 895 // if necessary
michael@0 896 // 2) There are multiple shells, indicicating multiple downloads for
michael@0 897 // the same download uri are running. In this case we create
michael@0 898 // anoter shell for the download (so we have one shell for each data
michael@0 899 // item).
michael@0 900 //
michael@0 901 // Note: If a cancelled session download is already in the list, and the
michael@0 902 // download is retired, onDataItemAdded is called again for the same
michael@0 903 // data item. Thus, we also check that we make sure we don't have a view item
michael@0 904 // already.
michael@0 905 if (!shouldCreateShell &&
michael@0 906 aDataItem && this.getViewItem(aDataItem) == null) {
michael@0 907 // If there's a past-download-only shell for this download-uri with no
michael@0 908 // associated data item, use it for the new data item. Otherwise, go ahead
michael@0 909 // and create another shell.
michael@0 910 shouldCreateShell = true;
michael@0 911 for (let shell of shellsForURI) {
michael@0 912 if (!shell.dataItem) {
michael@0 913 shouldCreateShell = false;
michael@0 914 shell.dataItem = aDataItem;
michael@0 915 newOrUpdatedShell = shell;
michael@0 916 this._viewItemsForDataItems.set(aDataItem, shell);
michael@0 917 break;
michael@0 918 }
michael@0 919 }
michael@0 920 }
michael@0 921
michael@0 922 if (shouldCreateShell) {
michael@0 923 // Bug 836271: The annotations for a url should be cached only when the
michael@0 924 // places node is available, i.e. when we know we we'd be notified for
michael@0 925 // annoation changes.
michael@0 926 // Otherwise we may cache NOT_AVILABLE values first for a given session
michael@0 927 // download, and later use these NOT_AVILABLE values when a history
michael@0 928 // download for the same URL is added.
michael@0 929 let cachedAnnotations = aPlacesNode ? this._getAnnotationsFor(downloadURI) : null;
michael@0 930 let shell = new DownloadElementShell(aDataItem, aPlacesNode, cachedAnnotations);
michael@0 931 newOrUpdatedShell = shell;
michael@0 932 shellsForURI.add(shell);
michael@0 933 if (aDataItem)
michael@0 934 this._viewItemsForDataItems.set(aDataItem, shell);
michael@0 935 }
michael@0 936 else if (aPlacesNode) {
michael@0 937 for (let shell of shellsForURI) {
michael@0 938 if (shell.placesNode != aPlacesNode)
michael@0 939 shell.placesNode = aPlacesNode;
michael@0 940 }
michael@0 941 }
michael@0 942
michael@0 943 if (newOrUpdatedShell) {
michael@0 944 if (aNewest) {
michael@0 945 this._richlistbox.insertBefore(newOrUpdatedShell.element,
michael@0 946 this._richlistbox.firstChild);
michael@0 947 if (!this._lastSessionDownloadElement) {
michael@0 948 this._lastSessionDownloadElement = newOrUpdatedShell.element;
michael@0 949 }
michael@0 950 // Some operations like retrying an history download move an element to
michael@0 951 // the top of the richlistbox, along with other session downloads.
michael@0 952 // More generally, if a new download is added, should be made visible.
michael@0 953 this._richlistbox.ensureElementIsVisible(newOrUpdatedShell.element);
michael@0 954 }
michael@0 955 else if (aDataItem) {
michael@0 956 let before = this._lastSessionDownloadElement ?
michael@0 957 this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild;
michael@0 958 this._richlistbox.insertBefore(newOrUpdatedShell.element, before);
michael@0 959 this._lastSessionDownloadElement = newOrUpdatedShell.element;
michael@0 960 }
michael@0 961 else {
michael@0 962 let appendTo = aDocumentFragment || this._richlistbox;
michael@0 963 appendTo.appendChild(newOrUpdatedShell.element);
michael@0 964 }
michael@0 965
michael@0 966 if (this.searchTerm) {
michael@0 967 newOrUpdatedShell.element.hidden =
michael@0 968 !newOrUpdatedShell.element._shell.matchesSearchTerm(this.searchTerm);
michael@0 969 }
michael@0 970 }
michael@0 971
michael@0 972 // If aDocumentFragment is defined this is a batch change, so it's up to
michael@0 973 // the caller to append the fragment and activate the visible shells.
michael@0 974 if (!aDocumentFragment) {
michael@0 975 this._ensureVisibleElementsAreActive();
michael@0 976 goUpdateCommand("downloadsCmd_clearDownloads");
michael@0 977 }
michael@0 978 },
michael@0 979
michael@0 980 _removeElement: function DPV__removeElement(aElement) {
michael@0 981 // If the element was selected exclusively, select its next
michael@0 982 // sibling first, if not, try for previous sibling, if any.
michael@0 983 if ((aElement.nextSibling || aElement.previousSibling) &&
michael@0 984 this._richlistbox.selectedItems &&
michael@0 985 this._richlistbox.selectedItems.length == 1 &&
michael@0 986 this._richlistbox.selectedItems[0] == aElement) {
michael@0 987 this._richlistbox.selectItem(aElement.nextSibling ||
michael@0 988 aElement.previousSibling);
michael@0 989 }
michael@0 990
michael@0 991 if (this._lastSessionDownloadElement == aElement)
michael@0 992 this._lastSessionDownloadElement = aElement.previousSibling;
michael@0 993
michael@0 994 this._richlistbox.removeItemFromSelection(aElement);
michael@0 995 this._richlistbox.removeChild(aElement);
michael@0 996 this._ensureVisibleElementsAreActive();
michael@0 997 goUpdateCommand("downloadsCmd_clearDownloads");
michael@0 998 },
michael@0 999
michael@0 1000 _removeHistoryDownloadFromView:
michael@0 1001 function DPV__removeHistoryDownloadFromView(aPlacesNode) {
michael@0 1002 let downloadURI = aPlacesNode.uri;
michael@0 1003 let shellsForURI = this._downloadElementsShellsForURI.get(downloadURI);
michael@0 1004 if (shellsForURI) {
michael@0 1005 for (let shell of shellsForURI) {
michael@0 1006 if (shell.dataItem) {
michael@0 1007 shell.placesNode = null;
michael@0 1008 }
michael@0 1009 else {
michael@0 1010 this._removeElement(shell.element);
michael@0 1011 shellsForURI.delete(shell);
michael@0 1012 if (shellsForURI.size == 0)
michael@0 1013 this._downloadElementsShellsForURI.delete(downloadURI);
michael@0 1014 }
michael@0 1015 }
michael@0 1016 }
michael@0 1017 },
michael@0 1018
michael@0 1019 _removeSessionDownloadFromView:
michael@0 1020 function DPV__removeSessionDownloadFromView(aDataItem) {
michael@0 1021 let shells = this._downloadElementsShellsForURI.get(aDataItem.uri);
michael@0 1022 if (shells.size == 0)
michael@0 1023 throw new Error("Should have had at leaat one shell for this uri");
michael@0 1024
michael@0 1025 let shell = this.getViewItem(aDataItem);
michael@0 1026 if (!shells.has(shell))
michael@0 1027 throw new Error("Missing download element shell in shells list for url");
michael@0 1028
michael@0 1029 // If there's more than one item for this download uri, we can let the
michael@0 1030 // view item for this this particular data item go away.
michael@0 1031 // If there's only one item for this download uri, we should only
michael@0 1032 // keep it if it is associated with a history download.
michael@0 1033 if (shells.size > 1 || !shell.placesNode) {
michael@0 1034 this._removeElement(shell.element);
michael@0 1035 shells.delete(shell);
michael@0 1036 if (shells.size == 0)
michael@0 1037 this._downloadElementsShellsForURI.delete(aDataItem.uri);
michael@0 1038 }
michael@0 1039 else {
michael@0 1040 shell.dataItem = null;
michael@0 1041 // Move it below the session-download items;
michael@0 1042 if (this._lastSessionDownloadElement == shell.element) {
michael@0 1043 this._lastSessionDownloadElement = shell.element.previousSibling;
michael@0 1044 }
michael@0 1045 else {
michael@0 1046 let before = this._lastSessionDownloadElement ?
michael@0 1047 this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild;
michael@0 1048 this._richlistbox.insertBefore(shell.element, before);
michael@0 1049 }
michael@0 1050 }
michael@0 1051 },
michael@0 1052
michael@0 1053 _ensureVisibleElementsAreActive:
michael@0 1054 function DPV__ensureVisibleElementsAreActive() {
michael@0 1055 if (!this.active || this._ensureVisibleTimer || !this._richlistbox.firstChild)
michael@0 1056 return;
michael@0 1057
michael@0 1058 this._ensureVisibleTimer = setTimeout(function() {
michael@0 1059 delete this._ensureVisibleTimer;
michael@0 1060 if (!this._richlistbox.firstChild)
michael@0 1061 return;
michael@0 1062
michael@0 1063 let rlbRect = this._richlistbox.getBoundingClientRect();
michael@0 1064 let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 1065 .getInterface(Ci.nsIDOMWindowUtils);
michael@0 1066 let nodes = winUtils.nodesFromRect(rlbRect.left, rlbRect.top,
michael@0 1067 0, rlbRect.width, rlbRect.height, 0,
michael@0 1068 true, false);
michael@0 1069 // nodesFromRect returns nodes in z-index order, and for the same z-index
michael@0 1070 // sorts them in inverted DOM order, thus starting from the one that would
michael@0 1071 // be on top.
michael@0 1072 let firstVisibleNode, lastVisibleNode;
michael@0 1073 for (let node of nodes) {
michael@0 1074 if (node.localName === "richlistitem" && node._shell) {
michael@0 1075 node._shell.ensureActive();
michael@0 1076 // The first visible node is the last match.
michael@0 1077 firstVisibleNode = node;
michael@0 1078 // While the last visible node is the first match.
michael@0 1079 if (!lastVisibleNode)
michael@0 1080 lastVisibleNode = node;
michael@0 1081 }
michael@0 1082 }
michael@0 1083
michael@0 1084 // Also activate the first invisible nodes in both boundaries (that is,
michael@0 1085 // above and below the visible area) to ensure proper keyboard navigation
michael@0 1086 // in both directions.
michael@0 1087 let nodeBelowVisibleArea = lastVisibleNode && lastVisibleNode.nextSibling;
michael@0 1088 if (nodeBelowVisibleArea && nodeBelowVisibleArea._shell)
michael@0 1089 nodeBelowVisibleArea._shell.ensureActive();
michael@0 1090
michael@0 1091 let nodeABoveVisibleArea =
michael@0 1092 firstVisibleNode && firstVisibleNode.previousSibling;
michael@0 1093 if (nodeABoveVisibleArea && nodeABoveVisibleArea._shell)
michael@0 1094 nodeABoveVisibleArea._shell.ensureActive();
michael@0 1095 }.bind(this), 10);
michael@0 1096 },
michael@0 1097
michael@0 1098 _place: "",
michael@0 1099 get place() this._place,
michael@0 1100 set place(val) {
michael@0 1101 // Don't reload everything if we don't have to.
michael@0 1102 if (this._place == val) {
michael@0 1103 // XXXmano: places.js relies on this behavior (see Bug 822203).
michael@0 1104 this.searchTerm = "";
michael@0 1105 return val;
michael@0 1106 }
michael@0 1107
michael@0 1108 this._place = val;
michael@0 1109
michael@0 1110 let history = PlacesUtils.history;
michael@0 1111 let queries = { }, options = { };
michael@0 1112 history.queryStringToQueries(val, queries, { }, options);
michael@0 1113 if (!queries.value.length)
michael@0 1114 queries.value = [history.getNewQuery()];
michael@0 1115
michael@0 1116 let result = history.executeQueries(queries.value, queries.value.length,
michael@0 1117 options.value);
michael@0 1118 result.addObserver(this, false);
michael@0 1119 return val;
michael@0 1120 },
michael@0 1121
michael@0 1122 _result: null,
michael@0 1123 get result() this._result,
michael@0 1124 set result(val) {
michael@0 1125 if (this._result == val)
michael@0 1126 return val;
michael@0 1127
michael@0 1128 if (this._result) {
michael@0 1129 this._result.removeObserver(this);
michael@0 1130 this._resultNode.containerOpen = false;
michael@0 1131 }
michael@0 1132
michael@0 1133 if (val) {
michael@0 1134 this._result = val;
michael@0 1135 this._resultNode = val.root;
michael@0 1136 this._resultNode.containerOpen = true;
michael@0 1137 this._ensureInitialSelection();
michael@0 1138 }
michael@0 1139 else {
michael@0 1140 delete this._resultNode;
michael@0 1141 delete this._result;
michael@0 1142 }
michael@0 1143
michael@0 1144 return val;
michael@0 1145 },
michael@0 1146
michael@0 1147 get selectedNodes() {
michael@0 1148 let placesNodes = [];
michael@0 1149 let selectedElements = this._richlistbox.selectedItems;
michael@0 1150 for (let elt of selectedElements) {
michael@0 1151 if (elt._shell.placesNode)
michael@0 1152 placesNodes.push(elt._shell.placesNode);
michael@0 1153 }
michael@0 1154 return placesNodes;
michael@0 1155 },
michael@0 1156
michael@0 1157 get selectedNode() {
michael@0 1158 let selectedNodes = this.selectedNodes;
michael@0 1159 return selectedNodes.length == 1 ? selectedNodes[0] : null;
michael@0 1160 },
michael@0 1161
michael@0 1162 get hasSelection() this.selectedNodes.length > 0,
michael@0 1163
michael@0 1164 containerStateChanged:
michael@0 1165 function DPV_containerStateChanged(aNode, aOldState, aNewState) {
michael@0 1166 this.invalidateContainer(aNode)
michael@0 1167 },
michael@0 1168
michael@0 1169 invalidateContainer:
michael@0 1170 function DPV_invalidateContainer(aContainer) {
michael@0 1171 if (aContainer != this._resultNode)
michael@0 1172 throw new Error("Unexpected container node");
michael@0 1173 if (!aContainer.containerOpen)
michael@0 1174 throw new Error("Root container for the downloads query cannot be closed");
michael@0 1175
michael@0 1176 let suppressOnSelect = this._richlistbox.suppressOnSelect;
michael@0 1177 this._richlistbox.suppressOnSelect = true;
michael@0 1178 try {
michael@0 1179 // Remove the invalidated history downloads from the list and unset the
michael@0 1180 // places node for data downloads.
michael@0 1181 // Loop backwards since _removeHistoryDownloadFromView may removeChild().
michael@0 1182 for (let i = this._richlistbox.childNodes.length - 1; i >= 0; --i) {
michael@0 1183 let element = this._richlistbox.childNodes[i];
michael@0 1184 if (element._shell.placesNode)
michael@0 1185 this._removeHistoryDownloadFromView(element._shell.placesNode);
michael@0 1186 }
michael@0 1187 }
michael@0 1188 finally {
michael@0 1189 this._richlistbox.suppressOnSelect = suppressOnSelect;
michael@0 1190 }
michael@0 1191
michael@0 1192 if (aContainer.childCount > 0) {
michael@0 1193 let elementsToAppendFragment = document.createDocumentFragment();
michael@0 1194 for (let i = 0; i < aContainer.childCount; i++) {
michael@0 1195 try {
michael@0 1196 this._addDownloadData(null, aContainer.getChild(i), false,
michael@0 1197 elementsToAppendFragment);
michael@0 1198 }
michael@0 1199 catch(ex) {
michael@0 1200 Cu.reportError(ex);
michael@0 1201 }
michael@0 1202 }
michael@0 1203
michael@0 1204 // _addDownloadData may not add new elements if there were already
michael@0 1205 // data items in place.
michael@0 1206 if (elementsToAppendFragment.firstChild) {
michael@0 1207 this._appendDownloadsFragment(elementsToAppendFragment);
michael@0 1208 this._ensureVisibleElementsAreActive();
michael@0 1209 }
michael@0 1210 }
michael@0 1211
michael@0 1212 goUpdateDownloadCommands();
michael@0 1213 },
michael@0 1214
michael@0 1215 _appendDownloadsFragment: function DPV__appendDownloadsFragment(aDOMFragment) {
michael@0 1216 // Workaround multiple reflows hang by removing the richlistbox
michael@0 1217 // and adding it back when we're done.
michael@0 1218
michael@0 1219 // Hack for bug 836283: reset xbl fields to their old values after the
michael@0 1220 // binding is reattached to avoid breaking the selection state
michael@0 1221 let xblFields = new Map();
michael@0 1222 for (let [key, value] in Iterator(this._richlistbox)) {
michael@0 1223 xblFields.set(key, value);
michael@0 1224 }
michael@0 1225
michael@0 1226 let parentNode = this._richlistbox.parentNode;
michael@0 1227 let nextSibling = this._richlistbox.nextSibling;
michael@0 1228 parentNode.removeChild(this._richlistbox);
michael@0 1229 this._richlistbox.appendChild(aDOMFragment);
michael@0 1230 parentNode.insertBefore(this._richlistbox, nextSibling);
michael@0 1231
michael@0 1232 for (let [key, value] of xblFields) {
michael@0 1233 this._richlistbox[key] = value;
michael@0 1234 }
michael@0 1235 },
michael@0 1236
michael@0 1237 nodeInserted: function DPV_nodeInserted(aParent, aPlacesNode) {
michael@0 1238 this._addDownloadData(null, aPlacesNode);
michael@0 1239 },
michael@0 1240
michael@0 1241 nodeRemoved: function DPV_nodeRemoved(aParent, aPlacesNode, aOldIndex) {
michael@0 1242 this._removeHistoryDownloadFromView(aPlacesNode);
michael@0 1243 },
michael@0 1244
michael@0 1245 nodeIconChanged: function DPV_nodeIconChanged(aNode) {
michael@0 1246 this._forEachDownloadElementShellForURI(aNode.uri, function(aDownloadElementShell) {
michael@0 1247 aDownloadElementShell.placesNodeIconChanged();
michael@0 1248 });
michael@0 1249 },
michael@0 1250
michael@0 1251 nodeAnnotationChanged: function DPV_nodeAnnotationChanged(aNode, aAnnoName) {
michael@0 1252 this._forEachDownloadElementShellForURI(aNode.uri, function(aDownloadElementShell) {
michael@0 1253 aDownloadElementShell.placesNodeAnnotationChanged(aAnnoName);
michael@0 1254 });
michael@0 1255 },
michael@0 1256
michael@0 1257 nodeTitleChanged: function DPV_nodeTitleChanged(aNode, aNewTitle) {
michael@0 1258 this._forEachDownloadElementShellForURI(aNode.uri, function(aDownloadElementShell) {
michael@0 1259 aDownloadElementShell.placesNodeTitleChanged();
michael@0 1260 });
michael@0 1261 },
michael@0 1262
michael@0 1263 nodeKeywordChanged: function() {},
michael@0 1264 nodeDateAddedChanged: function() {},
michael@0 1265 nodeLastModifiedChanged: function() {},
michael@0 1266 nodeHistoryDetailsChanged: function() {},
michael@0 1267 nodeTagsChanged: function() {},
michael@0 1268 sortingChanged: function() {},
michael@0 1269 nodeMoved: function() {},
michael@0 1270 nodeURIChanged: function() {},
michael@0 1271 batching: function() {},
michael@0 1272
michael@0 1273 get controller() this._richlistbox.controller,
michael@0 1274
michael@0 1275 get searchTerm() this._searchTerm,
michael@0 1276 set searchTerm(aValue) {
michael@0 1277 if (this._searchTerm != aValue) {
michael@0 1278 for (let element of this._richlistbox.childNodes) {
michael@0 1279 element.hidden = !element._shell.matchesSearchTerm(aValue);
michael@0 1280 }
michael@0 1281 this._ensureVisibleElementsAreActive();
michael@0 1282 }
michael@0 1283 return this._searchTerm = aValue;
michael@0 1284 },
michael@0 1285
michael@0 1286 /**
michael@0 1287 * When the view loads, we want to select the first item.
michael@0 1288 * However, because session downloads, for which the data is loaded
michael@0 1289 * asynchronously, always come first in the list, and because the list
michael@0 1290 * may (or may not) already contain history downloads at that point, it
michael@0 1291 * turns out that by the time we can select the first item, the user may
michael@0 1292 * have already started using the view.
michael@0 1293 * To make things even more complicated, in other cases, the places data
michael@0 1294 * may be loaded after the session downloads data. Thus we cannot rely on
michael@0 1295 * the order in which the data comes in.
michael@0 1296 * We work around this by attempting to select the first element twice,
michael@0 1297 * once after the places data is loaded and once when the session downloads
michael@0 1298 * data is done loading. However, if the selection has changed in-between,
michael@0 1299 * we assume the user has already started using the view and give up.
michael@0 1300 */
michael@0 1301 _ensureInitialSelection: function DPV__ensureInitialSelection() {
michael@0 1302 // Either they're both null, or the selection has not changed in between.
michael@0 1303 if (this._richlistbox.selectedItem == this._initiallySelectedElement) {
michael@0 1304 let firstDownloadElement = this._richlistbox.firstChild;
michael@0 1305 if (firstDownloadElement != this._initiallySelectedElement) {
michael@0 1306 // We may be called before _ensureVisibleElementsAreActive,
michael@0 1307 // or before the download binding is attached. Therefore, ensure the
michael@0 1308 // first item is activated, and pass the item to the richlistbox
michael@0 1309 // setters only at a point we know for sure the binding is attached.
michael@0 1310 firstDownloadElement._shell.ensureActive();
michael@0 1311 Services.tm.mainThread.dispatch(function() {
michael@0 1312 this._richlistbox.selectedItem = firstDownloadElement;
michael@0 1313 this._richlistbox.currentItem = firstDownloadElement;
michael@0 1314 this._initiallySelectedElement = firstDownloadElement;
michael@0 1315 }.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
michael@0 1316 }
michael@0 1317 }
michael@0 1318 },
michael@0 1319
michael@0 1320 onDataLoadStarting: function() { },
michael@0 1321 onDataLoadCompleted: function DPV_onDataLoadCompleted() {
michael@0 1322 this._ensureInitialSelection();
michael@0 1323 },
michael@0 1324
michael@0 1325 onDataItemAdded: function DPV_onDataItemAdded(aDataItem, aNewest) {
michael@0 1326 this._addDownloadData(aDataItem, null, aNewest);
michael@0 1327 },
michael@0 1328
michael@0 1329 onDataItemRemoved: function DPV_onDataItemRemoved(aDataItem) {
michael@0 1330 this._removeSessionDownloadFromView(aDataItem);
michael@0 1331 },
michael@0 1332
michael@0 1333 getViewItem: function(aDataItem)
michael@0 1334 this._viewItemsForDataItems.get(aDataItem, null),
michael@0 1335
michael@0 1336 supportsCommand: function DPV_supportsCommand(aCommand) {
michael@0 1337 if (DOWNLOAD_VIEW_SUPPORTED_COMMANDS.indexOf(aCommand) != -1) {
michael@0 1338 // The clear-downloads command may be performed by the toolbar-button,
michael@0 1339 // which can be focused on OS X. Thus enable this command even if the
michael@0 1340 // richlistbox is not focused.
michael@0 1341 // For other commands, be prudent and disable them unless the richlistview
michael@0 1342 // is focused. It's important to make the decision here rather than in
michael@0 1343 // isCommandEnabled. Otherwise our controller may "steal" commands from
michael@0 1344 // other controls in the window (see goUpdateCommand &
michael@0 1345 // getControllerForCommand).
michael@0 1346 if (document.activeElement == this._richlistbox ||
michael@0 1347 aCommand == "downloadsCmd_clearDownloads") {
michael@0 1348 return true;
michael@0 1349 }
michael@0 1350 }
michael@0 1351 return false;
michael@0 1352 },
michael@0 1353
michael@0 1354 isCommandEnabled: function DPV_isCommandEnabled(aCommand) {
michael@0 1355 switch (aCommand) {
michael@0 1356 case "cmd_copy":
michael@0 1357 return this._richlistbox.selectedItems.length > 0;
michael@0 1358 case "cmd_selectAll":
michael@0 1359 return true;
michael@0 1360 case "cmd_paste":
michael@0 1361 return this._canDownloadClipboardURL();
michael@0 1362 case "downloadsCmd_clearDownloads":
michael@0 1363 return this._canClearDownloads();
michael@0 1364 default:
michael@0 1365 return Array.every(this._richlistbox.selectedItems, function(element) {
michael@0 1366 return element._shell.isCommandEnabled(aCommand);
michael@0 1367 });
michael@0 1368 }
michael@0 1369 },
michael@0 1370
michael@0 1371 _canClearDownloads: function DPV__canClearDownloads() {
michael@0 1372 // Downloads can be cleared if there's at least one removeable download in
michael@0 1373 // the list (either a history download or a completed session download).
michael@0 1374 // Because history downloads are always removable and are listed after the
michael@0 1375 // session downloads, check from bottom to top.
michael@0 1376 for (let elt = this._richlistbox.lastChild; elt; elt = elt.previousSibling) {
michael@0 1377 if (elt._shell.placesNode || !elt._shell.dataItem.inProgress)
michael@0 1378 return true;
michael@0 1379 }
michael@0 1380 return false;
michael@0 1381 },
michael@0 1382
michael@0 1383 _copySelectedDownloadsToClipboard:
michael@0 1384 function DPV__copySelectedDownloadsToClipboard() {
michael@0 1385 let selectedElements = this._richlistbox.selectedItems;
michael@0 1386 let urls = [e._shell.downloadURI for each (e in selectedElements)];
michael@0 1387
michael@0 1388 Cc["@mozilla.org/widget/clipboardhelper;1"].
michael@0 1389 getService(Ci.nsIClipboardHelper).copyString(urls.join("\n"), document);
michael@0 1390 },
michael@0 1391
michael@0 1392 _getURLFromClipboardData: function DPV__getURLFromClipboardData() {
michael@0 1393 let trans = Cc["@mozilla.org/widget/transferable;1"].
michael@0 1394 createInstance(Ci.nsITransferable);
michael@0 1395 trans.init(null);
michael@0 1396
michael@0 1397 let flavors = ["text/x-moz-url", "text/unicode"];
michael@0 1398 flavors.forEach(trans.addDataFlavor);
michael@0 1399
michael@0 1400 Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard);
michael@0 1401
michael@0 1402 // Getting the data or creating the nsIURI might fail.
michael@0 1403 try {
michael@0 1404 let data = {};
michael@0 1405 trans.getAnyTransferData({}, data, {});
michael@0 1406 let [url, name] = data.value.QueryInterface(Ci.nsISupportsString)
michael@0 1407 .data.split("\n");
michael@0 1408 if (url)
michael@0 1409 return [NetUtil.newURI(url, null, null).spec, name];
michael@0 1410 }
michael@0 1411 catch(ex) { }
michael@0 1412
michael@0 1413 return ["", ""];
michael@0 1414 },
michael@0 1415
michael@0 1416 _canDownloadClipboardURL: function DPV__canDownloadClipboardURL() {
michael@0 1417 let [url, name] = this._getURLFromClipboardData();
michael@0 1418 return url != "";
michael@0 1419 },
michael@0 1420
michael@0 1421 _downloadURLFromClipboard: function DPV__downloadURLFromClipboard() {
michael@0 1422 let [url, name] = this._getURLFromClipboardData();
michael@0 1423 let browserWin = RecentWindow.getMostRecentBrowserWindow();
michael@0 1424 let initiatingDoc = browserWin ? browserWin.document : document;
michael@0 1425 DownloadURL(url, name, initiatingDoc);
michael@0 1426 },
michael@0 1427
michael@0 1428 doCommand: function DPV_doCommand(aCommand) {
michael@0 1429 switch (aCommand) {
michael@0 1430 case "cmd_copy":
michael@0 1431 this._copySelectedDownloadsToClipboard();
michael@0 1432 break;
michael@0 1433 case "cmd_selectAll":
michael@0 1434 this._richlistbox.selectAll();
michael@0 1435 break;
michael@0 1436 case "cmd_paste":
michael@0 1437 this._downloadURLFromClipboard();
michael@0 1438 break;
michael@0 1439 case "downloadsCmd_clearDownloads":
michael@0 1440 this._downloadsData.removeFinished();
michael@0 1441 if (this.result) {
michael@0 1442 Cc["@mozilla.org/browser/download-history;1"]
michael@0 1443 .getService(Ci.nsIDownloadHistory)
michael@0 1444 .removeAllDownloads();
michael@0 1445 }
michael@0 1446 // There may be no selection or focus change as a result
michael@0 1447 // of these change, and we want the command updated immediately.
michael@0 1448 goUpdateCommand("downloadsCmd_clearDownloads");
michael@0 1449 break;
michael@0 1450 default: {
michael@0 1451 // Slicing the array to get a freezed list of selected items. Otherwise,
michael@0 1452 // the selectedItems array is live and doCommand may alter the selection
michael@0 1453 // while we are trying to do one particular action, like removing items
michael@0 1454 // from history.
michael@0 1455 let selectedElements = this._richlistbox.selectedItems.slice();
michael@0 1456 for (let element of selectedElements) {
michael@0 1457 element._shell.doCommand(aCommand);
michael@0 1458 }
michael@0 1459 }
michael@0 1460 }
michael@0 1461 },
michael@0 1462
michael@0 1463 onEvent: function() { },
michael@0 1464
michael@0 1465 onContextMenu: function DPV_onContextMenu(aEvent)
michael@0 1466 {
michael@0 1467 let element = this._richlistbox.selectedItem;
michael@0 1468 if (!element || !element._shell)
michael@0 1469 return false;
michael@0 1470
michael@0 1471 // Set the state attribute so that only the appropriate items are displayed.
michael@0 1472 let contextMenu = document.getElementById("downloadsContextMenu");
michael@0 1473 let state = element._shell.getDownloadMetaData().state;
michael@0 1474 if (state !== undefined)
michael@0 1475 contextMenu.setAttribute("state", state);
michael@0 1476 else
michael@0 1477 contextMenu.removeAttribute("state");
michael@0 1478
michael@0 1479 if (state == nsIDM.DOWNLOAD_DOWNLOADING) {
michael@0 1480 // The resumable property of a download may change at any time, so
michael@0 1481 // ensure we update the related command now.
michael@0 1482 goUpdateCommand("downloadsCmd_pauseResume");
michael@0 1483 }
michael@0 1484 return true;
michael@0 1485 },
michael@0 1486
michael@0 1487 onKeyPress: function DPV_onKeyPress(aEvent) {
michael@0 1488 let selectedElements = this._richlistbox.selectedItems;
michael@0 1489 if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
michael@0 1490 // In the content tree, opening bookmarks by pressing return is only
michael@0 1491 // supported when a single item is selected. To be consistent, do the
michael@0 1492 // same here.
michael@0 1493 if (selectedElements.length == 1) {
michael@0 1494 let element = selectedElements[0];
michael@0 1495 if (element._shell)
michael@0 1496 element._shell.doDefaultCommand();
michael@0 1497 }
michael@0 1498 }
michael@0 1499 else if (aEvent.charCode == " ".charCodeAt(0)) {
michael@0 1500 // Pausue/Resume every selected download
michael@0 1501 for (let element of selectedElements) {
michael@0 1502 if (element._shell.isCommandEnabled("downloadsCmd_pauseResume"))
michael@0 1503 element._shell.doCommand("downloadsCmd_pauseResume");
michael@0 1504 }
michael@0 1505 }
michael@0 1506 },
michael@0 1507
michael@0 1508 onDoubleClick: function DPV_onDoubleClick(aEvent) {
michael@0 1509 if (aEvent.button != 0)
michael@0 1510 return;
michael@0 1511
michael@0 1512 let selectedElements = this._richlistbox.selectedItems;
michael@0 1513 if (selectedElements.length != 1)
michael@0 1514 return;
michael@0 1515
michael@0 1516 let element = selectedElements[0];
michael@0 1517 if (element._shell)
michael@0 1518 element._shell.doDefaultCommand();
michael@0 1519 },
michael@0 1520
michael@0 1521 onScroll: function DPV_onScroll() {
michael@0 1522 this._ensureVisibleElementsAreActive();
michael@0 1523 },
michael@0 1524
michael@0 1525 onSelect: function DPV_onSelect() {
michael@0 1526 goUpdateDownloadCommands();
michael@0 1527
michael@0 1528 let selectedElements = this._richlistbox.selectedItems;
michael@0 1529 for (let elt of selectedElements) {
michael@0 1530 if (elt._shell)
michael@0 1531 elt._shell.onSelect();
michael@0 1532 }
michael@0 1533 },
michael@0 1534
michael@0 1535 onDragStart: function DPV_onDragStart(aEvent) {
michael@0 1536 // TODO Bug 831358: Support d&d for multiple selection.
michael@0 1537 // For now, we just drag the first element.
michael@0 1538 let selectedItem = this._richlistbox.selectedItem;
michael@0 1539 if (!selectedItem)
michael@0 1540 return;
michael@0 1541
michael@0 1542 let metaData = selectedItem._shell.getDownloadMetaData();
michael@0 1543 if (!("filePath" in metaData))
michael@0 1544 return;
michael@0 1545 let file = new FileUtils.File(metaData.filePath);
michael@0 1546 if (!file.exists())
michael@0 1547 return;
michael@0 1548
michael@0 1549 let dt = aEvent.dataTransfer;
michael@0 1550 dt.mozSetDataAt("application/x-moz-file", file, 0);
michael@0 1551 let url = Services.io.newFileURI(file).spec;
michael@0 1552 dt.setData("text/uri-list", url);
michael@0 1553 dt.setData("text/plain", url);
michael@0 1554 dt.effectAllowed = "copyMove";
michael@0 1555 dt.addElement(selectedItem);
michael@0 1556 },
michael@0 1557
michael@0 1558 onDragOver: function DPV_onDragOver(aEvent) {
michael@0 1559 let types = aEvent.dataTransfer.types;
michael@0 1560 if (types.contains("text/uri-list") ||
michael@0 1561 types.contains("text/x-moz-url") ||
michael@0 1562 types.contains("text/plain")) {
michael@0 1563 aEvent.preventDefault();
michael@0 1564 }
michael@0 1565 },
michael@0 1566
michael@0 1567 onDrop: function DPV_onDrop(aEvent) {
michael@0 1568 let dt = aEvent.dataTransfer;
michael@0 1569 // If dragged item is from our source, do not try to
michael@0 1570 // redownload already downloaded file.
michael@0 1571 if (dt.mozGetDataAt("application/x-moz-file", 0))
michael@0 1572 return;
michael@0 1573
michael@0 1574 let name = { };
michael@0 1575 let url = Services.droppedLinkHandler.dropLink(aEvent, name);
michael@0 1576 if (url) {
michael@0 1577 let browserWin = RecentWindow.getMostRecentBrowserWindow();
michael@0 1578 let initiatingDoc = browserWin ? browserWin.document : document;
michael@0 1579 DownloadURL(url, name.value, initiatingDoc);
michael@0 1580 }
michael@0 1581 }
michael@0 1582 };
michael@0 1583
michael@0 1584 for (let methodName of ["load", "applyFilter", "selectNode", "selectItems"]) {
michael@0 1585 DownloadsPlacesView.prototype[methodName] = function() {
michael@0 1586 throw new Error("|" + methodName + "| is not implemented by the downloads view.");
michael@0 1587 }
michael@0 1588 }
michael@0 1589
michael@0 1590 function goUpdateDownloadCommands() {
michael@0 1591 for (let command of DOWNLOAD_VIEW_SUPPORTED_COMMANDS) {
michael@0 1592 goUpdateCommand(command);
michael@0 1593 }
michael@0 1594 }

mercurial