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