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 +}