downloads.js (59057B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 /* vim: set ts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 /** 8 * Handles the Downloads panel user interface for each browser window. 9 * 10 * This file includes the following constructors and global objects: 11 * 12 * DownloadsPanel 13 * Main entry point for the downloads panel interface. 14 * 15 * DownloadsView 16 * Builds and updates the downloads list widget, responding to changes in the 17 * download state and real-time data. In addition, handles part of the user 18 * interaction events raised by the downloads list widget. 19 * 20 * DownloadsViewItem 21 * Builds and updates a single item in the downloads list widget, responding to 22 * changes in the download state and real-time data, and handles the user 23 * interaction events related to a single item in the downloads list widgets. 24 * 25 * DownloadsViewController 26 * Handles part of the user interaction events raised by the downloads list 27 * widget, in particular the "commands" that apply to multiple items, and 28 * dispatches the commands that apply to individual items. 29 */ 30 31 "use strict"; 32 33 var { XPCOMUtils } = ChromeUtils.importESModule( 34 "resource://gre/modules/XPCOMUtils.sys.mjs" 35 ); 36 37 ChromeUtils.defineESModuleGetters(this, { 38 DownloadsViewUI: 39 "moz-src:///browser/components/downloads/DownloadsViewUI.sys.mjs", 40 FileUtils: "resource://gre/modules/FileUtils.sys.mjs", 41 NetUtil: "resource://gre/modules/NetUtil.sys.mjs", 42 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 43 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 44 DownloadsTorWarning: 45 "moz-src:///browser/components/downloads/DownloadsTorWarning.sys.mjs", 46 }); 47 48 const { Integration } = ChromeUtils.importESModule( 49 "resource://gre/modules/Integration.sys.mjs" 50 ); 51 52 /* global DownloadIntegration */ 53 Integration.downloads.defineESModuleGetter( 54 this, 55 "DownloadIntegration", 56 "resource://gre/modules/DownloadIntegration.sys.mjs" 57 ); 58 59 // DownloadsPanel 60 61 /** 62 * Main entry point for the downloads panel interface. 63 */ 64 var DownloadsPanel = { 65 // Initialization and termination 66 67 /** 68 * Timeout that re-enables previously disabled download items in the downloads panel 69 * after some time has passed. 70 */ 71 _delayTimeout: null, 72 73 /** The panel is not linked to downloads data yet. */ 74 _initialized: false, 75 76 /** The panel will be shown as soon as data is available. */ 77 _waitingDataForOpen: false, 78 79 /** 80 * Tracks whether to show the tor warning or not. 81 * 82 * @type {?DownloadsTorWarning} 83 */ 84 _torWarning: null, 85 86 /** 87 * Starts loading the download data in background, without opening the panel. 88 * Use showPanel instead to load the data and open the panel at the same time. 89 */ 90 initialize() { 91 DownloadsCommon.log( 92 "Attempting to initialize DownloadsPanel for a window." 93 ); 94 95 if (DownloadIntegration.downloadSpamProtection) { 96 DownloadIntegration.downloadSpamProtection.register( 97 DownloadsView, 98 window 99 ); 100 } 101 102 if (!this._torWarning) { 103 this._torWarning = new DownloadsTorWarning( 104 document.getElementById("downloadsPanelTorWarning"), 105 true, 106 () => { 107 // Re-assign focus that was lost. 108 this._focusPanel(true); 109 }, 110 () => { 111 this.hidePanel(); 112 } 113 ); 114 } 115 this._torWarning.activate(); 116 117 if (this._initialized) { 118 DownloadsCommon.log("DownloadsPanel is already initialized."); 119 return; 120 } 121 this._initialized = true; 122 123 window.addEventListener("unload", this.onWindowUnload); 124 const downloadPanelCommands = document.getElementById( 125 "downloadPanelCommands" 126 ); 127 downloadPanelCommands.addEventListener("command", this); 128 downloadPanelCommands.addEventListener("commandupdate", () => { 129 goUpdateCommand("cmd_delete"); 130 }); 131 132 // Load and resume active downloads if required. If there are downloads to 133 // be shown in the panel, they will be loaded asynchronously. 134 DownloadsCommon.initializeAllDataLinks(); 135 136 // Now that data loading has eventually started, load the required XUL 137 // elements and initialize our views. 138 139 this.panel.hidden = false; 140 DownloadsViewController.initialize(); 141 DownloadsCommon.log("Attaching DownloadsView..."); 142 DownloadsCommon.getData(window).addView(DownloadsView); 143 DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit).addView( 144 DownloadsSummary 145 ); 146 147 DownloadsCommon.log( 148 "DownloadsView attached - the panel for this window", 149 "should now see download items come in." 150 ); 151 DownloadsPanel._attachEventListeners(); 152 DownloadsCommon.log("DownloadsPanel initialized."); 153 }, 154 155 /** 156 * Closes the downloads panel and frees the internal resources related to the 157 * downloads. The downloads panel can be reopened later, even after this 158 * function has been called. 159 */ 160 terminate() { 161 DownloadsCommon.log("Attempting to terminate DownloadsPanel for a window."); 162 if (!this._initialized) { 163 DownloadsCommon.log( 164 "DownloadsPanel was never initialized. Nothing to do." 165 ); 166 return; 167 } 168 169 window.removeEventListener("unload", this.onWindowUnload); 170 document 171 .getElementById("downloadPanelCommands") 172 .removeEventListener("command", this); 173 174 // Ensure that the panel is closed before shutting down. 175 this.hidePanel(); 176 177 DownloadsViewController.terminate(); 178 DownloadsCommon.getData(window).removeView(DownloadsView); 179 DownloadsCommon.getSummary( 180 window, 181 DownloadsView.kItemCountLimit 182 ).removeView(DownloadsSummary); 183 this._unattachEventListeners(); 184 185 if (DownloadIntegration.downloadSpamProtection) { 186 DownloadIntegration.downloadSpamProtection.unregister(window); 187 } 188 189 this._torWarning?.deactivate(); 190 191 this._initialized = false; 192 193 DownloadsSummary.active = false; 194 DownloadsCommon.log("DownloadsPanel terminated."); 195 }, 196 197 // Panel interface 198 199 /** 200 * Main panel element in the browser window. 201 */ 202 get panel() { 203 delete this.panel; 204 return (this.panel = document.getElementById("downloadsPanel")); 205 }, 206 207 /** 208 * Starts opening the downloads panel interface, anchored to the downloads 209 * button of the browser window. The list of downloads to display is 210 * initialized the first time this method is called, and the panel is shown 211 * only when data is ready. 212 */ 213 showPanel(openedManually = false, isKeyPress = false) { 214 Glean.downloads.panelShown.add(1); 215 216 DownloadsCommon.log("Opening the downloads panel."); 217 218 this._openedManually = openedManually; 219 this._preventFocusRing = !openedManually || !isKeyPress; 220 221 if (this.isPanelShowing) { 222 DownloadsCommon.log("Panel is already showing - focusing instead."); 223 this._focusPanel(); 224 return; 225 } 226 227 // As a belt-and-suspenders check, ensure the button is not hidden. 228 DownloadsButton.unhide(); 229 230 this.initialize(); 231 // Delay displaying the panel because this function will sometimes be 232 // called while another window is closing (like the window for selecting 233 // whether to save or open the file), and that would cause the panel to 234 // close immediately. 235 setTimeout(() => this._openPopupIfDataReady(), 0); 236 237 DownloadsCommon.log("Waiting for the downloads panel to appear."); 238 this._waitingDataForOpen = true; 239 }, 240 241 /** 242 * Hides the downloads panel, if visible, but keeps the internal state so that 243 * the panel can be reopened quickly if required. 244 */ 245 hidePanel() { 246 DownloadsCommon.log("Closing the downloads panel."); 247 248 if (!this.isPanelShowing) { 249 DownloadsCommon.log("Downloads panel is not showing - nothing to do."); 250 return; 251 } 252 253 PanelMultiView.hidePopup(this.panel); 254 DownloadsCommon.log("Downloads panel is now closed."); 255 }, 256 257 /** 258 * Indicates whether the panel is showing. 259 * 260 * Note: this includes the hiding state. 261 */ 262 get isPanelShowing() { 263 return this._waitingDataForOpen || this.panel.state != "closed"; 264 }, 265 266 handleEvent(aEvent) { 267 switch (aEvent.type) { 268 case "click": 269 DownloadsPanel.showDownloadsHistory(); 270 break; 271 case "command": 272 if (aEvent.currentTarget == DownloadsView.downloadsHistory) { 273 DownloadsPanel.showDownloadsHistory(); 274 return; 275 } 276 277 if ( 278 aEvent.currentTarget == DownloadsBlockedSubview.elements.deleteButton 279 ) { 280 DownloadsBlockedSubview.confirmBlock(); 281 return; 282 } 283 284 // Handle the commands defined in downloadsPanel.inc.xhtml. 285 // Every command "id" is also its corresponding command. 286 goDoCommand(aEvent.target.id); 287 break; 288 case "mousemove": 289 if ( 290 !DownloadsView.contextMenuOpen && 291 !DownloadsView.subViewOpen && 292 this.panel.contains(document.activeElement) 293 ) { 294 // Let mouse movement remove focus rings and reset focus in the panel. 295 // This behavior is copied from PanelMultiView. 296 document.activeElement.blur(); 297 DownloadsView.richListBox.removeAttribute("force-focus-visible"); 298 this._preventFocusRing = true; 299 this._focusPanel(); 300 } 301 break; 302 case "mouseover": 303 DownloadsView._onDownloadMouseOver(aEvent); 304 break; 305 case "mouseout": 306 DownloadsView._onDownloadMouseOut(aEvent); 307 break; 308 case "contextmenu": 309 DownloadsView._onDownloadContextMenu(aEvent); 310 break; 311 case "dragstart": 312 DownloadsView._onDownloadDragStart(aEvent); 313 break; 314 case "mousedown": 315 if (DownloadsView.richListBox.hasAttribute("disabled")) { 316 this._handlePotentiallySpammyDownloadActivation(aEvent); 317 } 318 break; 319 320 case "keydown": 321 if (aEvent.currentTarget == DownloadsSummary._summaryNode) { 322 DownloadsSummary._onKeyDown(aEvent); 323 return; 324 } 325 326 this._onKeyDown(aEvent); 327 break; 328 case "keypress": 329 this._onKeyPress(aEvent); 330 break; 331 case "focus": 332 case "select": 333 this._onSelect(aEvent); 334 break; 335 case "popupshown": 336 this._onPopupShown(aEvent); 337 break; 338 case "popuphidden": 339 this._onPopupHidden(aEvent); 340 break; 341 } 342 }, 343 344 // Callback functions from DownloadsView 345 346 /** 347 * Called after data loading finished. 348 */ 349 onViewLoadCompleted() { 350 this._openPopupIfDataReady(); 351 }, 352 353 // User interface event functions 354 355 onWindowUnload() { 356 // This function is registered as an event listener, we can't use "this". 357 DownloadsPanel.terminate(); 358 }, 359 360 _onPopupShown(aEvent) { 361 // Ignore events raised by nested popups. 362 if (aEvent.target != this.panel) { 363 return; 364 } 365 366 DownloadsCommon.log("Downloads panel has shown."); 367 368 // Since at most one popup is open at any given time, we can set globally. 369 DownloadsCommon.getIndicatorData(window).attentionSuppressed |= 370 DownloadsCommon.SUPPRESS_PANEL_OPEN; 371 372 // Ensure that the first item is selected when the panel is focused. 373 if (DownloadsView.richListBox.itemCount > 0) { 374 DownloadsView.richListBox.selectedIndex = 0; 375 } 376 377 this._focusPanel(); 378 }, 379 380 _onPopupHidden(aEvent) { 381 // Ignore events raised by nested popups. 382 if (aEvent.target != this.panel) { 383 return; 384 } 385 386 DownloadsCommon.log("Downloads panel has hidden."); 387 388 if (this._delayTimeout) { 389 DownloadsView.richListBox.removeAttribute("disabled"); 390 clearTimeout(this._delayTimeout); 391 this._stopWatchingForSpammyDownloadActivation(); 392 this._delayTimeout = null; 393 } 394 395 DownloadsView.richListBox.removeAttribute("force-focus-visible"); 396 397 // Since at most one popup is open at any given time, we can set globally. 398 DownloadsCommon.getIndicatorData(window).attentionSuppressed &= 399 ~DownloadsCommon.SUPPRESS_PANEL_OPEN; 400 401 // Allow the anchor to be hidden. 402 DownloadsButton.releaseAnchor(); 403 }, 404 405 // Related operations 406 407 /** 408 * Shows or focuses the user interface dedicated to downloads history. 409 */ 410 showDownloadsHistory() { 411 DownloadsCommon.log("Showing download history."); 412 // Hide the panel before showing another window, otherwise focus will return 413 // to the browser window when the panel closes automatically. 414 this.hidePanel(); 415 416 BrowserCommands.downloadsUI(); 417 }, 418 419 // Internal functions 420 421 /** 422 * Attach event listeners to a panel element. These listeners should be 423 * removed in _unattachEventListeners. This is called automatically after the 424 * panel has successfully loaded. 425 */ 426 _attachEventListeners() { 427 // Handle keydown to support accel-V. 428 this.panel.addEventListener("keydown", this); 429 // Handle keypress to be able to preventDefault() events before they reach 430 // the richlistbox, for keyboard navigation. 431 this.panel.addEventListener("keypress", this); 432 // Handle mousedown to be able to notice clicks on disabled items. 433 this.panel.addEventListener("mousedown", this); 434 this.panel.addEventListener("mousemove", this); 435 this.panel.addEventListener("popupshown", this); 436 this.panel.addEventListener("popuphidden", this); 437 DownloadsView.richListBox.addEventListener("focus", this); 438 DownloadsView.richListBox.addEventListener("select", this); 439 DownloadsView.richListBox.addEventListener("mouseover", this); 440 DownloadsView.richListBox.addEventListener("mouseout", this); 441 DownloadsView.richListBox.addEventListener("contextmenu", this); 442 DownloadsView.richListBox.addEventListener("dragstart", this); 443 444 DownloadsView.downloadsHistory.addEventListener("command", this); 445 DownloadsBlockedSubview.elements.deleteButton.addEventListener( 446 "command", 447 this 448 ); 449 DownloadsSummary._summaryNode.addEventListener("click", this); 450 DownloadsSummary._summaryNode.addEventListener("keydown", this); 451 }, 452 453 /** 454 * Unattach event listeners that were added in _attachEventListeners. This 455 * is called automatically on panel termination. 456 */ 457 _unattachEventListeners() { 458 this.panel.removeEventListener("keydown", this); 459 this.panel.removeEventListener("keypress", this); 460 this.panel.removeEventListener("mousedown", this); 461 this.panel.removeEventListener("mousemove", this); 462 this.panel.removeEventListener("popupshown", this); 463 this.panel.removeEventListener("popuphidden", this); 464 DownloadsView.richListBox.removeEventListener("focus", this); 465 DownloadsView.richListBox.removeEventListener("select", this); 466 DownloadsView.richListBox.removeEventListener("mouseover", this); 467 DownloadsView.richListBox.removeEventListener("mouseout", this); 468 DownloadsView.richListBox.removeEventListener("contextmenu", this); 469 DownloadsView.richListBox.removeEventListener("dragstart", this); 470 DownloadsView.downloadsHistory.removeEventListener("command", this); 471 DownloadsBlockedSubview.elements.deleteButton.removeEventListener( 472 "command", 473 this 474 ); 475 DownloadsSummary._summaryNode.removeEventListener("click", this); 476 DownloadsSummary._summaryNode.removeEventListener("keydown", this); 477 }, 478 479 _onKeyPress(aEvent) { 480 // Handle unmodified keys only. 481 if (aEvent.altKey || aEvent.ctrlKey || aEvent.shiftKey || aEvent.metaKey) { 482 return; 483 } 484 485 // Pass keypress events to the richlistbox view when it's focused. 486 if (document.activeElement === DownloadsView.richListBox) { 487 DownloadsView.onDownloadKeyPress(aEvent); 488 } 489 }, 490 491 /** 492 * Keydown listener that listens for the keys to start key focusing, as well 493 * as the the accel-V "paste" event, which initiates a file download if the 494 * pasted item can be resolved to a URI. 495 */ 496 _onKeyDown(aEvent) { 497 if (DownloadsView.richListBox.hasAttribute("disabled")) { 498 this._handlePotentiallySpammyDownloadActivation(aEvent); 499 return; 500 } 501 502 let richListBox = DownloadsView.richListBox; 503 504 // If the user has pressed the up or down cursor key, force-enable focus 505 // indicators for the richlistbox. :focus-visible doesn't work in this case 506 // because the the focused element may not change here if the richlistbox 507 // already had focus. The force-focus-visible attribute will be removed 508 // again if the user moves the mouse on the panel or if the panel is closed. 509 if ( 510 aEvent.keyCode == aEvent.DOM_VK_UP || 511 aEvent.keyCode == aEvent.DOM_VK_DOWN 512 ) { 513 richListBox.setAttribute("force-focus-visible", "true"); 514 } 515 516 // If the footer is focused and the downloads list has at least 1 element 517 // in it, focus the last element in the list when going up. 518 if (aEvent.keyCode == aEvent.DOM_VK_UP && richListBox.firstElementChild) { 519 if ( 520 document 521 .getElementById("downloadsFooter") 522 .contains(document.activeElement) 523 ) { 524 richListBox.selectedItem = richListBox.lastElementChild; 525 richListBox.focus(); 526 aEvent.preventDefault(); 527 return; 528 } 529 } 530 531 if (aEvent.keyCode == aEvent.DOM_VK_DOWN) { 532 // If the last element in the list is selected, or the footer is already 533 // focused, focus the footer. 534 if ( 535 DownloadsView.canChangeSelectedItem && 536 (richListBox.selectedItem === richListBox.lastElementChild || 537 document 538 .getElementById("downloadsFooter") 539 .contains(document.activeElement)) 540 ) { 541 richListBox.selectedIndex = -1; 542 DownloadsFooter.focus(); 543 aEvent.preventDefault(); 544 return; 545 } 546 } 547 548 let pasting = 549 aEvent.keyCode == aEvent.DOM_VK_V && aEvent.getModifierState("Accel"); 550 551 if (!pasting) { 552 return; 553 } 554 555 DownloadsCommon.log("Received a paste event."); 556 557 let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( 558 Ci.nsITransferable 559 ); 560 trans.init(null); 561 let flavors = ["text/x-moz-url", "text/plain"]; 562 flavors.forEach(trans.addDataFlavor); 563 Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard); 564 // Getting the data or creating the nsIURI might fail 565 try { 566 let data = {}; 567 trans.getAnyTransferData({}, data); 568 let [url, name] = data.value 569 .QueryInterface(Ci.nsISupportsString) 570 .data.split("\n"); 571 if (!url) { 572 return; 573 } 574 575 let uri = NetUtil.newURI(url); 576 DownloadsCommon.log("Pasted URL seems valid. Starting download."); 577 DownloadURL(uri.spec, name, document); 578 } catch (ex) {} 579 }, 580 581 _onSelect() { 582 let richlistbox = DownloadsView.richListBox; 583 richlistbox.itemChildren.forEach(item => { 584 let button = item.querySelector("button"); 585 if (item.selected) { 586 button.removeAttribute("tabindex"); 587 } else { 588 button.setAttribute("tabindex", -1); 589 } 590 }); 591 }, 592 593 /** 594 * Move focus to the main element in the downloads panel, unless another 595 * element in the panel is already focused. 596 * 597 * @param {bool} [forceFocus=false] - Whether to force move the focus. 598 */ 599 _focusPanel(forceFocus = false) { 600 if (!forceFocus) { 601 // We may be invoked while the panel is still waiting to be shown. 602 if (this.panel.state != "open") { 603 return; 604 } 605 606 if ( 607 document.activeElement && 608 (this.panel.contains(document.activeElement) || 609 this.panel.shadowRoot.contains(document.activeElement)) 610 ) { 611 return; 612 } 613 } 614 615 let focusOptions = {}; 616 if (this._preventFocusRing) { 617 focusOptions.focusVisible = false; 618 } 619 620 // Focus the "Got it" button if it is visible. 621 // This should ensure that the alert is read aloud by Orca when the 622 // downloads panel is opened. See tor-browser#42642. 623 if (!this._torWarning?.hidden) { 624 this._torWarning.dismissButton.focus(focusOptions); 625 return; 626 } 627 628 if (DownloadsView.richListBox.itemCount > 0) { 629 if (DownloadsView.canChangeSelectedItem) { 630 DownloadsView.richListBox.selectedIndex = 0; 631 } 632 DownloadsView.richListBox.focus(focusOptions); 633 } else { 634 DownloadsFooter.focus(focusOptions); 635 } 636 }, 637 638 _delayPopupItems() { 639 DownloadsView.richListBox.setAttribute("disabled", true); 640 this._startWatchingForSpammyDownloadActivation(); 641 642 this._refreshDelayTimer(); 643 }, 644 645 _refreshDelayTimer() { 646 // If timeout already exists, overwrite it to avoid multiple timeouts. 647 if (this._delayTimeout) { 648 clearTimeout(this._delayTimeout); 649 } 650 651 let delay = Services.prefs.getIntPref("security.dialog_enable_delay"); 652 this._delayTimeout = setTimeout(() => { 653 DownloadsView.richListBox.removeAttribute("disabled"); 654 this._stopWatchingForSpammyDownloadActivation(); 655 this._focusPanel(); 656 this._delayTimeout = null; 657 }, delay); 658 }, 659 660 _startWatchingForSpammyDownloadActivation() { 661 window.addEventListener("keydown", this, { 662 capture: true, 663 mozSystemGroup: true, 664 }); 665 }, 666 667 _lastBeepTime: 0, 668 _handlePotentiallySpammyDownloadActivation(aEvent) { 669 let isSpammyKey = 670 aEvent.type.startsWith("key") && 671 (aEvent.key == "Enter" || aEvent.key == " "); 672 let isSpammyMouse = aEvent.type.startsWith("mouse") && aEvent.button == 0; 673 if (isSpammyKey || isSpammyMouse) { 674 // Throttle our beeping to a maximum of once per second, otherwise it 675 // appears on Win10 that beeps never make it through at all. 676 if (Date.now() - this._lastBeepTime > 1000) { 677 Cc["@mozilla.org/sound;1"].getService(Ci.nsISound).beep(); 678 this._lastBeepTime = Date.now(); 679 } 680 681 this._refreshDelayTimer(); 682 } 683 }, 684 685 _stopWatchingForSpammyDownloadActivation() { 686 window.removeEventListener("keydown", this, { 687 capture: true, 688 mozSystemGroup: true, 689 }); 690 }, 691 692 /** 693 * Opens the downloads panel when data is ready to be displayed. 694 */ 695 _openPopupIfDataReady() { 696 // We don't want to open the popup if we already displayed it, or if we are 697 // still loading data. 698 if (!this._waitingDataForOpen || DownloadsView.loading) { 699 return; 700 } 701 this._waitingDataForOpen = false; 702 703 // At this point, if the window is minimized, opening the panel could fail 704 // without any notification, and there would be no way to either open or 705 // close the panel any more. To prevent this, check if the window is 706 // minimized and in that case force the panel to the closed state. 707 if (window.windowState == window.STATE_MINIMIZED) { 708 return; 709 } 710 711 // Ensure the anchor is visible. If that is not possible, show the panel 712 // anchored to the top area of the window, near the default anchor position. 713 let anchor = DownloadsButton.getAnchor(); 714 715 if (!anchor) { 716 DownloadsCommon.error("Downloads button cannot be found."); 717 return; 718 } 719 720 let onBookmarksToolbar = !!anchor.closest("#PersonalToolbar"); 721 this.panel.classList.toggle("bookmarks-toolbar", onBookmarksToolbar); 722 723 // When the panel is opened, we check if the target files of visible items 724 // still exist, and update the allowed items interactions accordingly. We 725 // do these checks on a background thread, and don't prevent the panel to 726 // be displayed while these checks are being performed. 727 for (let viewItem of DownloadsView._visibleViewItems.values()) { 728 viewItem.download.refresh().catch(console.error); 729 } 730 731 DownloadsCommon.log("Opening downloads panel popup."); 732 733 // Delay displaying the panel because this function will sometimes be 734 // called while another window is closing (like the window for selecting 735 // whether to save or open the file), and that would cause the panel to 736 // close immediately. 737 setTimeout(() => { 738 PanelMultiView.openPopup( 739 this.panel, 740 anchor, 741 "bottomright topright", 742 0, 743 0, 744 false, 745 null 746 ).then(() => { 747 if (!this._openedManually) { 748 this._delayPopupItems(); 749 } 750 751 let isPrivate = 752 window && PrivateBrowsingUtils.isContentWindowPrivate(window); 753 754 if ( 755 // If private, show message asking whether to delete files at end of session 756 isPrivate && 757 Services.prefs.getBoolPref( 758 "browser.download.enableDeletePrivate", 759 false 760 ) && 761 !Services.prefs.getBoolPref( 762 "browser.download.deletePrivate.chosen", 763 false 764 ) 765 ) { 766 PrivateDownloadsSubview.openWhenReady(); 767 } 768 }, console.error); 769 }, 0); 770 }, 771 }; 772 773 XPCOMUtils.defineConstant(this, "DownloadsPanel", DownloadsPanel); 774 775 // DownloadsView 776 777 /** 778 * Builds and updates the downloads list widget, responding to changes in the 779 * download state and real-time data. In addition, handles part of the user 780 * interaction events raised by the downloads list widget. 781 */ 782 var DownloadsView = { 783 // Functions handling download items in the list 784 785 /** 786 * Maximum number of items shown by the list at any given time. 787 */ 788 kItemCountLimit: 5, 789 790 /** 791 * Indicates whether there is a DownloadsBlockedSubview open. 792 */ 793 subViewOpen: false, 794 795 /** 796 * Indicates whether we are still loading downloads data asynchronously. 797 */ 798 loading: false, 799 800 /** 801 * Ordered array of all Download objects. We need to keep this array because 802 * only a limited number of items are shown at once, and if an item that is 803 * currently visible is removed from the list, we might need to take another 804 * item from the array and make it appear at the bottom. 805 */ 806 _downloads: [], 807 808 /** 809 * Associates the visible Download objects with their corresponding 810 * DownloadsViewItem object. There is a limited number of view items in the 811 * panel at any given time. 812 */ 813 _visibleViewItems: new Map(), 814 815 /** 816 * Called when the number of items in the list changes. 817 */ 818 _itemCountChanged() { 819 DownloadsCommon.log( 820 "The downloads item count has changed - we are tracking", 821 this._downloads.length, 822 "downloads in total." 823 ); 824 let count = this._downloads.length; 825 let hiddenCount = count - this.kItemCountLimit; 826 827 if (count > 0) { 828 DownloadsCommon.log( 829 "Setting the panel's hasdownloads attribute to true." 830 ); 831 DownloadsPanel.panel.setAttribute("hasdownloads", "true"); 832 } else { 833 DownloadsCommon.log("Removing the panel's hasdownloads attribute."); 834 DownloadsPanel.panel.removeAttribute("hasdownloads"); 835 } 836 837 // If we've got some hidden downloads, we should activate the 838 // DownloadsSummary. The DownloadsSummary will determine whether or not 839 // it's appropriate to actually display the summary. 840 DownloadsSummary.active = hiddenCount > 0; 841 }, 842 843 /** 844 * Element corresponding to the list of downloads. 845 */ 846 get richListBox() { 847 delete this.richListBox; 848 return (this.richListBox = document.getElementById("downloadsListBox")); 849 }, 850 851 /** 852 * Element corresponding to the button for showing more downloads. 853 */ 854 get downloadsHistory() { 855 delete this.downloadsHistory; 856 return (this.downloadsHistory = 857 document.getElementById("downloadsHistory")); 858 }, 859 860 // Callback functions from DownloadsData 861 862 /** 863 * Called before multiple downloads are about to be loaded. 864 */ 865 onDownloadBatchStarting() { 866 DownloadsCommon.log("onDownloadBatchStarting called for DownloadsView."); 867 this.loading = true; 868 }, 869 870 /** 871 * Called after data loading finished. 872 */ 873 onDownloadBatchEnded() { 874 DownloadsCommon.log("onDownloadBatchEnded called for DownloadsView."); 875 876 this.loading = false; 877 878 // We suppressed item count change notifications during the batch load, at 879 // this point we should just call the function once. 880 this._itemCountChanged(); 881 882 // Notify the panel that all the initially available downloads have been 883 // loaded. This ensures that the interface is visible, if still required. 884 DownloadsPanel.onViewLoadCompleted(); 885 }, 886 887 /** 888 * Called when a new download data item is available, either during the 889 * asynchronous data load or when a new download is started. 890 * 891 * @param aDownload 892 * Download object that was just added. 893 */ 894 onDownloadAdded(download) { 895 DownloadsCommon.log("A new download data item was added"); 896 897 this._downloads.unshift(download); 898 899 // The newly added item is visible in the panel and we must add the 900 // corresponding element. If the list overflows, remove the last item from 901 // the panel to make room for the new one that we just added at the top. 902 this._addViewItem(download, true); 903 if (this._downloads.length > this.kItemCountLimit) { 904 this._removeViewItem(this._downloads[this.kItemCountLimit]); 905 } 906 907 // For better performance during batch loads, don't update the count for 908 // every item, because the interface won't be visible until load finishes. 909 if (!this.loading) { 910 this._itemCountChanged(); 911 } 912 }, 913 914 onDownloadChanged(download) { 915 let viewItem = this._visibleViewItems.get(download); 916 if (viewItem) { 917 viewItem.onChanged(); 918 } 919 }, 920 921 /** 922 * Called when a data item is removed. Ensures that the widget associated 923 * with the view item is removed from the user interface. 924 * 925 * @param download 926 * Download object that is being removed. 927 */ 928 onDownloadRemoved(download) { 929 DownloadsCommon.log("A download data item was removed."); 930 931 let itemIndex = this._downloads.indexOf(download); 932 this._downloads.splice(itemIndex, 1); 933 934 if (itemIndex < this.kItemCountLimit) { 935 // The item to remove is visible in the panel. 936 this._removeViewItem(download); 937 if (this._downloads.length >= this.kItemCountLimit) { 938 // Reinsert the next item into the panel. 939 this._addViewItem(this._downloads[this.kItemCountLimit - 1], false); 940 } 941 } 942 943 this._itemCountChanged(); 944 }, 945 946 /** 947 * Associates each richlistitem for a download with its corresponding 948 * DownloadsViewItem object. 949 */ 950 _itemsForElements: new Map(), 951 952 itemForElement(element) { 953 return this._itemsForElements.get(element); 954 }, 955 956 /** 957 * Creates a new view item associated with the specified data item, and adds 958 * it to the top or the bottom of the list. 959 */ 960 _addViewItem(download, aNewest) { 961 DownloadsCommon.log( 962 "Adding a new DownloadsViewItem to the downloads list.", 963 "aNewest =", 964 aNewest 965 ); 966 967 let element = document.createXULElement("richlistitem"); 968 element.setAttribute("align", "center"); 969 970 let viewItem = new DownloadsViewItem(download, element); 971 this._visibleViewItems.set(download, viewItem); 972 this._itemsForElements.set(element, viewItem); 973 if (aNewest) { 974 this.richListBox.insertBefore( 975 element, 976 this.richListBox.firstElementChild 977 ); 978 } else { 979 this.richListBox.appendChild(element); 980 } 981 viewItem.ensureActive(); 982 }, 983 984 /** 985 * Removes the view item associated with the specified data item. 986 */ 987 _removeViewItem(download) { 988 DownloadsCommon.log( 989 "Removing a DownloadsViewItem from the downloads list." 990 ); 991 let element = this._visibleViewItems.get(download).element; 992 let previousSelectedIndex = this.richListBox.selectedIndex; 993 this.richListBox.removeChild(element); 994 if (previousSelectedIndex != -1) { 995 this.richListBox.selectedIndex = Math.min( 996 previousSelectedIndex, 997 this.richListBox.itemCount - 1 998 ); 999 } 1000 this._visibleViewItems.delete(download); 1001 this._itemsForElements.delete(element); 1002 }, 1003 1004 // User interface event functions 1005 1006 onDownloadClick(aEvent) { 1007 // Handle primary clicks in the main area only: 1008 if (aEvent.button == 0 && aEvent.target.closest(".downloadMainArea")) { 1009 let target = aEvent.target.closest("richlistitem"); 1010 // Ignore clicks if the box is disabled. 1011 if (target.closest("richlistbox").hasAttribute("disabled")) { 1012 return; 1013 } 1014 let download = DownloadsView.itemForElement(target).download; 1015 if (download.succeeded) { 1016 download._launchedFromPanel = true; 1017 } 1018 let command = "downloadsCmd_open"; 1019 if (download.hasBlockedData) { 1020 command = "downloadsCmd_showBlockedInfo"; 1021 } else if (aEvent.shiftKey || aEvent.ctrlKey || aEvent.metaKey) { 1022 // We adjust the command for supported modifiers to suggest where the download 1023 // may be opened 1024 let openWhere = BrowserUtils.whereToOpenLink(aEvent, false, true); 1025 if (["tab", "window", "tabshifted"].includes(openWhere)) { 1026 command += ":" + openWhere; 1027 } 1028 } 1029 // Toggle opening the file after the download has completed 1030 if (!download.stopped && command.startsWith("downloadsCmd_open")) { 1031 download.launchWhenSucceeded = !download.launchWhenSucceeded; 1032 download._launchedFromPanel = download.launchWhenSucceeded; 1033 } 1034 1035 DownloadsCommon.log("onDownloadClick, resolved command: ", command); 1036 goDoCommand(command); 1037 } 1038 }, 1039 1040 onDownloadButton(event) { 1041 let target = event.target.closest("richlistitem"); 1042 DownloadsView.itemForElement(target).onButton(); 1043 }, 1044 1045 /** 1046 * Handles keypress events on a download item. 1047 */ 1048 onDownloadKeyPress(aEvent) { 1049 // Pressing the key on buttons should not invoke the action because the 1050 // event has already been handled by the button itself. 1051 if ( 1052 aEvent.originalTarget.hasAttribute("command") || 1053 aEvent.originalTarget.hasAttribute("oncommand") 1054 ) { 1055 return; 1056 } 1057 1058 if (aEvent.charCode == " ".charCodeAt(0)) { 1059 aEvent.preventDefault(); 1060 goDoCommand("downloadsCmd_pauseResume"); 1061 return; 1062 } 1063 1064 if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) { 1065 let readyToDownload = !DownloadsView.richListBox.disabled; 1066 if (readyToDownload) { 1067 goDoCommand("downloadsCmd_doDefault"); 1068 } 1069 } 1070 }, 1071 1072 get contextMenu() { 1073 let menu = document.getElementById("downloadsContextMenu"); 1074 if (menu) { 1075 delete this.contextMenu; 1076 this.contextMenu = menu; 1077 } 1078 return menu; 1079 }, 1080 1081 /** 1082 * Indicates whether there is an open contextMenu for a download item. 1083 */ 1084 get contextMenuOpen() { 1085 return this.contextMenu.state != "closed"; 1086 }, 1087 1088 /** 1089 * Whether it's possible to change the currently selected item. 1090 */ 1091 get canChangeSelectedItem() { 1092 // When the context menu or a subview are open, the selected item should 1093 // not change. 1094 return !this.contextMenuOpen && !this.subViewOpen; 1095 }, 1096 1097 /** 1098 * Mouse listeners to handle selection on hover. 1099 */ 1100 _onDownloadMouseOver(aEvent) { 1101 let item = aEvent.target.closest("richlistitem,richlistbox"); 1102 if (item.localName != "richlistitem") { 1103 return; 1104 } 1105 1106 if (aEvent.target.classList.contains("downloadButton")) { 1107 item.classList.add("downloadHoveringButton"); 1108 } 1109 1110 item.classList.toggle( 1111 "hoveringMainArea", 1112 aEvent.target.closest(".downloadMainArea") 1113 ); 1114 1115 if (this.canChangeSelectedItem) { 1116 this.richListBox.selectedItem = item; 1117 } 1118 }, 1119 1120 _onDownloadMouseOut(aEvent) { 1121 let item = aEvent.target.closest("richlistitem,richlistbox"); 1122 if (item.localName != "richlistitem") { 1123 return; 1124 } 1125 1126 if (aEvent.target.classList.contains("downloadButton")) { 1127 item.classList.remove("downloadHoveringButton"); 1128 } 1129 1130 // If the destination element is outside of the richlistitem, clear the 1131 // selection. 1132 if (this.canChangeSelectedItem && !item.contains(aEvent.relatedTarget)) { 1133 this.richListBox.selectedIndex = -1; 1134 } 1135 }, 1136 1137 _onDownloadContextMenu(aEvent) { 1138 let element = aEvent.originalTarget.closest("richlistitem"); 1139 if (!element) { 1140 aEvent.preventDefault(); 1141 return; 1142 } 1143 // Ensure the selected item is the expected one, so commands and the 1144 // context menu are updated appropriately. 1145 this.richListBox.selectedItem = element; 1146 DownloadsViewController.updateCommands(); 1147 1148 DownloadsViewUI.updateContextMenuForElement(this.contextMenu, element); 1149 // Hide the copy location item if there is somehow no URL. We have to do 1150 // this here instead of in DownloadsViewUI because DownloadsPlacesView 1151 // allows selecting multiple downloads, so in that view the menuitem will be 1152 // shown according to whether at least one of the selected items has a URL. 1153 this.contextMenu.querySelector(".downloadCopyLocationMenuItem").hidden = 1154 !element._shell.download.source?.url; 1155 }, 1156 1157 _onDownloadDragStart(aEvent) { 1158 let element = aEvent.target.closest("richlistitem"); 1159 if (!element) { 1160 return; 1161 } 1162 1163 // We must check for existence synchronously because this is a DOM event. 1164 let file = new FileUtils.File( 1165 DownloadsView.itemForElement(element).download.target.path 1166 ); 1167 if (!file.exists()) { 1168 return; 1169 } 1170 1171 let dataTransfer = aEvent.dataTransfer; 1172 dataTransfer.mozSetDataAt("application/x-moz-file", file, 0); 1173 dataTransfer.effectAllowed = "copyMove"; 1174 let spec = NetUtil.newURI(file).spec; 1175 dataTransfer.setData("text/uri-list", spec); 1176 dataTransfer.addElement(element); 1177 1178 aEvent.stopPropagation(); 1179 }, 1180 }; 1181 1182 XPCOMUtils.defineConstant(this, "DownloadsView", DownloadsView); 1183 1184 // DownloadsViewItem 1185 1186 /** 1187 * Builds and updates a single item in the downloads list widget, responding to 1188 * changes in the download state and real-time data, and handles the user 1189 * interaction events related to a single item in the downloads list widgets. 1190 * 1191 * @param download 1192 * Download object to be associated with the view item. 1193 * @param aElement 1194 * XUL element corresponding to the single download item in the view. 1195 */ 1196 1197 class DownloadsViewItem extends DownloadsViewUI.DownloadElementShell { 1198 constructor(download, aElement) { 1199 super(); 1200 1201 this.download = download; 1202 this.element = aElement; 1203 this.element._shell = this; 1204 1205 this.element.setAttribute("type", "download"); 1206 this.element.classList.add("download-state"); 1207 1208 this.isPanel = true; 1209 } 1210 1211 onChanged() { 1212 let newState = DownloadsCommon.stateOfDownload(this.download); 1213 if (this.downloadState !== newState) { 1214 this.downloadState = newState; 1215 this._updateState(); 1216 } else { 1217 this._updateStateInner(); 1218 } 1219 } 1220 1221 isCommandEnabled(aCommand) { 1222 switch (aCommand) { 1223 case "downloadsCmd_open": 1224 case "downloadsCmd_open:current": 1225 case "downloadsCmd_open:tab": 1226 case "downloadsCmd_open:tabshifted": 1227 case "downloadsCmd_open:window": 1228 case "downloadsCmd_alwaysOpenSimilarFiles": { 1229 if (!this.download.succeeded) { 1230 return false; 1231 } 1232 1233 let file = new FileUtils.File(this.download.target.path); 1234 return file.exists(); 1235 } 1236 case "downloadsCmd_show": { 1237 let file = new FileUtils.File(this.download.target.path); 1238 if (file.exists()) { 1239 return true; 1240 } 1241 1242 if (!this.download.target.partFilePath) { 1243 return false; 1244 } 1245 1246 let partFile = new FileUtils.File(this.download.target.partFilePath); 1247 return partFile.exists(); 1248 } 1249 case "downloadsCmd_copyLocation": 1250 return !!this.download.source?.url; 1251 case "cmd_delete": 1252 case "downloadsCmd_doDefault": 1253 return true; 1254 case "downloadsCmd_showBlockedInfo": 1255 return this.download.hasBlockedData; 1256 } 1257 return DownloadsViewUI.DownloadElementShell.prototype.isCommandEnabled.call( 1258 this, 1259 aCommand 1260 ); 1261 } 1262 1263 doCommand(aCommand) { 1264 if (this.isCommandEnabled(aCommand)) { 1265 let [command, modifier] = aCommand.split(":"); 1266 // split off an optional command "modifier" into an argument, 1267 // e.g. "downloadsCmd_open:window" 1268 this[command](modifier); 1269 } 1270 } 1271 1272 // Item commands 1273 1274 downloadsCmd_unblock() { 1275 DownloadsPanel.hidePanel(); 1276 this.confirmUnblock(window, "unblock"); 1277 } 1278 1279 downloadsCmd_chooseUnblock() { 1280 DownloadsPanel.hidePanel(); 1281 this.confirmUnblock(window, "chooseUnblock"); 1282 } 1283 1284 downloadsCmd_unblockAndOpen() { 1285 DownloadsPanel.hidePanel(); 1286 this.unblockAndOpenDownload().catch(console.error); 1287 } 1288 downloadsCmd_unblockAndSave() { 1289 DownloadsPanel.hidePanel(); 1290 this.unblockAndSave(); 1291 } 1292 1293 downloadsCmd_open(openWhere) { 1294 super.downloadsCmd_open(openWhere); 1295 1296 // We explicitly close the panel here to give the user the feedback that 1297 // their click has been received, and we're handling the action. 1298 // Otherwise, we'd have to wait for the file-type handler to execute 1299 // before the panel would close. This also helps to prevent the user from 1300 // accidentally opening a file several times. 1301 DownloadsPanel.hidePanel(); 1302 } 1303 1304 downloadsCmd_openInSystemViewer() { 1305 super.downloadsCmd_openInSystemViewer(); 1306 1307 // We explicitly close the panel here to give the user the feedback that 1308 // their click has been received, and we're handling the action. 1309 DownloadsPanel.hidePanel(); 1310 } 1311 1312 downloadsCmd_alwaysOpenInSystemViewer() { 1313 super.downloadsCmd_alwaysOpenInSystemViewer(); 1314 1315 // We explicitly close the panel here to give the user the feedback that 1316 // their click has been received, and we're handling the action. 1317 DownloadsPanel.hidePanel(); 1318 } 1319 1320 downloadsCmd_alwaysOpenSimilarFiles() { 1321 super.downloadsCmd_alwaysOpenSimilarFiles(); 1322 1323 // We explicitly close the panel here to give the user the feedback that 1324 // their click has been received, and we're handling the action. 1325 DownloadsPanel.hidePanel(); 1326 } 1327 1328 downloadsCmd_show() { 1329 let file = new FileUtils.File(this.download.target.path); 1330 DownloadsCommon.showDownloadedFile(file); 1331 1332 // We explicitly close the panel here to give the user the feedback that 1333 // their click has been received, and we're handling the action. 1334 // Otherwise, we'd have to wait for the operating system file manager 1335 // window to open before the panel closed. This also helps to prevent the 1336 // user from opening the containing folder several times. 1337 DownloadsPanel.hidePanel(); 1338 } 1339 1340 async downloadsCmd_deleteFile() { 1341 await super.downloadsCmd_deleteFile(); 1342 // Protects against an unusual edge case where the user: 1343 // 1) downloads a file with Firefox; 2) deletes the file from outside of Firefox, e.g., a file manager; 1344 // 3) downloads the same file from the same source; 4) opens the downloads panel and uses the menuitem to delete one of those 2 files; 1345 // Under those conditions, Firefox will make 2 view items even though there's only 1 file. 1346 // Using this method will only delete the view item it was called on, because this instance is not aware of other view items with identical targets. 1347 // So the remaining view item needs to be refreshed to hide the "Delete" option. 1348 // That example only concerns 2 duplicate view items but you can have an arbitrary number, so iterate over all items... 1349 for (let viewItem of DownloadsView._visibleViewItems.values()) { 1350 viewItem.download.refresh().catch(console.error); 1351 } 1352 // Don't use DownloadsPanel.hidePanel for this method because it will remove 1353 // the view item from the list, which is already sufficient feedback. 1354 } 1355 1356 downloadsCmd_showBlockedInfo() { 1357 DownloadsBlockedSubview.toggle( 1358 this.element, 1359 ...this.rawBlockedTitleAndDetails 1360 ); 1361 } 1362 1363 downloadsCmd_openReferrer() { 1364 openURL(this.download.source.referrerInfo.originalReferrer); 1365 } 1366 1367 downloadsCmd_copyLocation() { 1368 DownloadsCommon.copyDownloadLink(this.download); 1369 } 1370 1371 downloadsCmd_doDefault() { 1372 let defaultCommand = this.currentDefaultCommandName; 1373 if (defaultCommand && this.isCommandEnabled(defaultCommand)) { 1374 this.doCommand(defaultCommand); 1375 } 1376 } 1377 } 1378 1379 // DownloadsViewController 1380 1381 /** 1382 * Handles part of the user interaction events raised by the downloads list 1383 * widget, in particular the "commands" that apply to multiple items, and 1384 * dispatches the commands that apply to individual items. 1385 */ 1386 var DownloadsViewController = { 1387 // Initialization and termination 1388 1389 initialize() { 1390 window.controllers.insertControllerAt(0, this); 1391 }, 1392 1393 terminate() { 1394 window.controllers.removeController(this); 1395 }, 1396 1397 // nsIController 1398 1399 supportsCommand(aCommand) { 1400 if ( 1401 aCommand === "downloadsCmd_clearList" || 1402 aCommand === "downloadsCmd_deletePrivate" || 1403 aCommand === "downloadsCmd_dismissDeletePrivate" 1404 ) { 1405 return true; 1406 } 1407 // Firstly, determine if this is a command that we can handle. 1408 if (!DownloadsViewUI.isCommandName(aCommand)) { 1409 return false; 1410 } 1411 // Strip off any :modifier suffix before checking if the command name is 1412 // a method on our view 1413 let [command] = aCommand.split(":"); 1414 if (!(command in this) && !(command in DownloadsViewItem.prototype)) { 1415 return false; 1416 } 1417 // The currently supported commands depend on whether the blocked subview is 1418 // showing. If it is, then take the following path. 1419 if (DownloadsView.subViewOpen) { 1420 let blockedSubviewCmds = [ 1421 "downloadsCmd_unblockAndOpen", 1422 "cmd_delete", 1423 "downloadsCmd_unblockAndSave", 1424 ]; 1425 return blockedSubviewCmds.includes(aCommand); 1426 } 1427 // If the blocked subview is not showing, then determine if focus is on a 1428 // control in the downloads list. 1429 let element = document.commandDispatcher.focusedElement; 1430 while (element && element != DownloadsView.richListBox) { 1431 element = element.parentNode; 1432 } 1433 // We should handle the command only if the downloads list is among the 1434 // ancestors of the focused element. 1435 return !!element; 1436 }, 1437 1438 isCommandEnabled(aCommand) { 1439 // Handle commands that are not selection-specific. 1440 switch (aCommand) { 1441 case "downloadsCmd_clearList": { 1442 return DownloadsCommon.getData(window).canRemoveFinished; 1443 } 1444 case "downloadsCmd_deletePrivate": 1445 case "downloadsCmd_dismissDeletePrivate": 1446 return true; 1447 default: { 1448 // Other commands are selection-specific. 1449 let element = DownloadsView.richListBox.selectedItem; 1450 return ( 1451 element && 1452 DownloadsView.itemForElement(element).isCommandEnabled(aCommand) 1453 ); 1454 } 1455 } 1456 }, 1457 1458 doCommand(aCommand) { 1459 // If this command is not selection-specific, execute it. 1460 if (aCommand in this) { 1461 this[aCommand](); 1462 return; 1463 } 1464 1465 // Other commands are selection-specific. 1466 let element = DownloadsView.richListBox.selectedItem; 1467 if (element) { 1468 // The doCommand function also checks if the command is enabled. 1469 DownloadsView.itemForElement(element).doCommand(aCommand); 1470 } 1471 }, 1472 1473 onEvent() {}, 1474 1475 // Other functions 1476 1477 updateCommands() { 1478 function updateCommandsForObject(object) { 1479 for (let name in object) { 1480 if (DownloadsViewUI.isCommandName(name)) { 1481 goUpdateCommand(name); 1482 } 1483 } 1484 } 1485 updateCommandsForObject(this); 1486 updateCommandsForObject(DownloadsViewItem.prototype); 1487 }, 1488 1489 // Selection-independent commands 1490 1491 downloadsCmd_clearList() { 1492 DownloadsCommon.getData(window).removeFinished(); 1493 }, 1494 1495 downloadsCmd_deletePrivate() { 1496 PrivateDownloadsSubview.choose(true /* deletePrivate */); 1497 }, 1498 1499 downloadsCmd_dismissDeletePrivate() { 1500 PrivateDownloadsSubview.choose(false /* deletePrivate */); 1501 }, 1502 }; 1503 1504 XPCOMUtils.defineConstant( 1505 this, 1506 "DownloadsViewController", 1507 DownloadsViewController 1508 ); 1509 1510 // DownloadsSummary 1511 1512 /** 1513 * Manages the summary at the bottom of the downloads panel list if the number 1514 * of items in the list exceeds the panels limit. 1515 */ 1516 var DownloadsSummary = { 1517 /** 1518 * Sets the active state of the summary. When active, the summary subscribes 1519 * to the DownloadsCommon DownloadsSummaryData singleton. 1520 * 1521 * @param aActive 1522 * Set to true to activate the summary. 1523 */ 1524 set active(aActive) { 1525 if (aActive == this._active || !this._summaryNode) { 1526 return; 1527 } 1528 if (aActive) { 1529 DownloadsCommon.getSummary( 1530 window, 1531 DownloadsView.kItemCountLimit 1532 ).refreshView(this); 1533 } else { 1534 DownloadsFooter.showingSummary = false; 1535 } 1536 1537 this._active = aActive; 1538 }, 1539 1540 /** 1541 * Returns the active state of the downloads summary. 1542 */ 1543 get active() { 1544 return this._active; 1545 }, 1546 1547 _active: false, 1548 1549 /** 1550 * Sets whether or not we show the progress bar. 1551 * 1552 * @param aShowingProgress 1553 * True if we should show the progress bar. 1554 */ 1555 set showingProgress(aShowingProgress) { 1556 if (aShowingProgress) { 1557 this._summaryNode.setAttribute("inprogress", "true"); 1558 } else { 1559 this._summaryNode.removeAttribute("inprogress"); 1560 } 1561 // If progress isn't being shown, then we simply do not show the summary. 1562 DownloadsFooter.showingSummary = aShowingProgress; 1563 }, 1564 1565 /** 1566 * Sets the amount of progress that is visible in the progress bar. 1567 * 1568 * @param aValue 1569 * A value between 0 and 100 to represent the progress of the 1570 * summarized downloads. 1571 */ 1572 set percentComplete(aValue) { 1573 if (this._progressNode) { 1574 this._progressNode.setAttribute("value", aValue); 1575 } 1576 }, 1577 1578 /** 1579 * Sets the description for the download summary. 1580 * 1581 * @param aValue 1582 * A string representing the description of the summarized 1583 * downloads. 1584 */ 1585 set description(aValue) { 1586 if (this._descriptionNode) { 1587 this._descriptionNode.setAttribute("value", aValue); 1588 this._descriptionNode.setAttribute("tooltiptext", aValue); 1589 } 1590 }, 1591 1592 /** 1593 * Sets the details for the download summary, such as the time remaining, 1594 * the amount of bytes transferred, etc. 1595 * 1596 * @param aValue 1597 * A string representing the details of the summarized 1598 * downloads. 1599 */ 1600 set details(aValue) { 1601 if (this._detailsNode) { 1602 this._detailsNode.setAttribute("value", aValue); 1603 this._detailsNode.setAttribute("tooltiptext", aValue); 1604 } 1605 }, 1606 1607 /** 1608 * Focuses the root element of the summary. 1609 */ 1610 focus(focusOptions) { 1611 if (this._summaryNode) { 1612 this._summaryNode.focus(focusOptions); 1613 } 1614 }, 1615 1616 /** 1617 * Respond to keydown events on the Downloads Summary node. 1618 * 1619 * @param aEvent 1620 * The keydown event being handled. 1621 */ 1622 _onKeyDown(aEvent) { 1623 if ( 1624 aEvent.charCode == " ".charCodeAt(0) || 1625 aEvent.keyCode == KeyEvent.DOM_VK_RETURN 1626 ) { 1627 DownloadsPanel.showDownloadsHistory(); 1628 } 1629 }, 1630 1631 /** 1632 * Element corresponding to the root of the downloads summary. 1633 */ 1634 get _summaryNode() { 1635 let node = document.getElementById("downloadsSummary"); 1636 if (!node) { 1637 return null; 1638 } 1639 delete this._summaryNode; 1640 return (this._summaryNode = node); 1641 }, 1642 1643 /** 1644 * Element corresponding to the progress bar in the downloads summary. 1645 */ 1646 get _progressNode() { 1647 let node = document.getElementById("downloadsSummaryProgress"); 1648 if (!node) { 1649 return null; 1650 } 1651 delete this._progressNode; 1652 return (this._progressNode = node); 1653 }, 1654 1655 /** 1656 * Element corresponding to the main description of the downloads 1657 * summary. 1658 */ 1659 get _descriptionNode() { 1660 let node = document.getElementById("downloadsSummaryDescription"); 1661 if (!node) { 1662 return null; 1663 } 1664 delete this._descriptionNode; 1665 return (this._descriptionNode = node); 1666 }, 1667 1668 /** 1669 * Element corresponding to the secondary description of the downloads 1670 * summary. 1671 */ 1672 get _detailsNode() { 1673 let node = document.getElementById("downloadsSummaryDetails"); 1674 if (!node) { 1675 return null; 1676 } 1677 delete this._detailsNode; 1678 return (this._detailsNode = node); 1679 }, 1680 }; 1681 1682 XPCOMUtils.defineConstant(this, "DownloadsSummary", DownloadsSummary); 1683 1684 // DownloadsFooter 1685 1686 /** 1687 * Manages events sent to to the footer vbox, which contains both the 1688 * DownloadsSummary as well as the "Show all downloads" button. 1689 */ 1690 var DownloadsFooter = { 1691 /** 1692 * Focuses the appropriate element within the footer. If the summary 1693 * is visible, focus it. If not, focus the "Show all downloads" 1694 * button. 1695 */ 1696 focus(focusOptions) { 1697 if (this._showingSummary) { 1698 DownloadsSummary.focus(focusOptions); 1699 } else { 1700 DownloadsView.downloadsHistory.focus(focusOptions); 1701 } 1702 }, 1703 1704 _showingSummary: false, 1705 1706 /** 1707 * Sets whether or not the Downloads Summary should be displayed in the 1708 * footer. If not, the "Show all downloads" button is shown instead. 1709 */ 1710 set showingSummary(aValue) { 1711 if (this._footerNode) { 1712 if (aValue) { 1713 this._footerNode.setAttribute("showingsummary", "true"); 1714 } else { 1715 this._footerNode.removeAttribute("showingsummary"); 1716 } 1717 this._showingSummary = aValue; 1718 } 1719 }, 1720 1721 /** 1722 * Element corresponding to the footer of the downloads panel. 1723 */ 1724 get _footerNode() { 1725 let node = document.getElementById("downloadsFooter"); 1726 if (!node) { 1727 return null; 1728 } 1729 delete this._footerNode; 1730 return (this._footerNode = node); 1731 }, 1732 }; 1733 1734 XPCOMUtils.defineConstant(this, "DownloadsFooter", DownloadsFooter); 1735 1736 // DownloadsBlockedSubview 1737 1738 /** 1739 * Manages the blocked subview that slides in when you click a blocked download. 1740 */ 1741 var DownloadsBlockedSubview = { 1742 /** 1743 * Elements in the subview. 1744 */ 1745 get elements() { 1746 let idSuffixes = [ 1747 "title", 1748 "details1", 1749 "details2", 1750 "unblockButton", 1751 "deleteButton", 1752 ]; 1753 let elements = idSuffixes.reduce((memo, s) => { 1754 memo[s] = document.getElementById("downloadsPanel-blockedSubview-" + s); 1755 return memo; 1756 }, {}); 1757 delete this.elements; 1758 return (this.elements = elements); 1759 }, 1760 1761 /** 1762 * The blocked-download richlistitem element that was clicked to show the 1763 * subview. If the subview is not showing, this is undefined. 1764 */ 1765 element: undefined, 1766 1767 /** 1768 * Slides in the blocked subview. 1769 * 1770 * @param element 1771 * The blocked-download richlistitem element that was clicked. 1772 * @param title 1773 * The title to show in the subview. 1774 * @param details 1775 * An array of strings with information about the block. 1776 */ 1777 toggle(element, title, details) { 1778 DownloadsView.subViewOpen = true; 1779 DownloadsViewController.updateCommands(); 1780 const { download } = DownloadsView.itemForElement(element); 1781 1782 let e = this.elements; 1783 let s = DownloadsCommon.strings; 1784 1785 e.deleteButton.hidden = 1786 download.error?.becauseBlockedByContentAnalysis && 1787 download.error?.reputationCheckVerdict === "Malware"; 1788 1789 e.unblockButton.hidden = 1790 download.error?.becauseBlockedByContentAnalysis && 1791 download.error?.reputationCheckVerdict === "Malware"; 1792 1793 title.l10n 1794 ? document.l10n.setAttributes(e.title, title.l10n.id, title.l10n.args) 1795 : (e.title.textContent = title); 1796 1797 details[0].l10n 1798 ? document.l10n.setAttributes( 1799 e.details1, 1800 details[0].l10n.id, 1801 details[0].l10n.args 1802 ) 1803 : (e.details1.textContent = details[0]); 1804 1805 e.details2.textContent = details[1]; 1806 1807 if (download.launchWhenSucceeded) { 1808 e.unblockButton.label = s.unblockButtonOpen; 1809 e.unblockButton.command = "downloadsCmd_unblockAndOpen"; 1810 } else { 1811 e.unblockButton.label = s.unblockButtonUnblock; 1812 e.unblockButton.command = "downloadsCmd_unblockAndSave"; 1813 } 1814 1815 e.deleteButton.label = s.unblockButtonConfirmBlock; 1816 1817 let verdict = element.getAttribute("verdict"); 1818 this.subview.setAttribute("verdict", verdict); 1819 1820 this.mainView.addEventListener("ViewShown", this); 1821 DownloadsPanel.panel.addEventListener("popuphidden", this); 1822 this.panelMultiView.showSubView(this.subview); 1823 1824 // Without this, the mainView is more narrow than the panel once all 1825 // downloads are removed from the panel. 1826 this.mainView.style.minWidth = window.getComputedStyle(this.subview).width; 1827 }, 1828 1829 handleEvent(event) { 1830 // This is called when the main view is shown or the panel is hidden. 1831 DownloadsView.subViewOpen = false; 1832 this.mainView.removeEventListener("ViewShown", this); 1833 DownloadsPanel.panel.removeEventListener("popuphidden", this); 1834 // Focus the proper element if we're going back to the main panel. 1835 if (event.type == "ViewShown") { 1836 DownloadsPanel.showPanel(); 1837 } 1838 }, 1839 1840 /** 1841 * Deletes the download and hides the entire panel. 1842 */ 1843 confirmBlock() { 1844 goDoCommand("cmd_delete"); 1845 DownloadsPanel.hidePanel(); 1846 }, 1847 }; 1848 1849 ChromeUtils.defineLazyGetter(DownloadsBlockedSubview, "panelMultiView", () => 1850 document.getElementById("downloadsPanel-multiView") 1851 ); 1852 ChromeUtils.defineLazyGetter(DownloadsBlockedSubview, "mainView", () => 1853 document.getElementById("downloadsPanel-mainView") 1854 ); 1855 ChromeUtils.defineLazyGetter(DownloadsBlockedSubview, "subview", () => 1856 document.getElementById("downloadsPanel-blockedSubview") 1857 ); 1858 1859 XPCOMUtils.defineConstant( 1860 this, 1861 "DownloadsBlockedSubview", 1862 DownloadsBlockedSubview 1863 ); 1864 1865 /** 1866 * Manages the private browsing downloads subview that appears when you download a file in private browsing mode 1867 */ 1868 var PrivateDownloadsSubview = { 1869 /** 1870 * Slides in the private downloads subview. 1871 * 1872 * @param element 1873 * The download richlistitem element that was clicked. 1874 */ 1875 openWhenReady() { 1876 DownloadsView.subViewOpen = true; 1877 DownloadsViewController.updateCommands(); 1878 1879 this.mainView.addEventListener("ViewShown", this, { once: true }); 1880 this.mainView.toggleAttribute("showing-private-browsing-choice", true); 1881 }, 1882 1883 handleEvent(event) { 1884 // This is called when the main view is shown or the panel is hidden. 1885 1886 // Focus the proper element if we're going back to the main panel. 1887 if (event.type == "ViewShown") { 1888 this.panelMultiView.showSubView(this.subview); 1889 } 1890 }, 1891 1892 /** 1893 * Sets whether to delete files at the end of private download session 1894 * Based on user response to download notification prompt 1895 * 1896 * @param deletePrivate 1897 * True if the user chose to delete files at the end of the session 1898 */ 1899 choose(deletePrivate) { 1900 if (deletePrivate) { 1901 Services.prefs.setBoolPref("browser.download.deletePrivate", true); 1902 } 1903 Services.prefs.setBoolPref("browser.download.deletePrivate.chosen", true); 1904 DownloadsView.subViewOpen = false; 1905 this.mainView.toggleAttribute("showing-private-browsing-choice", false); 1906 this.panelMultiView.goBack(); 1907 }, 1908 }; 1909 1910 ChromeUtils.defineLazyGetter(PrivateDownloadsSubview, "panelMultiView", () => 1911 document.getElementById("downloadsPanel-multiView") 1912 ); 1913 1914 ChromeUtils.defineLazyGetter(PrivateDownloadsSubview, "mainView", () => 1915 document.getElementById("downloadsPanel-mainView") 1916 ); 1917 1918 ChromeUtils.defineLazyGetter(PrivateDownloadsSubview, "subview", () => 1919 document.getElementById("downloadsPanel-privateBrowsing") 1920 ); 1921 1922 XPCOMUtils.defineConstant( 1923 this, 1924 "PrivateDownloadsSubview", 1925 PrivateDownloadsSubview 1926 );