browser/components/downloads/content/downloads.js

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

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

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

michael@0 1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* vim: set ts=2 et sw=2 tw=80: */
michael@0 3 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6
michael@0 7 /**
michael@0 8 * Handles the Downloads panel user interface for each browser window.
michael@0 9 *
michael@0 10 * This file includes the following constructors and global objects:
michael@0 11 *
michael@0 12 * DownloadsPanel
michael@0 13 * Main entry point for the downloads panel interface.
michael@0 14 *
michael@0 15 * DownloadsOverlayLoader
michael@0 16 * Allows loading the downloads panel and the status indicator interfaces on
michael@0 17 * demand, to improve startup performance.
michael@0 18 *
michael@0 19 * DownloadsView
michael@0 20 * Builds and updates the downloads list widget, responding to changes in the
michael@0 21 * download state and real-time data. In addition, handles part of the user
michael@0 22 * interaction events raised by the downloads list widget.
michael@0 23 *
michael@0 24 * DownloadsViewItem
michael@0 25 * Builds and updates a single item in the downloads list widget, responding to
michael@0 26 * changes in the download state and real-time data.
michael@0 27 *
michael@0 28 * DownloadsViewController
michael@0 29 * Handles part of the user interaction events raised by the downloads list
michael@0 30 * widget, in particular the "commands" that apply to multiple items, and
michael@0 31 * dispatches the commands that apply to individual items.
michael@0 32 *
michael@0 33 * DownloadsViewItemController
michael@0 34 * Handles all the user interaction events, in particular the "commands",
michael@0 35 * related to a single item in the downloads list widgets.
michael@0 36 */
michael@0 37
michael@0 38 /**
michael@0 39 * A few words on focus and focusrings
michael@0 40 *
michael@0 41 * We do quite a few hacks in the Downloads Panel for focusrings. In fact, we
michael@0 42 * basically suppress most if not all XUL-level focusrings, and style/draw
michael@0 43 * them ourselves (using :focus instead of -moz-focusring). There are a few
michael@0 44 * reasons for this:
michael@0 45 *
michael@0 46 * 1) Richlists on OSX don't have focusrings; instead, they are shown as
michael@0 47 * selected. This makes for some ambiguity when we have a focused/selected
michael@0 48 * item in the list, and the mouse is hovering a completed download (which
michael@0 49 * highlights).
michael@0 50 * 2) Windows doesn't show focusrings until after the first time that tab is
michael@0 51 * pressed (and by then you're focusing the second item in the panel).
michael@0 52 * 3) Richlistbox sets -moz-focusring even when we select it with a mouse.
michael@0 53 *
michael@0 54 * In general, the desired behaviour is to focus the first item after pressing
michael@0 55 * tab/down, and show that focus with a ring. Then, if the mouse moves over
michael@0 56 * the panel, to hide that focus ring; essentially resetting us to the state
michael@0 57 * before pressing the key.
michael@0 58 *
michael@0 59 * We end up capturing the tab/down key events, and preventing their default
michael@0 60 * behaviour. We then set a "keyfocus" attribute on the panel, which allows
michael@0 61 * us to draw a ring around the currently focused element. If the panel is
michael@0 62 * closed or the mouse moves over the panel, we remove the attribute.
michael@0 63 */
michael@0 64
michael@0 65 "use strict";
michael@0 66
michael@0 67 ////////////////////////////////////////////////////////////////////////////////
michael@0 68 //// Globals
michael@0 69
michael@0 70 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
michael@0 71 "resource://gre/modules/DownloadUtils.jsm");
michael@0 72 XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
michael@0 73 "resource:///modules/DownloadsCommon.jsm");
michael@0 74 XPCOMUtils.defineLazyModuleGetter(this, "OS",
michael@0 75 "resource://gre/modules/osfile.jsm");
michael@0 76 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
michael@0 77 "resource://gre/modules/PrivateBrowsingUtils.jsm");
michael@0 78 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
michael@0 79 "resource://gre/modules/PlacesUtils.jsm");
michael@0 80 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
michael@0 81 "resource://gre/modules/NetUtil.jsm");
michael@0 82
michael@0 83 ////////////////////////////////////////////////////////////////////////////////
michael@0 84 //// DownloadsPanel
michael@0 85
michael@0 86 /**
michael@0 87 * Main entry point for the downloads panel interface.
michael@0 88 */
michael@0 89 const DownloadsPanel = {
michael@0 90 //////////////////////////////////////////////////////////////////////////////
michael@0 91 //// Initialization and termination
michael@0 92
michael@0 93 /**
michael@0 94 * Internal state of the downloads panel, based on one of the kState
michael@0 95 * constants. This is not the same state as the XUL panel element.
michael@0 96 */
michael@0 97 _state: 0,
michael@0 98
michael@0 99 /** The panel is not linked to downloads data yet. */
michael@0 100 get kStateUninitialized() 0,
michael@0 101 /** This object is linked to data, but the panel is invisible. */
michael@0 102 get kStateHidden() 1,
michael@0 103 /** The panel will be shown as soon as possible. */
michael@0 104 get kStateWaitingData() 2,
michael@0 105 /** The panel is almost shown - we're just waiting to get a handle on the
michael@0 106 anchor. */
michael@0 107 get kStateWaitingAnchor() 3,
michael@0 108 /** The panel is open. */
michael@0 109 get kStateShown() 4,
michael@0 110
michael@0 111 /**
michael@0 112 * Location of the panel overlay.
michael@0 113 */
michael@0 114 get kDownloadsOverlay()
michael@0 115 "chrome://browser/content/downloads/downloadsOverlay.xul",
michael@0 116
michael@0 117 /**
michael@0 118 * Starts loading the download data in background, without opening the panel.
michael@0 119 * Use showPanel instead to load the data and open the panel at the same time.
michael@0 120 *
michael@0 121 * @param aCallback
michael@0 122 * Called when initialization is complete.
michael@0 123 */
michael@0 124 initialize: function DP_initialize(aCallback)
michael@0 125 {
michael@0 126 DownloadsCommon.log("Attempting to initialize DownloadsPanel for a window.");
michael@0 127 if (this._state != this.kStateUninitialized) {
michael@0 128 DownloadsCommon.log("DownloadsPanel is already initialized.");
michael@0 129 DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay,
michael@0 130 aCallback);
michael@0 131 return;
michael@0 132 }
michael@0 133 this._state = this.kStateHidden;
michael@0 134
michael@0 135 window.addEventListener("unload", this.onWindowUnload, false);
michael@0 136
michael@0 137 // Load and resume active downloads if required. If there are downloads to
michael@0 138 // be shown in the panel, they will be loaded asynchronously.
michael@0 139 DownloadsCommon.initializeAllDataLinks();
michael@0 140
michael@0 141 // Now that data loading has eventually started, load the required XUL
michael@0 142 // elements and initialize our views.
michael@0 143 DownloadsCommon.log("Ensuring DownloadsPanel overlay loaded.");
michael@0 144 DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay,
michael@0 145 function DP_I_callback() {
michael@0 146 DownloadsViewController.initialize();
michael@0 147 DownloadsCommon.log("Attaching DownloadsView...");
michael@0 148 DownloadsCommon.getData(window).addView(DownloadsView);
michael@0 149 DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit)
michael@0 150 .addView(DownloadsSummary);
michael@0 151 DownloadsCommon.log("DownloadsView attached - the panel for this window",
michael@0 152 "should now see download items come in.");
michael@0 153 DownloadsPanel._attachEventListeners();
michael@0 154 DownloadsCommon.log("DownloadsPanel initialized.");
michael@0 155 aCallback();
michael@0 156 });
michael@0 157 },
michael@0 158
michael@0 159 /**
michael@0 160 * Closes the downloads panel and frees the internal resources related to the
michael@0 161 * downloads. The downloads panel can be reopened later, even after this
michael@0 162 * function has been called.
michael@0 163 */
michael@0 164 terminate: function DP_terminate()
michael@0 165 {
michael@0 166 DownloadsCommon.log("Attempting to terminate DownloadsPanel for a window.");
michael@0 167 if (this._state == this.kStateUninitialized) {
michael@0 168 DownloadsCommon.log("DownloadsPanel was never initialized. Nothing to do.");
michael@0 169 return;
michael@0 170 }
michael@0 171
michael@0 172 window.removeEventListener("unload", this.onWindowUnload, false);
michael@0 173
michael@0 174 // Ensure that the panel is closed before shutting down.
michael@0 175 this.hidePanel();
michael@0 176
michael@0 177 DownloadsViewController.terminate();
michael@0 178 DownloadsCommon.getData(window).removeView(DownloadsView);
michael@0 179 DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit)
michael@0 180 .removeView(DownloadsSummary);
michael@0 181 this._unattachEventListeners();
michael@0 182
michael@0 183 this._state = this.kStateUninitialized;
michael@0 184
michael@0 185 DownloadsSummary.active = false;
michael@0 186 DownloadsCommon.log("DownloadsPanel terminated.");
michael@0 187 },
michael@0 188
michael@0 189 //////////////////////////////////////////////////////////////////////////////
michael@0 190 //// Panel interface
michael@0 191
michael@0 192 /**
michael@0 193 * Main panel element in the browser window, or null if the panel overlay
michael@0 194 * hasn't been loaded yet.
michael@0 195 */
michael@0 196 get panel()
michael@0 197 {
michael@0 198 // If the downloads panel overlay hasn't loaded yet, just return null
michael@0 199 // without reseting this.panel.
michael@0 200 let downloadsPanel = document.getElementById("downloadsPanel");
michael@0 201 if (!downloadsPanel)
michael@0 202 return null;
michael@0 203
michael@0 204 delete this.panel;
michael@0 205 return this.panel = downloadsPanel;
michael@0 206 },
michael@0 207
michael@0 208 /**
michael@0 209 * Starts opening the downloads panel interface, anchored to the downloads
michael@0 210 * button of the browser window. The list of downloads to display is
michael@0 211 * initialized the first time this method is called, and the panel is shown
michael@0 212 * only when data is ready.
michael@0 213 */
michael@0 214 showPanel: function DP_showPanel()
michael@0 215 {
michael@0 216 DownloadsCommon.log("Opening the downloads panel.");
michael@0 217
michael@0 218 if (this.isPanelShowing) {
michael@0 219 DownloadsCommon.log("Panel is already showing - focusing instead.");
michael@0 220 this._focusPanel();
michael@0 221 return;
michael@0 222 }
michael@0 223
michael@0 224 this.initialize(function DP_SP_callback() {
michael@0 225 // Delay displaying the panel because this function will sometimes be
michael@0 226 // called while another window is closing (like the window for selecting
michael@0 227 // whether to save or open the file), and that would cause the panel to
michael@0 228 // close immediately.
michael@0 229 setTimeout(function () DownloadsPanel._openPopupIfDataReady(), 0);
michael@0 230 }.bind(this));
michael@0 231
michael@0 232 DownloadsCommon.log("Waiting for the downloads panel to appear.");
michael@0 233 this._state = this.kStateWaitingData;
michael@0 234 },
michael@0 235
michael@0 236 /**
michael@0 237 * Hides the downloads panel, if visible, but keeps the internal state so that
michael@0 238 * the panel can be reopened quickly if required.
michael@0 239 */
michael@0 240 hidePanel: function DP_hidePanel()
michael@0 241 {
michael@0 242 DownloadsCommon.log("Closing the downloads panel.");
michael@0 243
michael@0 244 if (!this.isPanelShowing) {
michael@0 245 DownloadsCommon.log("Downloads panel is not showing - nothing to do.");
michael@0 246 return;
michael@0 247 }
michael@0 248
michael@0 249 this.panel.hidePopup();
michael@0 250
michael@0 251 // Ensure that we allow the panel to be reopened. Note that, if the popup
michael@0 252 // was open, then the onPopupHidden event handler has already updated the
michael@0 253 // current state, otherwise we must update the state ourselves.
michael@0 254 this._state = this.kStateHidden;
michael@0 255 DownloadsCommon.log("Downloads panel is now closed.");
michael@0 256 },
michael@0 257
michael@0 258 /**
michael@0 259 * Indicates whether the panel is shown or will be shown.
michael@0 260 */
michael@0 261 get isPanelShowing()
michael@0 262 {
michael@0 263 return this._state == this.kStateWaitingData ||
michael@0 264 this._state == this.kStateWaitingAnchor ||
michael@0 265 this._state == this.kStateShown;
michael@0 266 },
michael@0 267
michael@0 268 /**
michael@0 269 * Returns whether the user has started keyboard navigation.
michael@0 270 */
michael@0 271 get keyFocusing()
michael@0 272 {
michael@0 273 return this.panel.hasAttribute("keyfocus");
michael@0 274 },
michael@0 275
michael@0 276 /**
michael@0 277 * Set to true if the user has started keyboard navigation, and we should be
michael@0 278 * showing focusrings in the panel. Also adds a mousemove event handler to
michael@0 279 * the panel which disables keyFocusing.
michael@0 280 */
michael@0 281 set keyFocusing(aValue)
michael@0 282 {
michael@0 283 if (aValue) {
michael@0 284 this.panel.setAttribute("keyfocus", "true");
michael@0 285 this.panel.addEventListener("mousemove", this);
michael@0 286 } else {
michael@0 287 this.panel.removeAttribute("keyfocus");
michael@0 288 this.panel.removeEventListener("mousemove", this);
michael@0 289 }
michael@0 290 return aValue;
michael@0 291 },
michael@0 292
michael@0 293 /**
michael@0 294 * Handles the mousemove event for the panel, which disables focusring
michael@0 295 * visualization.
michael@0 296 */
michael@0 297 handleEvent: function DP_handleEvent(aEvent)
michael@0 298 {
michael@0 299 if (aEvent.type == "mousemove") {
michael@0 300 this.keyFocusing = false;
michael@0 301 }
michael@0 302 },
michael@0 303
michael@0 304 //////////////////////////////////////////////////////////////////////////////
michael@0 305 //// Callback functions from DownloadsView
michael@0 306
michael@0 307 /**
michael@0 308 * Called after data loading finished.
michael@0 309 */
michael@0 310 onViewLoadCompleted: function DP_onViewLoadCompleted()
michael@0 311 {
michael@0 312 this._openPopupIfDataReady();
michael@0 313 },
michael@0 314
michael@0 315 //////////////////////////////////////////////////////////////////////////////
michael@0 316 //// User interface event functions
michael@0 317
michael@0 318 onWindowUnload: function DP_onWindowUnload()
michael@0 319 {
michael@0 320 // This function is registered as an event listener, we can't use "this".
michael@0 321 DownloadsPanel.terminate();
michael@0 322 },
michael@0 323
michael@0 324 onPopupShown: function DP_onPopupShown(aEvent)
michael@0 325 {
michael@0 326 // Ignore events raised by nested popups.
michael@0 327 if (aEvent.target != aEvent.currentTarget) {
michael@0 328 return;
michael@0 329 }
michael@0 330
michael@0 331 DownloadsCommon.log("Downloads panel has shown.");
michael@0 332 this._state = this.kStateShown;
michael@0 333
michael@0 334 // Since at most one popup is open at any given time, we can set globally.
michael@0 335 DownloadsCommon.getIndicatorData(window).attentionSuppressed = true;
michael@0 336
michael@0 337 // Ensure that the first item is selected when the panel is focused.
michael@0 338 if (DownloadsView.richListBox.itemCount > 0 &&
michael@0 339 DownloadsView.richListBox.selectedIndex == -1) {
michael@0 340 DownloadsView.richListBox.selectedIndex = 0;
michael@0 341 }
michael@0 342
michael@0 343 this._focusPanel();
michael@0 344 },
michael@0 345
michael@0 346 onPopupHidden: function DP_onPopupHidden(aEvent)
michael@0 347 {
michael@0 348 // Ignore events raised by nested popups.
michael@0 349 if (aEvent.target != aEvent.currentTarget) {
michael@0 350 return;
michael@0 351 }
michael@0 352
michael@0 353 DownloadsCommon.log("Downloads panel has hidden.");
michael@0 354
michael@0 355 // Removes the keyfocus attribute so that we stop handling keyboard
michael@0 356 // navigation.
michael@0 357 this.keyFocusing = false;
michael@0 358
michael@0 359 // Since at most one popup is open at any given time, we can set globally.
michael@0 360 DownloadsCommon.getIndicatorData(window).attentionSuppressed = false;
michael@0 361
michael@0 362 // Allow the anchor to be hidden.
michael@0 363 DownloadsButton.releaseAnchor();
michael@0 364
michael@0 365 // Allow the panel to be reopened.
michael@0 366 this._state = this.kStateHidden;
michael@0 367 },
michael@0 368
michael@0 369 //////////////////////////////////////////////////////////////////////////////
michael@0 370 //// Related operations
michael@0 371
michael@0 372 /**
michael@0 373 * Shows or focuses the user interface dedicated to downloads history.
michael@0 374 */
michael@0 375 showDownloadsHistory: function DP_showDownloadsHistory()
michael@0 376 {
michael@0 377 DownloadsCommon.log("Showing download history.");
michael@0 378 // Hide the panel before showing another window, otherwise focus will return
michael@0 379 // to the browser window when the panel closes automatically.
michael@0 380 this.hidePanel();
michael@0 381
michael@0 382 BrowserDownloadsUI();
michael@0 383 },
michael@0 384
michael@0 385 //////////////////////////////////////////////////////////////////////////////
michael@0 386 //// Internal functions
michael@0 387
michael@0 388 /**
michael@0 389 * Attach event listeners to a panel element. These listeners should be
michael@0 390 * removed in _unattachEventListeners. This is called automatically after the
michael@0 391 * panel has successfully loaded.
michael@0 392 */
michael@0 393 _attachEventListeners: function DP__attachEventListeners()
michael@0 394 {
michael@0 395 // Handle keydown to support accel-V.
michael@0 396 this.panel.addEventListener("keydown", this._onKeyDown.bind(this), false);
michael@0 397 // Handle keypress to be able to preventDefault() events before they reach
michael@0 398 // the richlistbox, for keyboard navigation.
michael@0 399 this.panel.addEventListener("keypress", this._onKeyPress.bind(this), false);
michael@0 400 },
michael@0 401
michael@0 402 /**
michael@0 403 * Unattach event listeners that were added in _attachEventListeners. This
michael@0 404 * is called automatically on panel termination.
michael@0 405 */
michael@0 406 _unattachEventListeners: function DP__unattachEventListeners()
michael@0 407 {
michael@0 408 this.panel.removeEventListener("keydown", this._onKeyDown.bind(this),
michael@0 409 false);
michael@0 410 this.panel.removeEventListener("keypress", this._onKeyPress.bind(this),
michael@0 411 false);
michael@0 412 },
michael@0 413
michael@0 414 _onKeyPress: function DP__onKeyPress(aEvent)
michael@0 415 {
michael@0 416 // Handle unmodified keys only.
michael@0 417 if (aEvent.altKey || aEvent.ctrlKey || aEvent.shiftKey || aEvent.metaKey) {
michael@0 418 return;
michael@0 419 }
michael@0 420
michael@0 421 let richListBox = DownloadsView.richListBox;
michael@0 422
michael@0 423 // If the user has pressed the tab, up, or down cursor key, start keyboard
michael@0 424 // navigation, thus enabling focusrings in the panel. Keyboard navigation
michael@0 425 // is automatically disabled if the user moves the mouse on the panel, or
michael@0 426 // if the panel is closed.
michael@0 427 if ((aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_TAB ||
michael@0 428 aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP ||
michael@0 429 aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) &&
michael@0 430 !this.keyFocusing) {
michael@0 431 this.keyFocusing = true;
michael@0 432 // Ensure there's a selection, we will show the focus ring around it and
michael@0 433 // prevent the richlistbox from changing the selection.
michael@0 434 if (DownloadsView.richListBox.selectedIndex == -1)
michael@0 435 DownloadsView.richListBox.selectedIndex = 0;
michael@0 436 aEvent.preventDefault();
michael@0 437 return;
michael@0 438 }
michael@0 439
michael@0 440 if (aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) {
michael@0 441 // If the last element in the list is selected, or the footer is already
michael@0 442 // focused, focus the footer.
michael@0 443 if (richListBox.selectedItem === richListBox.lastChild ||
michael@0 444 document.activeElement.parentNode.id === "downloadsFooter") {
michael@0 445 DownloadsFooter.focus();
michael@0 446 aEvent.preventDefault();
michael@0 447 return;
michael@0 448 }
michael@0 449 }
michael@0 450
michael@0 451 // Pass keypress events to the richlistbox view when it's focused.
michael@0 452 if (document.activeElement === richListBox) {
michael@0 453 DownloadsView.onDownloadKeyPress(aEvent);
michael@0 454 }
michael@0 455 },
michael@0 456
michael@0 457 /**
michael@0 458 * Keydown listener that listens for the keys to start key focusing, as well
michael@0 459 * as the the accel-V "paste" event, which initiates a file download if the
michael@0 460 * pasted item can be resolved to a URI.
michael@0 461 */
michael@0 462 _onKeyDown: function DP__onKeyDown(aEvent)
michael@0 463 {
michael@0 464 // If the footer is focused and the downloads list has at least 1 element
michael@0 465 // in it, focus the last element in the list when going up.
michael@0 466 if (aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP &&
michael@0 467 document.activeElement.parentNode.id === "downloadsFooter" &&
michael@0 468 DownloadsView.richListBox.firstChild) {
michael@0 469 DownloadsView.richListBox.focus();
michael@0 470 DownloadsView.richListBox.selectedItem = DownloadsView.richListBox.lastChild;
michael@0 471 aEvent.preventDefault();
michael@0 472 return;
michael@0 473 }
michael@0 474
michael@0 475 let pasting = aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_V &&
michael@0 476 #ifdef XP_MACOSX
michael@0 477 aEvent.metaKey;
michael@0 478 #else
michael@0 479 aEvent.ctrlKey;
michael@0 480 #endif
michael@0 481
michael@0 482 if (!pasting) {
michael@0 483 return;
michael@0 484 }
michael@0 485
michael@0 486 DownloadsCommon.log("Received a paste event.");
michael@0 487
michael@0 488 let trans = Cc["@mozilla.org/widget/transferable;1"]
michael@0 489 .createInstance(Ci.nsITransferable);
michael@0 490 trans.init(null);
michael@0 491 let flavors = ["text/x-moz-url", "text/unicode"];
michael@0 492 flavors.forEach(trans.addDataFlavor);
michael@0 493 Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard);
michael@0 494 // Getting the data or creating the nsIURI might fail
michael@0 495 try {
michael@0 496 let data = {};
michael@0 497 trans.getAnyTransferData({}, data, {});
michael@0 498 let [url, name] = data.value
michael@0 499 .QueryInterface(Ci.nsISupportsString)
michael@0 500 .data
michael@0 501 .split("\n");
michael@0 502 if (!url) {
michael@0 503 return;
michael@0 504 }
michael@0 505
michael@0 506 let uri = NetUtil.newURI(url);
michael@0 507 DownloadsCommon.log("Pasted URL seems valid. Starting download.");
michael@0 508 DownloadURL(uri.spec, name, document);
michael@0 509 } catch (ex) {}
michael@0 510 },
michael@0 511
michael@0 512 /**
michael@0 513 * Move focus to the main element in the downloads panel, unless another
michael@0 514 * element in the panel is already focused.
michael@0 515 */
michael@0 516 _focusPanel: function DP_focusPanel()
michael@0 517 {
michael@0 518 // We may be invoked while the panel is still waiting to be shown.
michael@0 519 if (this._state != this.kStateShown) {
michael@0 520 return;
michael@0 521 }
michael@0 522
michael@0 523 let element = document.commandDispatcher.focusedElement;
michael@0 524 while (element && element != this.panel) {
michael@0 525 element = element.parentNode;
michael@0 526 }
michael@0 527 if (!element) {
michael@0 528 if (DownloadsView.richListBox.itemCount > 0) {
michael@0 529 DownloadsView.richListBox.focus();
michael@0 530 } else {
michael@0 531 DownloadsFooter.focus();
michael@0 532 }
michael@0 533 }
michael@0 534 },
michael@0 535
michael@0 536 /**
michael@0 537 * Opens the downloads panel when data is ready to be displayed.
michael@0 538 */
michael@0 539 _openPopupIfDataReady: function DP_openPopupIfDataReady()
michael@0 540 {
michael@0 541 // We don't want to open the popup if we already displayed it, or if we are
michael@0 542 // still loading data.
michael@0 543 if (this._state != this.kStateWaitingData || DownloadsView.loading) {
michael@0 544 return;
michael@0 545 }
michael@0 546
michael@0 547 this._state = this.kStateWaitingAnchor;
michael@0 548
michael@0 549 // Ensure the anchor is visible. If that is not possible, show the panel
michael@0 550 // anchored to the top area of the window, near the default anchor position.
michael@0 551 DownloadsButton.getAnchor(function DP_OPIDR_callback(aAnchor) {
michael@0 552 // If somehow we've switched states already (by getting a panel hiding
michael@0 553 // event before an overlay is loaded, for example), bail out.
michael@0 554 if (this._state != this.kStateWaitingAnchor)
michael@0 555 return;
michael@0 556
michael@0 557 // At this point, if the window is minimized, opening the panel could fail
michael@0 558 // without any notification, and there would be no way to either open or
michael@0 559 // close the panel anymore. To prevent this, check if the window is
michael@0 560 // minimized and in that case force the panel to the closed state.
michael@0 561 if (window.windowState == Ci.nsIDOMChromeWindow.STATE_MINIMIZED) {
michael@0 562 DownloadsButton.releaseAnchor();
michael@0 563 this._state = this.kStateHidden;
michael@0 564 return;
michael@0 565 }
michael@0 566
michael@0 567 // When the panel is opened, we check if the target files of visible items
michael@0 568 // still exist, and update the allowed items interactions accordingly. We
michael@0 569 // do these checks on a background thread, and don't prevent the panel to
michael@0 570 // be displayed while these checks are being performed.
michael@0 571 for each (let viewItem in DownloadsView._viewItems) {
michael@0 572 viewItem.verifyTargetExists();
michael@0 573 }
michael@0 574
michael@0 575 if (aAnchor) {
michael@0 576 DownloadsCommon.log("Opening downloads panel popup.");
michael@0 577 this.panel.openPopup(aAnchor, "bottomcenter topright", 0, 0, false,
michael@0 578 null);
michael@0 579 } else {
michael@0 580 DownloadsCommon.error("We can't find the anchor! Failure case - opening",
michael@0 581 "downloads panel on TabsToolbar. We should never",
michael@0 582 "get here!");
michael@0 583 Components.utils.reportError(
michael@0 584 "Downloads button cannot be found");
michael@0 585 }
michael@0 586 }.bind(this));
michael@0 587 }
michael@0 588 };
michael@0 589
michael@0 590 ////////////////////////////////////////////////////////////////////////////////
michael@0 591 //// DownloadsOverlayLoader
michael@0 592
michael@0 593 /**
michael@0 594 * Allows loading the downloads panel and the status indicator interfaces on
michael@0 595 * demand, to improve startup performance.
michael@0 596 */
michael@0 597 const DownloadsOverlayLoader = {
michael@0 598 /**
michael@0 599 * We cannot load two overlays at the same time, thus we use a queue of
michael@0 600 * pending load requests.
michael@0 601 */
michael@0 602 _loadRequests: [],
michael@0 603
michael@0 604 /**
michael@0 605 * True while we are waiting for an overlay to be loaded.
michael@0 606 */
michael@0 607 _overlayLoading: false,
michael@0 608
michael@0 609 /**
michael@0 610 * This object has a key for each overlay URI that is already loaded.
michael@0 611 */
michael@0 612 _loadedOverlays: {},
michael@0 613
michael@0 614 /**
michael@0 615 * Loads the specified overlay and invokes the given callback when finished.
michael@0 616 *
michael@0 617 * @param aOverlay
michael@0 618 * String containing the URI of the overlay to load in the current
michael@0 619 * window. If this overlay has already been loaded using this
michael@0 620 * function, then the overlay is not loaded again.
michael@0 621 * @param aCallback
michael@0 622 * Invoked when loading is completed. If the overlay is already
michael@0 623 * loaded, the function is called immediately.
michael@0 624 */
michael@0 625 ensureOverlayLoaded: function DOL_ensureOverlayLoaded(aOverlay, aCallback)
michael@0 626 {
michael@0 627 // The overlay is already loaded, invoke the callback immediately.
michael@0 628 if (aOverlay in this._loadedOverlays) {
michael@0 629 aCallback();
michael@0 630 return;
michael@0 631 }
michael@0 632
michael@0 633 // The callback will be invoked when loading is finished.
michael@0 634 this._loadRequests.push({ overlay: aOverlay, callback: aCallback });
michael@0 635 if (this._overlayLoading) {
michael@0 636 return;
michael@0 637 }
michael@0 638
michael@0 639 function DOL_EOL_loadCallback() {
michael@0 640 this._overlayLoading = false;
michael@0 641 this._loadedOverlays[aOverlay] = true;
michael@0 642
michael@0 643 this.processPendingRequests();
michael@0 644 }
michael@0 645
michael@0 646 this._overlayLoading = true;
michael@0 647 DownloadsCommon.log("Loading overlay ", aOverlay);
michael@0 648 document.loadOverlay(aOverlay, DOL_EOL_loadCallback.bind(this));
michael@0 649 },
michael@0 650
michael@0 651 /**
michael@0 652 * Re-processes all the currently pending requests, invoking the callbacks
michael@0 653 * and/or loading more overlays as needed. In most cases, there will be a
michael@0 654 * single request for one overlay, that will be processed immediately.
michael@0 655 */
michael@0 656 processPendingRequests: function DOL_processPendingRequests()
michael@0 657 {
michael@0 658 // Re-process all the currently pending requests, yet allow more requests
michael@0 659 // to be appended at the end of the array if we're not ready for them.
michael@0 660 let currentLength = this._loadRequests.length;
michael@0 661 for (let i = 0; i < currentLength; i++) {
michael@0 662 let request = this._loadRequests.shift();
michael@0 663
michael@0 664 // We must call ensureOverlayLoaded again for each request, to check if
michael@0 665 // the associated callback can be invoked now, or if we must still wait
michael@0 666 // for the associated overlay to load.
michael@0 667 this.ensureOverlayLoaded(request.overlay, request.callback);
michael@0 668 }
michael@0 669 }
michael@0 670 };
michael@0 671
michael@0 672 ////////////////////////////////////////////////////////////////////////////////
michael@0 673 //// DownloadsView
michael@0 674
michael@0 675 /**
michael@0 676 * Builds and updates the downloads list widget, responding to changes in the
michael@0 677 * download state and real-time data. In addition, handles part of the user
michael@0 678 * interaction events raised by the downloads list widget.
michael@0 679 */
michael@0 680 const DownloadsView = {
michael@0 681 //////////////////////////////////////////////////////////////////////////////
michael@0 682 //// Functions handling download items in the list
michael@0 683
michael@0 684 /**
michael@0 685 * Maximum number of items shown by the list at any given time.
michael@0 686 */
michael@0 687 kItemCountLimit: 3,
michael@0 688
michael@0 689 /**
michael@0 690 * Indicates whether we are still loading downloads data asynchronously.
michael@0 691 */
michael@0 692 loading: false,
michael@0 693
michael@0 694 /**
michael@0 695 * Ordered array of all DownloadsDataItem objects. We need to keep this array
michael@0 696 * because only a limited number of items are shown at once, and if an item
michael@0 697 * that is currently visible is removed from the list, we might need to take
michael@0 698 * another item from the array and make it appear at the bottom.
michael@0 699 */
michael@0 700 _dataItems: [],
michael@0 701
michael@0 702 /**
michael@0 703 * Object containing the available DownloadsViewItem objects, indexed by their
michael@0 704 * numeric download identifier. There is a limited number of view items in
michael@0 705 * the panel at any given time.
michael@0 706 */
michael@0 707 _viewItems: {},
michael@0 708
michael@0 709 /**
michael@0 710 * Called when the number of items in the list changes.
michael@0 711 */
michael@0 712 _itemCountChanged: function DV_itemCountChanged()
michael@0 713 {
michael@0 714 DownloadsCommon.log("The downloads item count has changed - we are tracking",
michael@0 715 this._dataItems.length, "downloads in total.");
michael@0 716 let count = this._dataItems.length;
michael@0 717 let hiddenCount = count - this.kItemCountLimit;
michael@0 718
michael@0 719 if (count > 0) {
michael@0 720 DownloadsCommon.log("Setting the panel's hasdownloads attribute to true.");
michael@0 721 DownloadsPanel.panel.setAttribute("hasdownloads", "true");
michael@0 722 } else {
michael@0 723 DownloadsCommon.log("Removing the panel's hasdownloads attribute.");
michael@0 724 DownloadsPanel.panel.removeAttribute("hasdownloads");
michael@0 725 }
michael@0 726
michael@0 727 // If we've got some hidden downloads, we should activate the
michael@0 728 // DownloadsSummary. The DownloadsSummary will determine whether or not
michael@0 729 // it's appropriate to actually display the summary.
michael@0 730 DownloadsSummary.active = hiddenCount > 0;
michael@0 731 },
michael@0 732
michael@0 733 /**
michael@0 734 * Element corresponding to the list of downloads.
michael@0 735 */
michael@0 736 get richListBox()
michael@0 737 {
michael@0 738 delete this.richListBox;
michael@0 739 return this.richListBox = document.getElementById("downloadsListBox");
michael@0 740 },
michael@0 741
michael@0 742 /**
michael@0 743 * Element corresponding to the button for showing more downloads.
michael@0 744 */
michael@0 745 get downloadsHistory()
michael@0 746 {
michael@0 747 delete this.downloadsHistory;
michael@0 748 return this.downloadsHistory = document.getElementById("downloadsHistory");
michael@0 749 },
michael@0 750
michael@0 751 //////////////////////////////////////////////////////////////////////////////
michael@0 752 //// Callback functions from DownloadsData
michael@0 753
michael@0 754 /**
michael@0 755 * Called before multiple downloads are about to be loaded.
michael@0 756 */
michael@0 757 onDataLoadStarting: function DV_onDataLoadStarting()
michael@0 758 {
michael@0 759 DownloadsCommon.log("onDataLoadStarting called for DownloadsView.");
michael@0 760 this.loading = true;
michael@0 761 },
michael@0 762
michael@0 763 /**
michael@0 764 * Called after data loading finished.
michael@0 765 */
michael@0 766 onDataLoadCompleted: function DV_onDataLoadCompleted()
michael@0 767 {
michael@0 768 DownloadsCommon.log("onDataLoadCompleted called for DownloadsView.");
michael@0 769
michael@0 770 this.loading = false;
michael@0 771
michael@0 772 // We suppressed item count change notifications during the batch load, at
michael@0 773 // this point we should just call the function once.
michael@0 774 this._itemCountChanged();
michael@0 775
michael@0 776 // Notify the panel that all the initially available downloads have been
michael@0 777 // loaded. This ensures that the interface is visible, if still required.
michael@0 778 DownloadsPanel.onViewLoadCompleted();
michael@0 779 },
michael@0 780
michael@0 781 /**
michael@0 782 * Called when a new download data item is available, either during the
michael@0 783 * asynchronous data load or when a new download is started.
michael@0 784 *
michael@0 785 * @param aDataItem
michael@0 786 * DownloadsDataItem object that was just added.
michael@0 787 * @param aNewest
michael@0 788 * When true, indicates that this item is the most recent and should be
michael@0 789 * added in the topmost position. This happens when a new download is
michael@0 790 * started. When false, indicates that the item is the least recent
michael@0 791 * and should be appended. The latter generally happens during the
michael@0 792 * asynchronous data load.
michael@0 793 */
michael@0 794 onDataItemAdded: function DV_onDataItemAdded(aDataItem, aNewest)
michael@0 795 {
michael@0 796 DownloadsCommon.log("A new download data item was added - aNewest =",
michael@0 797 aNewest);
michael@0 798
michael@0 799 if (aNewest) {
michael@0 800 this._dataItems.unshift(aDataItem);
michael@0 801 } else {
michael@0 802 this._dataItems.push(aDataItem);
michael@0 803 }
michael@0 804
michael@0 805 let itemsNowOverflow = this._dataItems.length > this.kItemCountLimit;
michael@0 806 if (aNewest || !itemsNowOverflow) {
michael@0 807 // The newly added item is visible in the panel and we must add the
michael@0 808 // corresponding element. This is either because it is the first item, or
michael@0 809 // because it was added at the bottom but the list still doesn't overflow.
michael@0 810 this._addViewItem(aDataItem, aNewest);
michael@0 811 }
michael@0 812 if (aNewest && itemsNowOverflow) {
michael@0 813 // If the list overflows, remove the last item from the panel to make room
michael@0 814 // for the new one that we just added at the top.
michael@0 815 this._removeViewItem(this._dataItems[this.kItemCountLimit]);
michael@0 816 }
michael@0 817
michael@0 818 // For better performance during batch loads, don't update the count for
michael@0 819 // every item, because the interface won't be visible until load finishes.
michael@0 820 if (!this.loading) {
michael@0 821 this._itemCountChanged();
michael@0 822 }
michael@0 823 },
michael@0 824
michael@0 825 /**
michael@0 826 * Called when a data item is removed. Ensures that the widget associated
michael@0 827 * with the view item is removed from the user interface.
michael@0 828 *
michael@0 829 * @param aDataItem
michael@0 830 * DownloadsDataItem object that is being removed.
michael@0 831 */
michael@0 832 onDataItemRemoved: function DV_onDataItemRemoved(aDataItem)
michael@0 833 {
michael@0 834 DownloadsCommon.log("A download data item was removed.");
michael@0 835
michael@0 836 let itemIndex = this._dataItems.indexOf(aDataItem);
michael@0 837 this._dataItems.splice(itemIndex, 1);
michael@0 838
michael@0 839 if (itemIndex < this.kItemCountLimit) {
michael@0 840 // The item to remove is visible in the panel.
michael@0 841 this._removeViewItem(aDataItem);
michael@0 842 if (this._dataItems.length >= this.kItemCountLimit) {
michael@0 843 // Reinsert the next item into the panel.
michael@0 844 this._addViewItem(this._dataItems[this.kItemCountLimit - 1], false);
michael@0 845 }
michael@0 846 }
michael@0 847
michael@0 848 this._itemCountChanged();
michael@0 849 },
michael@0 850
michael@0 851 /**
michael@0 852 * Returns the view item associated with the provided data item for this view.
michael@0 853 *
michael@0 854 * @param aDataItem
michael@0 855 * DownloadsDataItem object for which the view item is requested.
michael@0 856 *
michael@0 857 * @return Object that can be used to notify item status events.
michael@0 858 */
michael@0 859 getViewItem: function DV_getViewItem(aDataItem)
michael@0 860 {
michael@0 861 // If the item is visible, just return it, otherwise return a mock object
michael@0 862 // that doesn't react to notifications.
michael@0 863 if (aDataItem.downloadGuid in this._viewItems) {
michael@0 864 return this._viewItems[aDataItem.downloadGuid];
michael@0 865 }
michael@0 866 return this._invisibleViewItem;
michael@0 867 },
michael@0 868
michael@0 869 /**
michael@0 870 * Mock DownloadsDataItem object that doesn't react to notifications.
michael@0 871 */
michael@0 872 _invisibleViewItem: Object.freeze({
michael@0 873 onStateChange: function () { },
michael@0 874 onProgressChange: function () { }
michael@0 875 }),
michael@0 876
michael@0 877 /**
michael@0 878 * Creates a new view item associated with the specified data item, and adds
michael@0 879 * it to the top or the bottom of the list.
michael@0 880 */
michael@0 881 _addViewItem: function DV_addViewItem(aDataItem, aNewest)
michael@0 882 {
michael@0 883 DownloadsCommon.log("Adding a new DownloadsViewItem to the downloads list.",
michael@0 884 "aNewest =", aNewest);
michael@0 885
michael@0 886 let element = document.createElement("richlistitem");
michael@0 887 let viewItem = new DownloadsViewItem(aDataItem, element);
michael@0 888 this._viewItems[aDataItem.downloadGuid] = viewItem;
michael@0 889 if (aNewest) {
michael@0 890 this.richListBox.insertBefore(element, this.richListBox.firstChild);
michael@0 891 } else {
michael@0 892 this.richListBox.appendChild(element);
michael@0 893 }
michael@0 894 },
michael@0 895
michael@0 896 /**
michael@0 897 * Removes the view item associated with the specified data item.
michael@0 898 */
michael@0 899 _removeViewItem: function DV_removeViewItem(aDataItem)
michael@0 900 {
michael@0 901 DownloadsCommon.log("Removing a DownloadsViewItem from the downloads list.");
michael@0 902 let element = this.getViewItem(aDataItem)._element;
michael@0 903 let previousSelectedIndex = this.richListBox.selectedIndex;
michael@0 904 this.richListBox.removeChild(element);
michael@0 905 if (previousSelectedIndex != -1) {
michael@0 906 this.richListBox.selectedIndex = Math.min(previousSelectedIndex,
michael@0 907 this.richListBox.itemCount - 1);
michael@0 908 }
michael@0 909 delete this._viewItems[aDataItem.downloadGuid];
michael@0 910 },
michael@0 911
michael@0 912 //////////////////////////////////////////////////////////////////////////////
michael@0 913 //// User interface event functions
michael@0 914
michael@0 915 /**
michael@0 916 * Helper function to do commands on a specific download item.
michael@0 917 *
michael@0 918 * @param aEvent
michael@0 919 * Event object for the event being handled. If the event target is
michael@0 920 * not a richlistitem that represents a download, this function will
michael@0 921 * walk up the parent nodes until it finds a DOM node that is.
michael@0 922 * @param aCommand
michael@0 923 * The command to be performed.
michael@0 924 */
michael@0 925 onDownloadCommand: function DV_onDownloadCommand(aEvent, aCommand)
michael@0 926 {
michael@0 927 let target = aEvent.target;
michael@0 928 while (target.nodeName != "richlistitem") {
michael@0 929 target = target.parentNode;
michael@0 930 }
michael@0 931 new DownloadsViewItemController(target).doCommand(aCommand);
michael@0 932 },
michael@0 933
michael@0 934 onDownloadClick: function DV_onDownloadClick(aEvent)
michael@0 935 {
michael@0 936 // Handle primary clicks only, and exclude the action button.
michael@0 937 if (aEvent.button == 0 &&
michael@0 938 !aEvent.originalTarget.hasAttribute("oncommand")) {
michael@0 939 goDoCommand("downloadsCmd_open");
michael@0 940 }
michael@0 941 },
michael@0 942
michael@0 943 /**
michael@0 944 * Handles keypress events on a download item.
michael@0 945 */
michael@0 946 onDownloadKeyPress: function DV_onDownloadKeyPress(aEvent)
michael@0 947 {
michael@0 948 // Pressing the key on buttons should not invoke the action because the
michael@0 949 // event has already been handled by the button itself.
michael@0 950 if (aEvent.originalTarget.hasAttribute("command") ||
michael@0 951 aEvent.originalTarget.hasAttribute("oncommand")) {
michael@0 952 return;
michael@0 953 }
michael@0 954
michael@0 955 if (aEvent.charCode == " ".charCodeAt(0)) {
michael@0 956 goDoCommand("downloadsCmd_pauseResume");
michael@0 957 return;
michael@0 958 }
michael@0 959
michael@0 960 if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
michael@0 961 goDoCommand("downloadsCmd_doDefault");
michael@0 962 }
michael@0 963 },
michael@0 964
michael@0 965
michael@0 966 /**
michael@0 967 * Mouse listeners to handle selection on hover.
michael@0 968 */
michael@0 969 onDownloadMouseOver: function DV_onDownloadMouseOver(aEvent)
michael@0 970 {
michael@0 971 if (aEvent.originalTarget.parentNode == this.richListBox)
michael@0 972 this.richListBox.selectedItem = aEvent.originalTarget;
michael@0 973 },
michael@0 974 onDownloadMouseOut: function DV_onDownloadMouseOut(aEvent)
michael@0 975 {
michael@0 976 if (aEvent.originalTarget.parentNode == this.richListBox) {
michael@0 977 // If the destination element is outside of the richlistitem, clear the
michael@0 978 // selection.
michael@0 979 let element = aEvent.relatedTarget;
michael@0 980 while (element && element != aEvent.originalTarget) {
michael@0 981 element = element.parentNode;
michael@0 982 }
michael@0 983 if (!element)
michael@0 984 this.richListBox.selectedIndex = -1;
michael@0 985 }
michael@0 986 },
michael@0 987
michael@0 988 onDownloadContextMenu: function DV_onDownloadContextMenu(aEvent)
michael@0 989 {
michael@0 990 let element = this.richListBox.selectedItem;
michael@0 991 if (!element) {
michael@0 992 return;
michael@0 993 }
michael@0 994
michael@0 995 DownloadsViewController.updateCommands();
michael@0 996
michael@0 997 // Set the state attribute so that only the appropriate items are displayed.
michael@0 998 let contextMenu = document.getElementById("downloadsContextMenu");
michael@0 999 contextMenu.setAttribute("state", element.getAttribute("state"));
michael@0 1000 },
michael@0 1001
michael@0 1002 onDownloadDragStart: function DV_onDownloadDragStart(aEvent)
michael@0 1003 {
michael@0 1004 let element = this.richListBox.selectedItem;
michael@0 1005 if (!element) {
michael@0 1006 return;
michael@0 1007 }
michael@0 1008
michael@0 1009 let controller = new DownloadsViewItemController(element);
michael@0 1010 let localFile = controller.dataItem.localFile;
michael@0 1011 if (!localFile.exists()) {
michael@0 1012 return;
michael@0 1013 }
michael@0 1014
michael@0 1015 let dataTransfer = aEvent.dataTransfer;
michael@0 1016 dataTransfer.mozSetDataAt("application/x-moz-file", localFile, 0);
michael@0 1017 dataTransfer.effectAllowed = "copyMove";
michael@0 1018 var url = Services.io.newFileURI(localFile).spec;
michael@0 1019 dataTransfer.setData("text/uri-list", url);
michael@0 1020 dataTransfer.setData("text/plain", url);
michael@0 1021 dataTransfer.addElement(element);
michael@0 1022
michael@0 1023 aEvent.stopPropagation();
michael@0 1024 }
michael@0 1025 }
michael@0 1026
michael@0 1027 ////////////////////////////////////////////////////////////////////////////////
michael@0 1028 //// DownloadsViewItem
michael@0 1029
michael@0 1030 /**
michael@0 1031 * Builds and updates a single item in the downloads list widget, responding to
michael@0 1032 * changes in the download state and real-time data.
michael@0 1033 *
michael@0 1034 * @param aDataItem
michael@0 1035 * DownloadsDataItem to be associated with the view item.
michael@0 1036 * @param aElement
michael@0 1037 * XUL element corresponding to the single download item in the view.
michael@0 1038 */
michael@0 1039 function DownloadsViewItem(aDataItem, aElement)
michael@0 1040 {
michael@0 1041 this._element = aElement;
michael@0 1042 this.dataItem = aDataItem;
michael@0 1043
michael@0 1044 this.lastEstimatedSecondsLeft = Infinity;
michael@0 1045
michael@0 1046 // Set the URI that represents the correct icon for the target file. As soon
michael@0 1047 // as bug 239948 comment 12 is handled, the "file" property will be always a
michael@0 1048 // file URL rather than a file name. At that point we should remove the "//"
michael@0 1049 // (double slash) from the icon URI specification (see test_moz_icon_uri.js).
michael@0 1050 this.image = "moz-icon://" + this.dataItem.file + "?size=32";
michael@0 1051
michael@0 1052 let attributes = {
michael@0 1053 "type": "download",
michael@0 1054 "class": "download-state",
michael@0 1055 "id": "downloadsItem_" + this.dataItem.downloadGuid,
michael@0 1056 "downloadGuid": this.dataItem.downloadGuid,
michael@0 1057 "state": this.dataItem.state,
michael@0 1058 "progress": this.dataItem.inProgress ? this.dataItem.percentComplete : 100,
michael@0 1059 "target": this.dataItem.target,
michael@0 1060 "image": this.image
michael@0 1061 };
michael@0 1062
michael@0 1063 for (let attributeName in attributes) {
michael@0 1064 this._element.setAttribute(attributeName, attributes[attributeName]);
michael@0 1065 }
michael@0 1066
michael@0 1067 // Initialize more complex attributes.
michael@0 1068 this._updateProgress();
michael@0 1069 this._updateStatusLine();
michael@0 1070 this.verifyTargetExists();
michael@0 1071 }
michael@0 1072
michael@0 1073 DownloadsViewItem.prototype = {
michael@0 1074 /**
michael@0 1075 * The DownloadDataItem associated with this view item.
michael@0 1076 */
michael@0 1077 dataItem: null,
michael@0 1078
michael@0 1079 /**
michael@0 1080 * The XUL element corresponding to the associated richlistbox item.
michael@0 1081 */
michael@0 1082 _element: null,
michael@0 1083
michael@0 1084 /**
michael@0 1085 * The inner XUL element for the progress bar, or null if not available.
michael@0 1086 */
michael@0 1087 _progressElement: null,
michael@0 1088
michael@0 1089 //////////////////////////////////////////////////////////////////////////////
michael@0 1090 //// Callback functions from DownloadsData
michael@0 1091
michael@0 1092 /**
michael@0 1093 * Called when the download state might have changed. Sometimes the state of
michael@0 1094 * the download might be the same as before, if the data layer received
michael@0 1095 * multiple events for the same download.
michael@0 1096 */
michael@0 1097 onStateChange: function DVI_onStateChange(aOldState)
michael@0 1098 {
michael@0 1099 // If a download just finished successfully, it means that the target file
michael@0 1100 // now exists and we can extract its specific icon. To ensure that the icon
michael@0 1101 // is reloaded, we must change the URI used by the XUL image element, for
michael@0 1102 // example by adding a query parameter. Since this URI has a "moz-icon"
michael@0 1103 // scheme, this only works if we add one of the parameters explicitly
michael@0 1104 // supported by the nsIMozIconURI interface.
michael@0 1105 if (aOldState != Ci.nsIDownloadManager.DOWNLOAD_FINISHED &&
michael@0 1106 aOldState != this.dataItem.state) {
michael@0 1107 this._element.setAttribute("image", this.image + "&state=normal");
michael@0 1108
michael@0 1109 // We assume the existence of the target of a download that just completed
michael@0 1110 // successfully, without checking the condition in the background. If the
michael@0 1111 // panel is already open, this will take effect immediately. If the panel
michael@0 1112 // is opened later, a new background existence check will be performed.
michael@0 1113 this._element.setAttribute("exists", "true");
michael@0 1114 }
michael@0 1115
michael@0 1116 // Update the user interface after switching states.
michael@0 1117 this._element.setAttribute("state", this.dataItem.state);
michael@0 1118 this._updateProgress();
michael@0 1119 this._updateStatusLine();
michael@0 1120 },
michael@0 1121
michael@0 1122 /**
michael@0 1123 * Called when the download progress has changed.
michael@0 1124 */
michael@0 1125 onProgressChange: function DVI_onProgressChange() {
michael@0 1126 this._updateProgress();
michael@0 1127 this._updateStatusLine();
michael@0 1128 },
michael@0 1129
michael@0 1130 //////////////////////////////////////////////////////////////////////////////
michael@0 1131 //// Functions for updating the user interface
michael@0 1132
michael@0 1133 /**
michael@0 1134 * Updates the progress bar.
michael@0 1135 */
michael@0 1136 _updateProgress: function DVI_updateProgress() {
michael@0 1137 if (this.dataItem.starting) {
michael@0 1138 // Before the download starts, the progress meter has its initial value.
michael@0 1139 this._element.setAttribute("progressmode", "normal");
michael@0 1140 this._element.setAttribute("progress", "0");
michael@0 1141 } else if (this.dataItem.state == Ci.nsIDownloadManager.DOWNLOAD_SCANNING ||
michael@0 1142 this.dataItem.percentComplete == -1) {
michael@0 1143 // We might not know the progress of a running download, and we don't know
michael@0 1144 // the remaining time during the malware scanning phase.
michael@0 1145 this._element.setAttribute("progressmode", "undetermined");
michael@0 1146 } else {
michael@0 1147 // This is a running download of which we know the progress.
michael@0 1148 this._element.setAttribute("progressmode", "normal");
michael@0 1149 this._element.setAttribute("progress", this.dataItem.percentComplete);
michael@0 1150 }
michael@0 1151
michael@0 1152 // Find the progress element as soon as the download binding is accessible.
michael@0 1153 if (!this._progressElement) {
michael@0 1154 this._progressElement =
michael@0 1155 document.getAnonymousElementByAttribute(this._element, "anonid",
michael@0 1156 "progressmeter");
michael@0 1157 }
michael@0 1158
michael@0 1159 // Dispatch the ValueChange event for accessibility, if possible.
michael@0 1160 if (this._progressElement) {
michael@0 1161 let event = document.createEvent("Events");
michael@0 1162 event.initEvent("ValueChange", true, true);
michael@0 1163 this._progressElement.dispatchEvent(event);
michael@0 1164 }
michael@0 1165 },
michael@0 1166
michael@0 1167 /**
michael@0 1168 * Updates the main status line, including bytes transferred, bytes total,
michael@0 1169 * download rate, and time remaining.
michael@0 1170 */
michael@0 1171 _updateStatusLine: function DVI_updateStatusLine() {
michael@0 1172 const nsIDM = Ci.nsIDownloadManager;
michael@0 1173
michael@0 1174 let status = "";
michael@0 1175 let statusTip = "";
michael@0 1176
michael@0 1177 if (this.dataItem.paused) {
michael@0 1178 let transfer = DownloadUtils.getTransferTotal(this.dataItem.currBytes,
michael@0 1179 this.dataItem.maxBytes);
michael@0 1180
michael@0 1181 // We use the same XUL label to display both the state and the amount
michael@0 1182 // transferred, for example "Paused - 1.1 MB".
michael@0 1183 status = DownloadsCommon.strings.statusSeparatorBeforeNumber(
michael@0 1184 DownloadsCommon.strings.statePaused,
michael@0 1185 transfer);
michael@0 1186 } else if (this.dataItem.state == nsIDM.DOWNLOAD_DOWNLOADING) {
michael@0 1187 // We don't show the rate for each download in order to reduce clutter.
michael@0 1188 // The remaining time per download is likely enough information for the
michael@0 1189 // panel.
michael@0 1190 [status] =
michael@0 1191 DownloadUtils.getDownloadStatusNoRate(this.dataItem.currBytes,
michael@0 1192 this.dataItem.maxBytes,
michael@0 1193 this.dataItem.speed,
michael@0 1194 this.lastEstimatedSecondsLeft);
michael@0 1195
michael@0 1196 // We are, however, OK with displaying the rate in the tooltip.
michael@0 1197 let newEstimatedSecondsLeft;
michael@0 1198 [statusTip, newEstimatedSecondsLeft] =
michael@0 1199 DownloadUtils.getDownloadStatus(this.dataItem.currBytes,
michael@0 1200 this.dataItem.maxBytes,
michael@0 1201 this.dataItem.speed,
michael@0 1202 this.lastEstimatedSecondsLeft);
michael@0 1203 this.lastEstimatedSecondsLeft = newEstimatedSecondsLeft;
michael@0 1204 } else if (this.dataItem.starting) {
michael@0 1205 status = DownloadsCommon.strings.stateStarting;
michael@0 1206 } else if (this.dataItem.state == nsIDM.DOWNLOAD_SCANNING) {
michael@0 1207 status = DownloadsCommon.strings.stateScanning;
michael@0 1208 } else if (!this.dataItem.inProgress) {
michael@0 1209 let stateLabel = function () {
michael@0 1210 let s = DownloadsCommon.strings;
michael@0 1211 switch (this.dataItem.state) {
michael@0 1212 case nsIDM.DOWNLOAD_FAILED: return s.stateFailed;
michael@0 1213 case nsIDM.DOWNLOAD_CANCELED: return s.stateCanceled;
michael@0 1214 case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: return s.stateBlockedParentalControls;
michael@0 1215 case nsIDM.DOWNLOAD_BLOCKED_POLICY: return s.stateBlockedPolicy;
michael@0 1216 case nsIDM.DOWNLOAD_DIRTY: return s.stateDirty;
michael@0 1217 case nsIDM.DOWNLOAD_FINISHED: return this._fileSizeText;
michael@0 1218 }
michael@0 1219 return null;
michael@0 1220 }.apply(this);
michael@0 1221
michael@0 1222 let [displayHost, fullHost] =
michael@0 1223 DownloadUtils.getURIHost(this.dataItem.referrer || this.dataItem.uri);
michael@0 1224
michael@0 1225 let end = new Date(this.dataItem.endTime);
michael@0 1226 let [displayDate, fullDate] = DownloadUtils.getReadableDates(end);
michael@0 1227
michael@0 1228 // We use the same XUL label to display the state, the host name, and the
michael@0 1229 // end time, for example "Canceled - 222.net - 11:15" or "1.1 MB -
michael@0 1230 // website2.com - Yesterday". We show the full host and the complete date
michael@0 1231 // in the tooltip.
michael@0 1232 let firstPart = DownloadsCommon.strings.statusSeparator(stateLabel,
michael@0 1233 displayHost);
michael@0 1234 status = DownloadsCommon.strings.statusSeparator(firstPart, displayDate);
michael@0 1235 statusTip = DownloadsCommon.strings.statusSeparator(fullHost, fullDate);
michael@0 1236 }
michael@0 1237
michael@0 1238 this._element.setAttribute("status", status);
michael@0 1239 this._element.setAttribute("statusTip", statusTip || status);
michael@0 1240 },
michael@0 1241
michael@0 1242 /**
michael@0 1243 * Localized string representing the total size of completed downloads, for
michael@0 1244 * example "1.5 MB" or "Unknown size".
michael@0 1245 */
michael@0 1246 get _fileSizeText()
michael@0 1247 {
michael@0 1248 // Display the file size, but show "Unknown" for negative sizes.
michael@0 1249 let fileSize = this.dataItem.maxBytes;
michael@0 1250 if (fileSize < 0) {
michael@0 1251 return DownloadsCommon.strings.sizeUnknown;
michael@0 1252 }
michael@0 1253 let [size, unit] = DownloadUtils.convertByteUnits(fileSize);
michael@0 1254 return DownloadsCommon.strings.sizeWithUnits(size, unit);
michael@0 1255 },
michael@0 1256
michael@0 1257 //////////////////////////////////////////////////////////////////////////////
michael@0 1258 //// Functions called by the panel
michael@0 1259
michael@0 1260 /**
michael@0 1261 * Starts checking whether the target file of a finished download is still
michael@0 1262 * available on disk, and sets an attribute that controls how the item is
michael@0 1263 * presented visually.
michael@0 1264 *
michael@0 1265 * The existence check is executed on a background thread.
michael@0 1266 */
michael@0 1267 verifyTargetExists: function DVI_verifyTargetExists() {
michael@0 1268 // We don't need to check if the download is not finished successfully.
michael@0 1269 if (!this.dataItem.openable) {
michael@0 1270 return;
michael@0 1271 }
michael@0 1272
michael@0 1273 OS.File.exists(this.dataItem.localFile.path).then(
michael@0 1274 function DVI_RTE_onSuccess(aExists) {
michael@0 1275 if (aExists) {
michael@0 1276 this._element.setAttribute("exists", "true");
michael@0 1277 } else {
michael@0 1278 this._element.removeAttribute("exists");
michael@0 1279 }
michael@0 1280 }.bind(this), Cu.reportError);
michael@0 1281 },
michael@0 1282 };
michael@0 1283
michael@0 1284 ////////////////////////////////////////////////////////////////////////////////
michael@0 1285 //// DownloadsViewController
michael@0 1286
michael@0 1287 /**
michael@0 1288 * Handles part of the user interaction events raised by the downloads list
michael@0 1289 * widget, in particular the "commands" that apply to multiple items, and
michael@0 1290 * dispatches the commands that apply to individual items.
michael@0 1291 */
michael@0 1292 const DownloadsViewController = {
michael@0 1293 //////////////////////////////////////////////////////////////////////////////
michael@0 1294 //// Initialization and termination
michael@0 1295
michael@0 1296 initialize: function DVC_initialize()
michael@0 1297 {
michael@0 1298 window.controllers.insertControllerAt(0, this);
michael@0 1299 },
michael@0 1300
michael@0 1301 terminate: function DVC_terminate()
michael@0 1302 {
michael@0 1303 window.controllers.removeController(this);
michael@0 1304 },
michael@0 1305
michael@0 1306 //////////////////////////////////////////////////////////////////////////////
michael@0 1307 //// nsIController
michael@0 1308
michael@0 1309 supportsCommand: function DVC_supportsCommand(aCommand)
michael@0 1310 {
michael@0 1311 // Firstly, determine if this is a command that we can handle.
michael@0 1312 if (!(aCommand in this.commands) &&
michael@0 1313 !(aCommand in DownloadsViewItemController.prototype.commands)) {
michael@0 1314 return false;
michael@0 1315 }
michael@0 1316 // Secondly, determine if focus is on a control in the downloads list.
michael@0 1317 let element = document.commandDispatcher.focusedElement;
michael@0 1318 while (element && element != DownloadsView.richListBox) {
michael@0 1319 element = element.parentNode;
michael@0 1320 }
michael@0 1321 // We should handle the command only if the downloads list is among the
michael@0 1322 // ancestors of the focused element.
michael@0 1323 return !!element;
michael@0 1324 },
michael@0 1325
michael@0 1326 isCommandEnabled: function DVC_isCommandEnabled(aCommand)
michael@0 1327 {
michael@0 1328 // Handle commands that are not selection-specific.
michael@0 1329 if (aCommand == "downloadsCmd_clearList") {
michael@0 1330 return DownloadsCommon.getData(window).canRemoveFinished;
michael@0 1331 }
michael@0 1332
michael@0 1333 // Other commands are selection-specific.
michael@0 1334 let element = DownloadsView.richListBox.selectedItem;
michael@0 1335 return element &&
michael@0 1336 new DownloadsViewItemController(element).isCommandEnabled(aCommand);
michael@0 1337 },
michael@0 1338
michael@0 1339 doCommand: function DVC_doCommand(aCommand)
michael@0 1340 {
michael@0 1341 // If this command is not selection-specific, execute it.
michael@0 1342 if (aCommand in this.commands) {
michael@0 1343 this.commands[aCommand].apply(this);
michael@0 1344 return;
michael@0 1345 }
michael@0 1346
michael@0 1347 // Other commands are selection-specific.
michael@0 1348 let element = DownloadsView.richListBox.selectedItem;
michael@0 1349 if (element) {
michael@0 1350 // The doCommand function also checks if the command is enabled.
michael@0 1351 new DownloadsViewItemController(element).doCommand(aCommand);
michael@0 1352 }
michael@0 1353 },
michael@0 1354
michael@0 1355 onEvent: function () { },
michael@0 1356
michael@0 1357 //////////////////////////////////////////////////////////////////////////////
michael@0 1358 //// Other functions
michael@0 1359
michael@0 1360 updateCommands: function DVC_updateCommands()
michael@0 1361 {
michael@0 1362 Object.keys(this.commands).forEach(goUpdateCommand);
michael@0 1363 Object.keys(DownloadsViewItemController.prototype.commands)
michael@0 1364 .forEach(goUpdateCommand);
michael@0 1365 },
michael@0 1366
michael@0 1367 //////////////////////////////////////////////////////////////////////////////
michael@0 1368 //// Selection-independent commands
michael@0 1369
michael@0 1370 /**
michael@0 1371 * This object contains one key for each command that operates regardless of
michael@0 1372 * the currently selected item in the list.
michael@0 1373 */
michael@0 1374 commands: {
michael@0 1375 downloadsCmd_clearList: function DVC_downloadsCmd_clearList()
michael@0 1376 {
michael@0 1377 DownloadsCommon.getData(window).removeFinished();
michael@0 1378 }
michael@0 1379 }
michael@0 1380 };
michael@0 1381
michael@0 1382 ////////////////////////////////////////////////////////////////////////////////
michael@0 1383 //// DownloadsViewItemController
michael@0 1384
michael@0 1385 /**
michael@0 1386 * Handles all the user interaction events, in particular the "commands",
michael@0 1387 * related to a single item in the downloads list widgets.
michael@0 1388 */
michael@0 1389 function DownloadsViewItemController(aElement) {
michael@0 1390 let downloadGuid = aElement.getAttribute("downloadGuid");
michael@0 1391 this.dataItem = DownloadsCommon.getData(window).dataItems[downloadGuid];
michael@0 1392 }
michael@0 1393
michael@0 1394 DownloadsViewItemController.prototype = {
michael@0 1395 //////////////////////////////////////////////////////////////////////////////
michael@0 1396 //// Command dispatching
michael@0 1397
michael@0 1398 /**
michael@0 1399 * The DownloadDataItem controlled by this object.
michael@0 1400 */
michael@0 1401 dataItem: null,
michael@0 1402
michael@0 1403 isCommandEnabled: function DVIC_isCommandEnabled(aCommand)
michael@0 1404 {
michael@0 1405 switch (aCommand) {
michael@0 1406 case "downloadsCmd_open": {
michael@0 1407 return this.dataItem.openable && this.dataItem.localFile.exists();
michael@0 1408 }
michael@0 1409 case "downloadsCmd_show": {
michael@0 1410 return this.dataItem.localFile.exists() ||
michael@0 1411 this.dataItem.partFile.exists();
michael@0 1412 }
michael@0 1413 case "downloadsCmd_pauseResume":
michael@0 1414 return this.dataItem.inProgress && this.dataItem.resumable;
michael@0 1415 case "downloadsCmd_retry":
michael@0 1416 return this.dataItem.canRetry;
michael@0 1417 case "downloadsCmd_openReferrer":
michael@0 1418 return !!this.dataItem.referrer;
michael@0 1419 case "cmd_delete":
michael@0 1420 case "downloadsCmd_cancel":
michael@0 1421 case "downloadsCmd_copyLocation":
michael@0 1422 case "downloadsCmd_doDefault":
michael@0 1423 return true;
michael@0 1424 }
michael@0 1425 return false;
michael@0 1426 },
michael@0 1427
michael@0 1428 doCommand: function DVIC_doCommand(aCommand)
michael@0 1429 {
michael@0 1430 if (this.isCommandEnabled(aCommand)) {
michael@0 1431 this.commands[aCommand].apply(this);
michael@0 1432 }
michael@0 1433 },
michael@0 1434
michael@0 1435 //////////////////////////////////////////////////////////////////////////////
michael@0 1436 //// Item commands
michael@0 1437
michael@0 1438 /**
michael@0 1439 * This object contains one key for each command that operates on this item.
michael@0 1440 *
michael@0 1441 * In commands, the "this" identifier points to the controller item.
michael@0 1442 */
michael@0 1443 commands: {
michael@0 1444 cmd_delete: function DVIC_cmd_delete()
michael@0 1445 {
michael@0 1446 this.dataItem.remove();
michael@0 1447 PlacesUtils.bhistory.removePage(NetUtil.newURI(this.dataItem.uri));
michael@0 1448 },
michael@0 1449
michael@0 1450 downloadsCmd_cancel: function DVIC_downloadsCmd_cancel()
michael@0 1451 {
michael@0 1452 this.dataItem.cancel();
michael@0 1453 },
michael@0 1454
michael@0 1455 downloadsCmd_open: function DVIC_downloadsCmd_open()
michael@0 1456 {
michael@0 1457 this.dataItem.openLocalFile();
michael@0 1458
michael@0 1459 // We explicitly close the panel here to give the user the feedback that
michael@0 1460 // their click has been received, and we're handling the action.
michael@0 1461 // Otherwise, we'd have to wait for the file-type handler to execute
michael@0 1462 // before the panel would close. This also helps to prevent the user from
michael@0 1463 // accidentally opening a file several times.
michael@0 1464 DownloadsPanel.hidePanel();
michael@0 1465 },
michael@0 1466
michael@0 1467 downloadsCmd_show: function DVIC_downloadsCmd_show()
michael@0 1468 {
michael@0 1469 this.dataItem.showLocalFile();
michael@0 1470
michael@0 1471 // We explicitly close the panel here to give the user the feedback that
michael@0 1472 // their click has been received, and we're handling the action.
michael@0 1473 // Otherwise, we'd have to wait for the operating system file manager
michael@0 1474 // window to open before the panel closed. This also helps to prevent the
michael@0 1475 // user from opening the containing folder several times.
michael@0 1476 DownloadsPanel.hidePanel();
michael@0 1477 },
michael@0 1478
michael@0 1479 downloadsCmd_pauseResume: function DVIC_downloadsCmd_pauseResume()
michael@0 1480 {
michael@0 1481 this.dataItem.togglePauseResume();
michael@0 1482 },
michael@0 1483
michael@0 1484 downloadsCmd_retry: function DVIC_downloadsCmd_retry()
michael@0 1485 {
michael@0 1486 this.dataItem.retry();
michael@0 1487 },
michael@0 1488
michael@0 1489 downloadsCmd_openReferrer: function DVIC_downloadsCmd_openReferrer()
michael@0 1490 {
michael@0 1491 openURL(this.dataItem.referrer);
michael@0 1492 },
michael@0 1493
michael@0 1494 downloadsCmd_copyLocation: function DVIC_downloadsCmd_copyLocation()
michael@0 1495 {
michael@0 1496 let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
michael@0 1497 .getService(Ci.nsIClipboardHelper);
michael@0 1498 clipboard.copyString(this.dataItem.uri, document);
michael@0 1499 },
michael@0 1500
michael@0 1501 downloadsCmd_doDefault: function DVIC_downloadsCmd_doDefault()
michael@0 1502 {
michael@0 1503 const nsIDM = Ci.nsIDownloadManager;
michael@0 1504
michael@0 1505 // Determine the default command for the current item.
michael@0 1506 let defaultCommand = function () {
michael@0 1507 switch (this.dataItem.state) {
michael@0 1508 case nsIDM.DOWNLOAD_NOTSTARTED: return "downloadsCmd_cancel";
michael@0 1509 case nsIDM.DOWNLOAD_FINISHED: return "downloadsCmd_open";
michael@0 1510 case nsIDM.DOWNLOAD_FAILED: return "downloadsCmd_retry";
michael@0 1511 case nsIDM.DOWNLOAD_CANCELED: return "downloadsCmd_retry";
michael@0 1512 case nsIDM.DOWNLOAD_PAUSED: return "downloadsCmd_pauseResume";
michael@0 1513 case nsIDM.DOWNLOAD_QUEUED: return "downloadsCmd_cancel";
michael@0 1514 case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: return "downloadsCmd_openReferrer";
michael@0 1515 case nsIDM.DOWNLOAD_SCANNING: return "downloadsCmd_show";
michael@0 1516 case nsIDM.DOWNLOAD_DIRTY: return "downloadsCmd_openReferrer";
michael@0 1517 case nsIDM.DOWNLOAD_BLOCKED_POLICY: return "downloadsCmd_openReferrer";
michael@0 1518 }
michael@0 1519 return "";
michael@0 1520 }.apply(this);
michael@0 1521 if (defaultCommand && this.isCommandEnabled(defaultCommand))
michael@0 1522 this.doCommand(defaultCommand);
michael@0 1523 }
michael@0 1524 }
michael@0 1525 };
michael@0 1526
michael@0 1527
michael@0 1528 ////////////////////////////////////////////////////////////////////////////////
michael@0 1529 //// DownloadsSummary
michael@0 1530
michael@0 1531 /**
michael@0 1532 * Manages the summary at the bottom of the downloads panel list if the number
michael@0 1533 * of items in the list exceeds the panels limit.
michael@0 1534 */
michael@0 1535 const DownloadsSummary = {
michael@0 1536
michael@0 1537 /**
michael@0 1538 * Sets the active state of the summary. When active, the summary subscribes
michael@0 1539 * to the DownloadsCommon DownloadsSummaryData singleton.
michael@0 1540 *
michael@0 1541 * @param aActive
michael@0 1542 * Set to true to activate the summary.
michael@0 1543 */
michael@0 1544 set active(aActive)
michael@0 1545 {
michael@0 1546 if (aActive == this._active || !this._summaryNode) {
michael@0 1547 return this._active;
michael@0 1548 }
michael@0 1549 if (aActive) {
michael@0 1550 DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit)
michael@0 1551 .refreshView(this);
michael@0 1552 } else {
michael@0 1553 DownloadsFooter.showingSummary = false;
michael@0 1554 }
michael@0 1555
michael@0 1556 return this._active = aActive;
michael@0 1557 },
michael@0 1558
michael@0 1559 /**
michael@0 1560 * Returns the active state of the downloads summary.
michael@0 1561 */
michael@0 1562 get active() this._active,
michael@0 1563
michael@0 1564 _active: false,
michael@0 1565
michael@0 1566 /**
michael@0 1567 * Sets whether or not we show the progress bar.
michael@0 1568 *
michael@0 1569 * @param aShowingProgress
michael@0 1570 * True if we should show the progress bar.
michael@0 1571 */
michael@0 1572 set showingProgress(aShowingProgress)
michael@0 1573 {
michael@0 1574 if (aShowingProgress) {
michael@0 1575 this._summaryNode.setAttribute("inprogress", "true");
michael@0 1576 } else {
michael@0 1577 this._summaryNode.removeAttribute("inprogress");
michael@0 1578 }
michael@0 1579 // If progress isn't being shown, then we simply do not show the summary.
michael@0 1580 return DownloadsFooter.showingSummary = aShowingProgress;
michael@0 1581 },
michael@0 1582
michael@0 1583 /**
michael@0 1584 * Sets the amount of progress that is visible in the progress bar.
michael@0 1585 *
michael@0 1586 * @param aValue
michael@0 1587 * A value between 0 and 100 to represent the progress of the
michael@0 1588 * summarized downloads.
michael@0 1589 */
michael@0 1590 set percentComplete(aValue)
michael@0 1591 {
michael@0 1592 if (this._progressNode) {
michael@0 1593 this._progressNode.setAttribute("value", aValue);
michael@0 1594 }
michael@0 1595 return aValue;
michael@0 1596 },
michael@0 1597
michael@0 1598 /**
michael@0 1599 * Sets the description for the download summary.
michael@0 1600 *
michael@0 1601 * @param aValue
michael@0 1602 * A string representing the description of the summarized
michael@0 1603 * downloads.
michael@0 1604 */
michael@0 1605 set description(aValue)
michael@0 1606 {
michael@0 1607 if (this._descriptionNode) {
michael@0 1608 this._descriptionNode.setAttribute("value", aValue);
michael@0 1609 this._descriptionNode.setAttribute("tooltiptext", aValue);
michael@0 1610 }
michael@0 1611 return aValue;
michael@0 1612 },
michael@0 1613
michael@0 1614 /**
michael@0 1615 * Sets the details for the download summary, such as the time remaining,
michael@0 1616 * the amount of bytes transferred, etc.
michael@0 1617 *
michael@0 1618 * @param aValue
michael@0 1619 * A string representing the details of the summarized
michael@0 1620 * downloads.
michael@0 1621 */
michael@0 1622 set details(aValue)
michael@0 1623 {
michael@0 1624 if (this._detailsNode) {
michael@0 1625 this._detailsNode.setAttribute("value", aValue);
michael@0 1626 this._detailsNode.setAttribute("tooltiptext", aValue);
michael@0 1627 }
michael@0 1628 return aValue;
michael@0 1629 },
michael@0 1630
michael@0 1631 /**
michael@0 1632 * Focuses the root element of the summary.
michael@0 1633 */
michael@0 1634 focus: function()
michael@0 1635 {
michael@0 1636 if (this._summaryNode) {
michael@0 1637 this._summaryNode.focus();
michael@0 1638 }
michael@0 1639 },
michael@0 1640
michael@0 1641 /**
michael@0 1642 * Respond to keydown events on the Downloads Summary node.
michael@0 1643 *
michael@0 1644 * @param aEvent
michael@0 1645 * The keydown event being handled.
michael@0 1646 */
michael@0 1647 onKeyDown: function DS_onKeyDown(aEvent)
michael@0 1648 {
michael@0 1649 if (aEvent.charCode == " ".charCodeAt(0) ||
michael@0 1650 aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
michael@0 1651 DownloadsPanel.showDownloadsHistory();
michael@0 1652 }
michael@0 1653 },
michael@0 1654
michael@0 1655 /**
michael@0 1656 * Respond to click events on the Downloads Summary node.
michael@0 1657 *
michael@0 1658 * @param aEvent
michael@0 1659 * The click event being handled.
michael@0 1660 */
michael@0 1661 onClick: function DS_onClick(aEvent)
michael@0 1662 {
michael@0 1663 DownloadsPanel.showDownloadsHistory();
michael@0 1664 },
michael@0 1665
michael@0 1666 /**
michael@0 1667 * Element corresponding to the root of the downloads summary.
michael@0 1668 */
michael@0 1669 get _summaryNode()
michael@0 1670 {
michael@0 1671 let node = document.getElementById("downloadsSummary");
michael@0 1672 if (!node) {
michael@0 1673 return null;
michael@0 1674 }
michael@0 1675 delete this._summaryNode;
michael@0 1676 return this._summaryNode = node;
michael@0 1677 },
michael@0 1678
michael@0 1679 /**
michael@0 1680 * Element corresponding to the progress bar in the downloads summary.
michael@0 1681 */
michael@0 1682 get _progressNode()
michael@0 1683 {
michael@0 1684 let node = document.getElementById("downloadsSummaryProgress");
michael@0 1685 if (!node) {
michael@0 1686 return null;
michael@0 1687 }
michael@0 1688 delete this._progressNode;
michael@0 1689 return this._progressNode = node;
michael@0 1690 },
michael@0 1691
michael@0 1692 /**
michael@0 1693 * Element corresponding to the main description of the downloads
michael@0 1694 * summary.
michael@0 1695 */
michael@0 1696 get _descriptionNode()
michael@0 1697 {
michael@0 1698 let node = document.getElementById("downloadsSummaryDescription");
michael@0 1699 if (!node) {
michael@0 1700 return null;
michael@0 1701 }
michael@0 1702 delete this._descriptionNode;
michael@0 1703 return this._descriptionNode = node;
michael@0 1704 },
michael@0 1705
michael@0 1706 /**
michael@0 1707 * Element corresponding to the secondary description of the downloads
michael@0 1708 * summary.
michael@0 1709 */
michael@0 1710 get _detailsNode()
michael@0 1711 {
michael@0 1712 let node = document.getElementById("downloadsSummaryDetails");
michael@0 1713 if (!node) {
michael@0 1714 return null;
michael@0 1715 }
michael@0 1716 delete this._detailsNode;
michael@0 1717 return this._detailsNode = node;
michael@0 1718 }
michael@0 1719 }
michael@0 1720
michael@0 1721 ////////////////////////////////////////////////////////////////////////////////
michael@0 1722 //// DownloadsFooter
michael@0 1723
michael@0 1724 /**
michael@0 1725 * Manages events sent to to the footer vbox, which contains both the
michael@0 1726 * DownloadsSummary as well as the "Show All Downloads" button.
michael@0 1727 */
michael@0 1728 const DownloadsFooter = {
michael@0 1729
michael@0 1730 /**
michael@0 1731 * Focuses the appropriate element within the footer. If the summary
michael@0 1732 * is visible, focus it. If not, focus the "Show All Downloads"
michael@0 1733 * button.
michael@0 1734 */
michael@0 1735 focus: function DF_focus()
michael@0 1736 {
michael@0 1737 if (this._showingSummary) {
michael@0 1738 DownloadsSummary.focus();
michael@0 1739 } else {
michael@0 1740 DownloadsView.downloadsHistory.focus();
michael@0 1741 }
michael@0 1742 },
michael@0 1743
michael@0 1744 _showingSummary: false,
michael@0 1745
michael@0 1746 /**
michael@0 1747 * Sets whether or not the Downloads Summary should be displayed in the
michael@0 1748 * footer. If not, the "Show All Downloads" button is shown instead.
michael@0 1749 */
michael@0 1750 set showingSummary(aValue)
michael@0 1751 {
michael@0 1752 if (this._footerNode) {
michael@0 1753 if (aValue) {
michael@0 1754 this._footerNode.setAttribute("showingsummary", "true");
michael@0 1755 } else {
michael@0 1756 this._footerNode.removeAttribute("showingsummary");
michael@0 1757 }
michael@0 1758 this._showingSummary = aValue;
michael@0 1759 }
michael@0 1760 return aValue;
michael@0 1761 },
michael@0 1762
michael@0 1763 /**
michael@0 1764 * Element corresponding to the footer of the downloads panel.
michael@0 1765 */
michael@0 1766 get _footerNode()
michael@0 1767 {
michael@0 1768 let node = document.getElementById("downloadsFooter");
michael@0 1769 if (!node) {
michael@0 1770 return null;
michael@0 1771 }
michael@0 1772 delete this._footerNode;
michael@0 1773 return this._footerNode = node;
michael@0 1774 }
michael@0 1775 };

mercurial