1737 lines
51 KiB
JavaScript
1737 lines
51 KiB
JavaScript
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set ts=2 et sw=2 tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
/* eslint-env mozilla/browser-window */
|
|
|
|
/**
|
|
* Handles the Downloads panel user interface for each browser window.
|
|
*
|
|
* This file includes the following constructors and global objects:
|
|
*
|
|
* DownloadsPanel
|
|
* Main entry point for the downloads panel interface.
|
|
*
|
|
* DownloadsView
|
|
* Builds and updates the downloads list widget, responding to changes in the
|
|
* download state and real-time data. In addition, handles part of the user
|
|
* interaction events raised by the downloads list widget.
|
|
*
|
|
* DownloadsViewItem
|
|
* Builds and updates a single item in the downloads list widget, responding to
|
|
* changes in the download state and real-time data, and handles the user
|
|
* interaction events related to a single item in the downloads list widgets.
|
|
*
|
|
* DownloadsViewController
|
|
* Handles part of the user interaction events raised by the downloads list
|
|
* widget, in particular the "commands" that apply to multiple items, and
|
|
* dispatches the commands that apply to individual items.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
var { XPCOMUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
);
|
|
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"DownloadsViewUI",
|
|
"resource:///modules/DownloadsViewUI.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"DownloadsSubview",
|
|
"resource:///modules/DownloadsSubview.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"FileUtils",
|
|
"resource://gre/modules/FileUtils.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"NetUtil",
|
|
"resource://gre/modules/NetUtil.jsm"
|
|
);
|
|
ChromeUtils.defineModuleGetter(
|
|
this,
|
|
"PlacesUtils",
|
|
"resource://gre/modules/PlacesUtils.jsm"
|
|
);
|
|
|
|
const { Integration } = ChromeUtils.import(
|
|
"resource://gre/modules/Integration.jsm"
|
|
);
|
|
|
|
/* global DownloadIntegration */
|
|
Integration.downloads.defineModuleGetter(
|
|
this,
|
|
"DownloadIntegration",
|
|
"resource://gre/modules/DownloadIntegration.jsm"
|
|
);
|
|
|
|
// DownloadsPanel
|
|
|
|
/**
|
|
* Main entry point for the downloads panel interface.
|
|
*/
|
|
var DownloadsPanel = {
|
|
// Initialization and termination
|
|
|
|
/**
|
|
* Timeout that re-enables previously disabled download items in the downloads panel
|
|
* after some time has passed.
|
|
*/
|
|
_delayTimeout: null,
|
|
|
|
/**
|
|
* Internal state of the downloads panel, based on one of the kState
|
|
* constants. This is not the same state as the XUL panel element.
|
|
*/
|
|
_state: 0,
|
|
|
|
/** The panel is not linked to downloads data yet. */
|
|
get kStateUninitialized() {
|
|
return 0;
|
|
},
|
|
/** This object is linked to data, but the panel is invisible. */
|
|
get kStateHidden() {
|
|
return 1;
|
|
},
|
|
/** The panel will be shown as soon as possible. */
|
|
get kStateWaitingData() {
|
|
return 2;
|
|
},
|
|
/** The panel is open. */
|
|
get kStateShown() {
|
|
return 3;
|
|
},
|
|
|
|
/**
|
|
* Starts loading the download data in background, without opening the panel.
|
|
* Use showPanel instead to load the data and open the panel at the same time.
|
|
*/
|
|
initialize() {
|
|
DownloadsCommon.log(
|
|
"Attempting to initialize DownloadsPanel for a window."
|
|
);
|
|
|
|
// Allow the download spam protection module to notify DownloadsView
|
|
// if it's been created.
|
|
if (
|
|
DownloadIntegration.downloadSpamProtection &&
|
|
!DownloadIntegration.downloadSpamProtection.spamList._views.has(
|
|
DownloadsView
|
|
)
|
|
) {
|
|
DownloadIntegration.downloadSpamProtection.spamList.addView(
|
|
DownloadsView
|
|
);
|
|
}
|
|
|
|
if (this._state != this.kStateUninitialized) {
|
|
DownloadsCommon.log("DownloadsPanel is already initialized.");
|
|
return;
|
|
}
|
|
this._state = this.kStateHidden;
|
|
|
|
window.addEventListener("unload", this.onWindowUnload);
|
|
|
|
// Load and resume active downloads if required. If there are downloads to
|
|
// be shown in the panel, they will be loaded asynchronously.
|
|
DownloadsCommon.initializeAllDataLinks();
|
|
|
|
// Now that data loading has eventually started, load the required XUL
|
|
// elements and initialize our views.
|
|
|
|
this.panel.hidden = false;
|
|
DownloadsViewController.initialize();
|
|
DownloadsCommon.log("Attaching DownloadsView...");
|
|
DownloadsCommon.getData(window).addView(DownloadsView);
|
|
DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit).addView(
|
|
DownloadsSummary
|
|
);
|
|
|
|
DownloadsCommon.log(
|
|
"DownloadsView attached - the panel for this window",
|
|
"should now see download items come in."
|
|
);
|
|
DownloadsPanel._attachEventListeners();
|
|
DownloadsCommon.log("DownloadsPanel initialized.");
|
|
},
|
|
|
|
/**
|
|
* Closes the downloads panel and frees the internal resources related to the
|
|
* downloads. The downloads panel can be reopened later, even after this
|
|
* function has been called.
|
|
*/
|
|
terminate() {
|
|
DownloadsCommon.log("Attempting to terminate DownloadsPanel for a window.");
|
|
if (this._state == this.kStateUninitialized) {
|
|
DownloadsCommon.log(
|
|
"DownloadsPanel was never initialized. Nothing to do."
|
|
);
|
|
return;
|
|
}
|
|
|
|
window.removeEventListener("unload", this.onWindowUnload);
|
|
|
|
// Ensure that the panel is closed before shutting down.
|
|
this.hidePanel();
|
|
|
|
DownloadsViewController.terminate();
|
|
DownloadsCommon.getData(window).removeView(DownloadsView);
|
|
DownloadsCommon.getSummary(
|
|
window,
|
|
DownloadsView.kItemCountLimit
|
|
).removeView(DownloadsSummary);
|
|
this._unattachEventListeners();
|
|
|
|
if (DownloadIntegration.downloadSpamProtection) {
|
|
DownloadIntegration.downloadSpamProtection.spamList.removeView(
|
|
DownloadsView
|
|
);
|
|
}
|
|
|
|
this._state = this.kStateUninitialized;
|
|
|
|
DownloadsSummary.active = false;
|
|
DownloadsCommon.log("DownloadsPanel terminated.");
|
|
},
|
|
|
|
// Panel interface
|
|
|
|
/**
|
|
* Main panel element in the browser window.
|
|
*/
|
|
get panel() {
|
|
delete this.panel;
|
|
return (this.panel = document.getElementById("downloadsPanel"));
|
|
},
|
|
|
|
/**
|
|
* Starts opening the downloads panel interface, anchored to the downloads
|
|
* button of the browser window. The list of downloads to display is
|
|
* initialized the first time this method is called, and the panel is shown
|
|
* only when data is ready.
|
|
*/
|
|
showPanel(openedManually = false, isKeyPress = false) {
|
|
Services.telemetry.scalarAdd("downloads.panel_shown", 1);
|
|
DownloadsCommon.log("Opening the downloads panel.");
|
|
|
|
this._openedManually = openedManually;
|
|
this._preventFocusRing = !openedManually || !isKeyPress;
|
|
|
|
if (this.isPanelShowing) {
|
|
DownloadsCommon.log("Panel is already showing - focusing instead.");
|
|
this._focusPanel();
|
|
return;
|
|
}
|
|
|
|
// As a belt-and-suspenders check, ensure the button is not hidden.
|
|
DownloadsButton.unhide();
|
|
|
|
this.initialize();
|
|
// Delay displaying the panel because this function will sometimes be
|
|
// called while another window is closing (like the window for selecting
|
|
// whether to save or open the file), and that would cause the panel to
|
|
// close immediately.
|
|
setTimeout(() => this._openPopupIfDataReady(), 0);
|
|
|
|
DownloadsCommon.log("Waiting for the downloads panel to appear.");
|
|
this._state = this.kStateWaitingData;
|
|
},
|
|
|
|
/**
|
|
* Hides the downloads panel, if visible, but keeps the internal state so that
|
|
* the panel can be reopened quickly if required.
|
|
*/
|
|
hidePanel() {
|
|
DownloadsCommon.log("Closing the downloads panel.");
|
|
|
|
if (!this.isPanelShowing) {
|
|
DownloadsCommon.log("Downloads panel is not showing - nothing to do.");
|
|
return;
|
|
}
|
|
|
|
PanelMultiView.hidePopup(this.panel);
|
|
|
|
// Ensure that we allow the panel to be reopened. Note that, if the popup
|
|
// was open, then the onPopupHidden event handler has already updated the
|
|
// current state, otherwise we must update the state ourselves.
|
|
this._state = this.kStateHidden;
|
|
DownloadsCommon.log("Downloads panel is now closed.");
|
|
},
|
|
|
|
/**
|
|
* Indicates whether the panel is shown or will be shown.
|
|
*/
|
|
get isPanelShowing() {
|
|
return (
|
|
this._state == this.kStateWaitingData || this._state == this.kStateShown
|
|
);
|
|
},
|
|
|
|
handleEvent(aEvent) {
|
|
switch (aEvent.type) {
|
|
case "mousemove":
|
|
if (
|
|
!DownloadsView.contextMenuOpen &&
|
|
!DownloadsView.subViewOpen &&
|
|
this.panel.contains(document.activeElement)
|
|
) {
|
|
// Let mouse movement remove focus rings and reset focus in the panel.
|
|
// This behavior is copied from PanelMultiView.
|
|
document.activeElement.blur();
|
|
DownloadsView.richListBox.removeAttribute("force-focus-visible");
|
|
this._preventFocusRing = true;
|
|
this._focusPanel();
|
|
}
|
|
break;
|
|
case "keydown":
|
|
this._onKeyDown(aEvent);
|
|
break;
|
|
case "keypress":
|
|
this._onKeyPress(aEvent);
|
|
break;
|
|
case "focus":
|
|
case "select":
|
|
this._onSelect(aEvent);
|
|
break;
|
|
}
|
|
},
|
|
|
|
// Callback functions from DownloadsView
|
|
|
|
/**
|
|
* Called after data loading finished.
|
|
*/
|
|
onViewLoadCompleted() {
|
|
this._openPopupIfDataReady();
|
|
},
|
|
|
|
// User interface event functions
|
|
|
|
onWindowUnload() {
|
|
// This function is registered as an event listener, we can't use "this".
|
|
DownloadsPanel.terminate();
|
|
},
|
|
|
|
onPopupShown(aEvent) {
|
|
// Ignore events raised by nested popups.
|
|
if (aEvent.target != aEvent.currentTarget) {
|
|
return;
|
|
}
|
|
|
|
DownloadsCommon.log("Downloads panel has shown.");
|
|
this._state = this.kStateShown;
|
|
|
|
// Since at most one popup is open at any given time, we can set globally.
|
|
DownloadsCommon.getIndicatorData(window).attentionSuppressed |=
|
|
DownloadsCommon.SUPPRESS_PANEL_OPEN;
|
|
|
|
// Ensure that the first item is selected when the panel is focused.
|
|
if (DownloadsView.richListBox.itemCount > 0) {
|
|
DownloadsView.richListBox.selectedIndex = 0;
|
|
}
|
|
|
|
this._focusPanel();
|
|
},
|
|
|
|
onPopupHidden(aEvent) {
|
|
// Ignore events raised by nested popups.
|
|
if (aEvent.target != aEvent.currentTarget) {
|
|
return;
|
|
}
|
|
|
|
DownloadsCommon.log("Downloads panel has hidden.");
|
|
|
|
if (this._delayTimeout) {
|
|
DownloadsView.richListBox.removeAttribute("disabled");
|
|
clearTimeout(this._delayTimeout);
|
|
this._stopWatchingForSpammyDownloadActivation();
|
|
this._delayTimeout = null;
|
|
}
|
|
|
|
DownloadsView.richListBox.removeAttribute("force-focus-visible");
|
|
|
|
// Since at most one popup is open at any given time, we can set globally.
|
|
DownloadsCommon.getIndicatorData(
|
|
window
|
|
).attentionSuppressed &= ~DownloadsCommon.SUPPRESS_PANEL_OPEN;
|
|
|
|
// Allow the anchor to be hidden.
|
|
DownloadsButton.releaseAnchor();
|
|
|
|
// Allow the panel to be reopened.
|
|
this._state = this.kStateHidden;
|
|
},
|
|
|
|
// Related operations
|
|
|
|
/**
|
|
* Shows or focuses the user interface dedicated to downloads history.
|
|
*/
|
|
showDownloadsHistory() {
|
|
DownloadsCommon.log("Showing download history.");
|
|
// Hide the panel before showing another window, otherwise focus will return
|
|
// to the browser window when the panel closes automatically.
|
|
this.hidePanel();
|
|
|
|
BrowserDownloadsUI();
|
|
},
|
|
|
|
// Internal functions
|
|
|
|
/**
|
|
* Attach event listeners to a panel element. These listeners should be
|
|
* removed in _unattachEventListeners. This is called automatically after the
|
|
* panel has successfully loaded.
|
|
*/
|
|
_attachEventListeners() {
|
|
// Handle keydown to support accel-V.
|
|
this.panel.addEventListener("keydown", this);
|
|
// Handle keypress to be able to preventDefault() events before they reach
|
|
// the richlistbox, for keyboard navigation.
|
|
this.panel.addEventListener("keypress", this);
|
|
this.panel.addEventListener("mousemove", this);
|
|
DownloadsView.richListBox.addEventListener("focus", this);
|
|
DownloadsView.richListBox.addEventListener("select", this);
|
|
},
|
|
|
|
/**
|
|
* Unattach event listeners that were added in _attachEventListeners. This
|
|
* is called automatically on panel termination.
|
|
*/
|
|
_unattachEventListeners() {
|
|
this.panel.removeEventListener("keydown", this);
|
|
this.panel.removeEventListener("keypress", this);
|
|
this.panel.removeEventListener("mousemove", this);
|
|
DownloadsView.richListBox.removeEventListener("focus", this);
|
|
DownloadsView.richListBox.removeEventListener("select", this);
|
|
},
|
|
|
|
_onKeyPress(aEvent) {
|
|
// Handle unmodified keys only.
|
|
if (aEvent.altKey || aEvent.ctrlKey || aEvent.shiftKey || aEvent.metaKey) {
|
|
return;
|
|
}
|
|
|
|
// Pass keypress events to the richlistbox view when it's focused.
|
|
if (document.activeElement === DownloadsView.richListBox) {
|
|
DownloadsView.onDownloadKeyPress(aEvent);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Keydown listener that listens for the keys to start key focusing, as well
|
|
* as the the accel-V "paste" event, which initiates a file download if the
|
|
* pasted item can be resolved to a URI.
|
|
*/
|
|
_onKeyDown(aEvent) {
|
|
if (DownloadsView.richListBox.hasAttribute("disabled")) {
|
|
this._handlePotentiallySpammyDownloadActivation(aEvent);
|
|
return;
|
|
}
|
|
|
|
let richListBox = DownloadsView.richListBox;
|
|
|
|
// If the user has pressed the up or down cursor key, force-enable focus
|
|
// indicators for the richlistbox. :focus-visible doesn't work in this case
|
|
// because the the focused element may not change here if the richlistbox
|
|
// already had focus. The force-focus-visible attribute will be removed
|
|
// again if the user moves the mouse on the panel or if the panel is closed.
|
|
if (
|
|
aEvent.keyCode == aEvent.DOM_VK_UP ||
|
|
aEvent.keyCode == aEvent.DOM_VK_DOWN
|
|
) {
|
|
richListBox.setAttribute("force-focus-visible", "true");
|
|
}
|
|
|
|
// If the footer is focused and the downloads list has at least 1 element
|
|
// in it, focus the last element in the list when going up.
|
|
if (aEvent.keyCode == aEvent.DOM_VK_UP && richListBox.firstElementChild) {
|
|
if (
|
|
document
|
|
.getElementById("downloadsFooter")
|
|
.contains(document.activeElement)
|
|
) {
|
|
richListBox.selectedItem = richListBox.lastElementChild;
|
|
richListBox.focus();
|
|
aEvent.preventDefault();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (aEvent.keyCode == aEvent.DOM_VK_DOWN) {
|
|
// If the last element in the list is selected, or the footer is already
|
|
// focused, focus the footer.
|
|
if (
|
|
richListBox.selectedItem === richListBox.lastElementChild ||
|
|
document
|
|
.getElementById("downloadsFooter")
|
|
.contains(document.activeElement)
|
|
) {
|
|
richListBox.selectedIndex = -1;
|
|
DownloadsFooter.focus();
|
|
aEvent.preventDefault();
|
|
return;
|
|
}
|
|
}
|
|
|
|
let pasting =
|
|
aEvent.keyCode == aEvent.DOM_VK_V && aEvent.getModifierState("Accel");
|
|
|
|
if (!pasting) {
|
|
return;
|
|
}
|
|
|
|
DownloadsCommon.log("Received a paste event.");
|
|
|
|
let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
|
|
Ci.nsITransferable
|
|
);
|
|
trans.init(null);
|
|
let flavors = ["text/x-moz-url", "text/unicode"];
|
|
flavors.forEach(trans.addDataFlavor);
|
|
Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard);
|
|
// Getting the data or creating the nsIURI might fail
|
|
try {
|
|
let data = {};
|
|
trans.getAnyTransferData({}, data);
|
|
let [url, name] = data.value
|
|
.QueryInterface(Ci.nsISupportsString)
|
|
.data.split("\n");
|
|
if (!url) {
|
|
return;
|
|
}
|
|
|
|
let uri = NetUtil.newURI(url);
|
|
DownloadsCommon.log("Pasted URL seems valid. Starting download.");
|
|
DownloadURL(uri.spec, name, document);
|
|
} catch (ex) {}
|
|
},
|
|
|
|
_onSelect() {
|
|
let richlistbox = DownloadsView.richListBox;
|
|
richlistbox.itemChildren.forEach(item => {
|
|
let button = item.querySelector("button");
|
|
if (item.selected) {
|
|
button.removeAttribute("tabindex");
|
|
} else {
|
|
button.setAttribute("tabindex", -1);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Move focus to the main element in the downloads panel, unless another
|
|
* element in the panel is already focused.
|
|
*/
|
|
_focusPanel() {
|
|
// We may be invoked while the panel is still waiting to be shown.
|
|
if (this._state != this.kStateShown) {
|
|
return;
|
|
}
|
|
|
|
if (document.activeElement && this.panel.contains(document.activeElement)) {
|
|
return;
|
|
}
|
|
let focusOptions = { preventFocusRing: !!this._preventFocusRing };
|
|
if (DownloadsView.richListBox.itemCount > 0) {
|
|
DownloadsView.richListBox.selectedIndex = 0;
|
|
DownloadsView.richListBox.focus(focusOptions);
|
|
} else {
|
|
DownloadsFooter.focus(focusOptions);
|
|
}
|
|
},
|
|
|
|
_delayPopupItems() {
|
|
DownloadsView.richListBox.setAttribute("disabled", true);
|
|
this._startWatchingForSpammyDownloadActivation();
|
|
|
|
this._refreshDelayTimer();
|
|
},
|
|
|
|
_refreshDelayTimer() {
|
|
// If timeout already exists, overwrite it to avoid multiple timeouts.
|
|
if (this._delayTimeout) {
|
|
clearTimeout(this._delayTimeout);
|
|
}
|
|
|
|
let delay = Services.prefs.getIntPref("security.dialog_enable_delay");
|
|
this._delayTimeout = setTimeout(() => {
|
|
DownloadsView.richListBox.removeAttribute("disabled");
|
|
this._stopWatchingForSpammyDownloadActivation();
|
|
this._focusPanel();
|
|
this._delayTimeout = null;
|
|
}, delay);
|
|
},
|
|
|
|
_startWatchingForSpammyDownloadActivation() {
|
|
Services.els.addSystemEventListener(window, "keydown", this, true);
|
|
},
|
|
|
|
_lastBeepTime: 0,
|
|
_handlePotentiallySpammyDownloadActivation(aEvent) {
|
|
if (aEvent.key == "Enter" || aEvent.key == " ") {
|
|
// Throttle our beeping to a maximum of once per second, otherwise it
|
|
// appears on Win10 that beeps never make it through at all.
|
|
if (Date.now() - this._lastBeepTime > 1000) {
|
|
Cc["@mozilla.org/sound;1"].getService(Ci.nsISound).beep();
|
|
this._lastBeepTime = Date.now();
|
|
}
|
|
|
|
this._refreshDelayTimer();
|
|
}
|
|
},
|
|
|
|
_stopWatchingForSpammyDownloadActivation() {
|
|
Services.els.removeSystemEventListener(window, "keydown", this, true);
|
|
},
|
|
|
|
/**
|
|
* Opens the downloads panel when data is ready to be displayed.
|
|
*/
|
|
_openPopupIfDataReady() {
|
|
// We don't want to open the popup if we already displayed it, or if we are
|
|
// still loading data.
|
|
if (this._state != this.kStateWaitingData || DownloadsView.loading) {
|
|
return;
|
|
}
|
|
|
|
// At this point, if the window is minimized, opening the panel could fail
|
|
// without any notification, and there would be no way to either open or
|
|
// close the panel any more. To prevent this, check if the window is
|
|
// minimized and in that case force the panel to the closed state.
|
|
if (window.windowState == window.STATE_MINIMIZED) {
|
|
this._state = this.kStateHidden;
|
|
return;
|
|
}
|
|
|
|
// Ensure the anchor is visible. If that is not possible, show the panel
|
|
// anchored to the top area of the window, near the default anchor position.
|
|
let anchor = DownloadsButton.getAnchor();
|
|
|
|
if (!anchor) {
|
|
DownloadsCommon.error("Downloads button cannot be found.");
|
|
this._state = this.kStateHidden;
|
|
return;
|
|
}
|
|
|
|
let onBookmarksToolbar = !!anchor.closest("#PersonalToolbar");
|
|
this.panel.classList.toggle("bookmarks-toolbar", onBookmarksToolbar);
|
|
|
|
// When the panel is opened, we check if the target files of visible items
|
|
// still exist, and update the allowed items interactions accordingly. We
|
|
// do these checks on a background thread, and don't prevent the panel to
|
|
// be displayed while these checks are being performed.
|
|
for (let viewItem of DownloadsView._visibleViewItems.values()) {
|
|
viewItem.download.refresh().catch(Cu.reportError);
|
|
}
|
|
|
|
DownloadsCommon.log("Opening downloads panel popup.");
|
|
|
|
// Delay displaying the panel because this function will sometimes be
|
|
// called while another window is closing (like the window for selecting
|
|
// whether to save or open the file), and that would cause the panel to
|
|
// close immediately.
|
|
setTimeout(() => {
|
|
PanelMultiView.openPopup(
|
|
this.panel,
|
|
anchor,
|
|
"bottomcenter topright",
|
|
0,
|
|
0,
|
|
false,
|
|
null
|
|
).catch(e => {
|
|
Cu.reportError(e);
|
|
this._state = this.kStateHidden;
|
|
});
|
|
|
|
if (!this._openedManually) {
|
|
this._delayPopupItems();
|
|
}
|
|
}, 0);
|
|
},
|
|
};
|
|
|
|
XPCOMUtils.defineConstant(this, "DownloadsPanel", DownloadsPanel);
|
|
|
|
// DownloadsView
|
|
|
|
/**
|
|
* Builds and updates the downloads list widget, responding to changes in the
|
|
* download state and real-time data. In addition, handles part of the user
|
|
* interaction events raised by the downloads list widget.
|
|
*/
|
|
var DownloadsView = {
|
|
// Functions handling download items in the list
|
|
|
|
/**
|
|
* Maximum number of items shown by the list at any given time.
|
|
*/
|
|
kItemCountLimit: 5,
|
|
|
|
/**
|
|
* Indicates whether there is a DownloadsBlockedSubview open.
|
|
*/
|
|
subViewOpen: false,
|
|
|
|
/**
|
|
* Indicates whether we are still loading downloads data asynchronously.
|
|
*/
|
|
loading: false,
|
|
|
|
/**
|
|
* Ordered array of all Download objects. We need to keep this array because
|
|
* only a limited number of items are shown at once, and if an item that is
|
|
* currently visible is removed from the list, we might need to take another
|
|
* item from the array and make it appear at the bottom.
|
|
*/
|
|
_downloads: [],
|
|
|
|
/**
|
|
* Associates the visible Download objects with their corresponding
|
|
* DownloadsViewItem object. There is a limited number of view items in the
|
|
* panel at any given time.
|
|
*/
|
|
_visibleViewItems: new Map(),
|
|
|
|
/**
|
|
* Called when the number of items in the list changes.
|
|
*/
|
|
_itemCountChanged() {
|
|
DownloadsCommon.log(
|
|
"The downloads item count has changed - we are tracking",
|
|
this._downloads.length,
|
|
"downloads in total."
|
|
);
|
|
let count = this._downloads.length;
|
|
let hiddenCount = count - this.kItemCountLimit;
|
|
|
|
if (count > 0) {
|
|
DownloadsCommon.log(
|
|
"Setting the panel's hasdownloads attribute to true."
|
|
);
|
|
DownloadsPanel.panel.setAttribute("hasdownloads", "true");
|
|
} else {
|
|
DownloadsCommon.log("Removing the panel's hasdownloads attribute.");
|
|
DownloadsPanel.panel.removeAttribute("hasdownloads");
|
|
}
|
|
|
|
// If we've got some hidden downloads, we should activate the
|
|
// DownloadsSummary. The DownloadsSummary will determine whether or not
|
|
// it's appropriate to actually display the summary.
|
|
DownloadsSummary.active = hiddenCount > 0;
|
|
},
|
|
|
|
/**
|
|
* Element corresponding to the list of downloads.
|
|
*/
|
|
get richListBox() {
|
|
delete this.richListBox;
|
|
return (this.richListBox = document.getElementById("downloadsListBox"));
|
|
},
|
|
|
|
/**
|
|
* Element corresponding to the button for showing more downloads.
|
|
*/
|
|
get downloadsHistory() {
|
|
delete this.downloadsHistory;
|
|
return (this.downloadsHistory = document.getElementById(
|
|
"downloadsHistory"
|
|
));
|
|
},
|
|
|
|
// Callback functions from DownloadsData
|
|
|
|
/**
|
|
* Called before multiple downloads are about to be loaded.
|
|
*/
|
|
onDownloadBatchStarting() {
|
|
DownloadsCommon.log("onDownloadBatchStarting called for DownloadsView.");
|
|
this.loading = true;
|
|
},
|
|
|
|
/**
|
|
* Called after data loading finished.
|
|
*/
|
|
onDownloadBatchEnded() {
|
|
DownloadsCommon.log("onDownloadBatchEnded called for DownloadsView.");
|
|
|
|
this.loading = false;
|
|
|
|
// We suppressed item count change notifications during the batch load, at
|
|
// this point we should just call the function once.
|
|
this._itemCountChanged();
|
|
|
|
// Notify the panel that all the initially available downloads have been
|
|
// loaded. This ensures that the interface is visible, if still required.
|
|
DownloadsPanel.onViewLoadCompleted();
|
|
},
|
|
|
|
/**
|
|
* Called when a new download data item is available, either during the
|
|
* asynchronous data load or when a new download is started.
|
|
*
|
|
* @param aDownload
|
|
* Download object that was just added.
|
|
*/
|
|
onDownloadAdded(download) {
|
|
DownloadsCommon.log("A new download data item was added");
|
|
|
|
this._downloads.unshift(download);
|
|
|
|
// The newly added item is visible in the panel and we must add the
|
|
// corresponding element. If the list overflows, remove the last item from
|
|
// the panel to make room for the new one that we just added at the top.
|
|
this._addViewItem(download, true);
|
|
if (this._downloads.length > this.kItemCountLimit) {
|
|
this._removeViewItem(this._downloads[this.kItemCountLimit]);
|
|
}
|
|
|
|
// For better performance during batch loads, don't update the count for
|
|
// every item, because the interface won't be visible until load finishes.
|
|
if (!this.loading) {
|
|
this._itemCountChanged();
|
|
}
|
|
},
|
|
|
|
onDownloadChanged(download) {
|
|
let viewItem = this._visibleViewItems.get(download);
|
|
if (viewItem) {
|
|
viewItem.onChanged();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Called when a data item is removed. Ensures that the widget associated
|
|
* with the view item is removed from the user interface.
|
|
*
|
|
* @param download
|
|
* Download object that is being removed.
|
|
*/
|
|
onDownloadRemoved(download) {
|
|
DownloadsCommon.log("A download data item was removed.");
|
|
|
|
let itemIndex = this._downloads.indexOf(download);
|
|
this._downloads.splice(itemIndex, 1);
|
|
|
|
if (itemIndex < this.kItemCountLimit) {
|
|
// The item to remove is visible in the panel.
|
|
this._removeViewItem(download);
|
|
if (this._downloads.length >= this.kItemCountLimit) {
|
|
// Reinsert the next item into the panel.
|
|
this._addViewItem(this._downloads[this.kItemCountLimit - 1], false);
|
|
}
|
|
}
|
|
|
|
this._itemCountChanged();
|
|
},
|
|
|
|
/**
|
|
* Associates each richlistitem for a download with its corresponding
|
|
* DownloadsViewItem object.
|
|
*/
|
|
_itemsForElements: new Map(),
|
|
|
|
itemForElement(element) {
|
|
return this._itemsForElements.get(element);
|
|
},
|
|
|
|
/**
|
|
* Creates a new view item associated with the specified data item, and adds
|
|
* it to the top or the bottom of the list.
|
|
*/
|
|
_addViewItem(download, aNewest) {
|
|
DownloadsCommon.log(
|
|
"Adding a new DownloadsViewItem to the downloads list.",
|
|
"aNewest =",
|
|
aNewest
|
|
);
|
|
|
|
let element = document.createXULElement("richlistitem");
|
|
element.setAttribute("align", "center");
|
|
|
|
let viewItem = new DownloadsViewItem(download, element);
|
|
this._visibleViewItems.set(download, viewItem);
|
|
this._itemsForElements.set(element, viewItem);
|
|
if (aNewest) {
|
|
this.richListBox.insertBefore(
|
|
element,
|
|
this.richListBox.firstElementChild
|
|
);
|
|
} else {
|
|
this.richListBox.appendChild(element);
|
|
}
|
|
viewItem.ensureActive();
|
|
},
|
|
|
|
/**
|
|
* Removes the view item associated with the specified data item.
|
|
*/
|
|
_removeViewItem(download) {
|
|
DownloadsCommon.log(
|
|
"Removing a DownloadsViewItem from the downloads list."
|
|
);
|
|
let element = this._visibleViewItems.get(download).element;
|
|
let previousSelectedIndex = this.richListBox.selectedIndex;
|
|
this.richListBox.removeChild(element);
|
|
if (previousSelectedIndex != -1) {
|
|
this.richListBox.selectedIndex = Math.min(
|
|
previousSelectedIndex,
|
|
this.richListBox.itemCount - 1
|
|
);
|
|
}
|
|
this._visibleViewItems.delete(download);
|
|
this._itemsForElements.delete(element);
|
|
},
|
|
|
|
// User interface event functions
|
|
|
|
onDownloadClick(aEvent) {
|
|
// Handle primary clicks in the main area only:
|
|
if (aEvent.button == 0 && aEvent.target.closest(".downloadMainArea")) {
|
|
let target = aEvent.target;
|
|
while (target.nodeName != "richlistitem") {
|
|
target = target.parentNode;
|
|
}
|
|
let download = DownloadsView.itemForElement(target).download;
|
|
if (download.succeeded) {
|
|
download._launchedFromPanel = true;
|
|
}
|
|
let command = "downloadsCmd_open";
|
|
if (download.hasBlockedData) {
|
|
command = "downloadsCmd_showBlockedInfo";
|
|
} else if (aEvent.shiftKey || aEvent.ctrlKey || aEvent.metaKey) {
|
|
// We adjust the command for supported modifiers to suggest where the download
|
|
// may be opened
|
|
let openWhere = target.ownerGlobal.whereToOpenLink(aEvent, false, true);
|
|
if (["tab", "window", "tabshifted"].includes(openWhere)) {
|
|
command += ":" + openWhere;
|
|
}
|
|
}
|
|
// Toggle opening the file after the download has completed
|
|
if (
|
|
!download.stopped &&
|
|
command.startsWith("downloadsCmd_open") &&
|
|
Services.prefs.getBoolPref(
|
|
"browser.download.improvements_to_download_panel"
|
|
)
|
|
) {
|
|
download.launchWhenSucceeded = !download.launchWhenSucceeded;
|
|
download._launchedFromPanel = download.launchWhenSucceeded;
|
|
}
|
|
|
|
DownloadsCommon.log("onDownloadClick, resolved command: ", command);
|
|
goDoCommand(command);
|
|
}
|
|
},
|
|
|
|
onDownloadButton(event) {
|
|
let target = event.target.closest("richlistitem");
|
|
DownloadsView.itemForElement(target).onButton();
|
|
},
|
|
|
|
/**
|
|
* Handles keypress events on a download item.
|
|
*/
|
|
onDownloadKeyPress(aEvent) {
|
|
// Pressing the key on buttons should not invoke the action because the
|
|
// event has already been handled by the button itself.
|
|
if (
|
|
aEvent.originalTarget.hasAttribute("command") ||
|
|
aEvent.originalTarget.hasAttribute("oncommand")
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (aEvent.charCode == " ".charCodeAt(0)) {
|
|
aEvent.preventDefault();
|
|
goDoCommand("downloadsCmd_pauseResume");
|
|
return;
|
|
}
|
|
|
|
if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
|
|
let readyToDownload = !DownloadsView.richListBox.disabled;
|
|
if (readyToDownload) {
|
|
goDoCommand("downloadsCmd_doDefault");
|
|
}
|
|
}
|
|
},
|
|
|
|
get contextMenu() {
|
|
let menu = document.getElementById("downloadsContextMenu");
|
|
if (menu) {
|
|
delete this.contextMenu;
|
|
this.contextMenu = menu;
|
|
}
|
|
return menu;
|
|
},
|
|
|
|
/**
|
|
* Indicates whether there is an open contextMenu for a download item.
|
|
*/
|
|
get contextMenuOpen() {
|
|
return this.contextMenu.state != "closed";
|
|
},
|
|
|
|
/**
|
|
* Mouse listeners to handle selection on hover.
|
|
*/
|
|
onDownloadMouseOver(aEvent) {
|
|
let item = aEvent.target.closest("richlistitem,richlistbox");
|
|
if (item.localName != "richlistitem") {
|
|
return;
|
|
}
|
|
|
|
if (aEvent.target.classList.contains("downloadButton")) {
|
|
item.classList.add("downloadHoveringButton");
|
|
}
|
|
|
|
item.classList.toggle(
|
|
"hoveringMainArea",
|
|
aEvent.target.closest(".downloadMainArea")
|
|
);
|
|
|
|
if (!this.contextMenuOpen && !this.subViewOpen) {
|
|
this.richListBox.selectedItem = item;
|
|
}
|
|
},
|
|
|
|
onDownloadMouseOut(aEvent) {
|
|
let item = aEvent.target.closest("richlistitem,richlistbox");
|
|
if (item.localName != "richlistitem") {
|
|
return;
|
|
}
|
|
|
|
if (aEvent.target.classList.contains("downloadButton")) {
|
|
item.classList.remove("downloadHoveringButton");
|
|
}
|
|
|
|
// If the destination element is outside of the richlistitem, clear the
|
|
// selection.
|
|
if (
|
|
!this.contextMenuOpen &&
|
|
!this.subViewOpen &&
|
|
!item.contains(aEvent.relatedTarget)
|
|
) {
|
|
this.richListBox.selectedIndex = -1;
|
|
}
|
|
},
|
|
|
|
onDownloadContextMenu(aEvent) {
|
|
let element = this.richListBox.selectedItem;
|
|
if (!element) {
|
|
return;
|
|
}
|
|
|
|
DownloadsViewController.updateCommands();
|
|
|
|
DownloadsViewUI.updateContextMenuForElement(this.contextMenu, element);
|
|
// Hide the copy location item if there is somehow no URL. We have to do
|
|
// this here instead of in DownloadsViewUI because DownloadsPlacesView
|
|
// allows selecting multiple downloads, so in that view the menuitem will be
|
|
// shown according to whether at least one of the selected items has a URL.
|
|
this.contextMenu.querySelector(
|
|
".downloadCopyLocationMenuItem"
|
|
).hidden = !element._shell.download.source?.url;
|
|
},
|
|
|
|
onDownloadDragStart(aEvent) {
|
|
let element = aEvent.target.closest("richlistitem");
|
|
if (!element) {
|
|
return;
|
|
}
|
|
|
|
// We must check for existence synchronously because this is a DOM event.
|
|
let file = new FileUtils.File(
|
|
DownloadsView.itemForElement(element).download.target.path
|
|
);
|
|
if (!file.exists()) {
|
|
return;
|
|
}
|
|
|
|
let dataTransfer = aEvent.dataTransfer;
|
|
dataTransfer.mozSetDataAt("application/x-moz-file", file, 0);
|
|
dataTransfer.effectAllowed = "copyMove";
|
|
let spec = NetUtil.newURI(file).spec;
|
|
dataTransfer.setData("text/uri-list", spec);
|
|
dataTransfer.setData("text/plain", spec);
|
|
dataTransfer.addElement(element);
|
|
|
|
aEvent.stopPropagation();
|
|
},
|
|
};
|
|
|
|
XPCOMUtils.defineConstant(this, "DownloadsView", DownloadsView);
|
|
|
|
// DownloadsViewItem
|
|
|
|
/**
|
|
* Builds and updates a single item in the downloads list widget, responding to
|
|
* changes in the download state and real-time data, and handles the user
|
|
* interaction events related to a single item in the downloads list widgets.
|
|
*
|
|
* @param download
|
|
* Download object to be associated with the view item.
|
|
* @param aElement
|
|
* XUL element corresponding to the single download item in the view.
|
|
*/
|
|
|
|
class DownloadsViewItem extends DownloadsViewUI.DownloadElementShell {
|
|
constructor(download, aElement) {
|
|
super();
|
|
|
|
this.download = download;
|
|
this.element = aElement;
|
|
this.element._shell = this;
|
|
|
|
this.element.setAttribute("type", "download");
|
|
this.element.classList.add("download-state");
|
|
|
|
this.isPanel = true;
|
|
}
|
|
|
|
onChanged() {
|
|
let newState = DownloadsCommon.stateOfDownload(this.download);
|
|
if (this.downloadState !== newState) {
|
|
this.downloadState = newState;
|
|
this._updateState();
|
|
} else {
|
|
this._updateStateInner();
|
|
}
|
|
}
|
|
|
|
isCommandEnabled(aCommand) {
|
|
switch (aCommand) {
|
|
case "downloadsCmd_open":
|
|
case "downloadsCmd_open:current":
|
|
case "downloadsCmd_open:tab":
|
|
case "downloadsCmd_open:tabshifted":
|
|
case "downloadsCmd_open:window":
|
|
case "downloadsCmd_alwaysOpenSimilarFiles": {
|
|
if (!this.download.succeeded) {
|
|
return false;
|
|
}
|
|
|
|
let file = new FileUtils.File(this.download.target.path);
|
|
return file.exists();
|
|
}
|
|
case "downloadsCmd_show": {
|
|
let file = new FileUtils.File(this.download.target.path);
|
|
if (file.exists()) {
|
|
return true;
|
|
}
|
|
|
|
if (!this.download.target.partFilePath) {
|
|
return false;
|
|
}
|
|
|
|
let partFile = new FileUtils.File(this.download.target.partFilePath);
|
|
return partFile.exists();
|
|
}
|
|
case "downloadsCmd_copyLocation":
|
|
return !!this.download.source?.url;
|
|
case "cmd_delete":
|
|
case "downloadsCmd_doDefault":
|
|
return true;
|
|
case "downloadsCmd_showBlockedInfo":
|
|
return this.download.hasBlockedData;
|
|
}
|
|
return DownloadsViewUI.DownloadElementShell.prototype.isCommandEnabled.call(
|
|
this,
|
|
aCommand
|
|
);
|
|
}
|
|
|
|
doCommand(aCommand) {
|
|
if (this.isCommandEnabled(aCommand)) {
|
|
let [command, modifier] = aCommand.split(":");
|
|
// split off an optional command "modifier" into an argument,
|
|
// e.g. "downloadsCmd_open:window"
|
|
this[command](modifier);
|
|
}
|
|
}
|
|
|
|
// Item commands
|
|
|
|
downloadsCmd_unblock() {
|
|
DownloadsPanel.hidePanel();
|
|
this.confirmUnblock(window, "unblock");
|
|
}
|
|
|
|
downloadsCmd_chooseUnblock() {
|
|
DownloadsPanel.hidePanel();
|
|
this.confirmUnblock(window, "chooseUnblock");
|
|
}
|
|
|
|
downloadsCmd_unblockAndOpen() {
|
|
DownloadsPanel.hidePanel();
|
|
this.unblockAndOpenDownload().catch(Cu.reportError);
|
|
}
|
|
downloadsCmd_unblockAndSave() {
|
|
DownloadsPanel.hidePanel();
|
|
this.unblockAndSave();
|
|
}
|
|
|
|
downloadsCmd_open(openWhere) {
|
|
super.downloadsCmd_open(openWhere);
|
|
|
|
// We explicitly close the panel here to give the user the feedback that
|
|
// their click has been received, and we're handling the action.
|
|
// Otherwise, we'd have to wait for the file-type handler to execute
|
|
// before the panel would close. This also helps to prevent the user from
|
|
// accidentally opening a file several times.
|
|
DownloadsPanel.hidePanel();
|
|
}
|
|
|
|
downloadsCmd_openInSystemViewer() {
|
|
super.downloadsCmd_openInSystemViewer();
|
|
|
|
// We explicitly close the panel here to give the user the feedback that
|
|
// their click has been received, and we're handling the action.
|
|
DownloadsPanel.hidePanel();
|
|
}
|
|
|
|
downloadsCmd_alwaysOpenInSystemViewer() {
|
|
super.downloadsCmd_alwaysOpenInSystemViewer();
|
|
|
|
// We explicitly close the panel here to give the user the feedback that
|
|
// their click has been received, and we're handling the action.
|
|
DownloadsPanel.hidePanel();
|
|
}
|
|
|
|
downloadsCmd_alwaysOpenSimilarFiles() {
|
|
super.downloadsCmd_alwaysOpenSimilarFiles();
|
|
|
|
// We explicitly close the panel here to give the user the feedback that
|
|
// their click has been received, and we're handling the action.
|
|
DownloadsPanel.hidePanel();
|
|
}
|
|
|
|
downloadsCmd_show() {
|
|
let file = new FileUtils.File(this.download.target.path);
|
|
DownloadsCommon.showDownloadedFile(file);
|
|
|
|
// We explicitly close the panel here to give the user the feedback that
|
|
// their click has been received, and we're handling the action.
|
|
// Otherwise, we'd have to wait for the operating system file manager
|
|
// window to open before the panel closed. This also helps to prevent the
|
|
// user from opening the containing folder several times.
|
|
DownloadsPanel.hidePanel();
|
|
}
|
|
|
|
async downloadsCmd_deleteFile() {
|
|
await super.downloadsCmd_deleteFile();
|
|
// Protects against an unusual edge case where the user:
|
|
// 1) downloads a file with Firefox; 2) deletes the file from outside of Firefox, e.g., a file manager;
|
|
// 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;
|
|
// Under those conditions, Firefox will make 2 view items even though there's only 1 file.
|
|
// 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.
|
|
// So the remaining view item needs to be refreshed to hide the "Delete" option.
|
|
// That example only concerns 2 duplicate view items but you can have an arbitrary number, so iterate over all items...
|
|
for (let viewItem of DownloadsView._visibleViewItems.values()) {
|
|
viewItem.download.refresh().catch(Cu.reportError);
|
|
}
|
|
// Don't use DownloadsPanel.hidePanel for this method because it will remove
|
|
// the view item from the list, which is already sufficient feedback.
|
|
}
|
|
|
|
downloadsCmd_showBlockedInfo() {
|
|
DownloadsBlockedSubview.toggle(
|
|
this.element,
|
|
...this.rawBlockedTitleAndDetails
|
|
);
|
|
}
|
|
|
|
downloadsCmd_openReferrer() {
|
|
openURL(this.download.source.referrerInfo.originalReferrer);
|
|
}
|
|
|
|
downloadsCmd_copyLocation() {
|
|
DownloadsCommon.copyDownloadLink(this.download);
|
|
}
|
|
|
|
downloadsCmd_doDefault() {
|
|
let defaultCommand = this.currentDefaultCommandName;
|
|
if (defaultCommand && this.isCommandEnabled(defaultCommand)) {
|
|
this.doCommand(defaultCommand);
|
|
}
|
|
}
|
|
}
|
|
|
|
// DownloadsViewController
|
|
|
|
/**
|
|
* Handles part of the user interaction events raised by the downloads list
|
|
* widget, in particular the "commands" that apply to multiple items, and
|
|
* dispatches the commands that apply to individual items.
|
|
*/
|
|
var DownloadsViewController = {
|
|
// Initialization and termination
|
|
|
|
initialize() {
|
|
window.controllers.insertControllerAt(0, this);
|
|
},
|
|
|
|
terminate() {
|
|
window.controllers.removeController(this);
|
|
},
|
|
|
|
// nsIController
|
|
|
|
supportsCommand(aCommand) {
|
|
if (aCommand === "downloadsCmd_clearList") {
|
|
return true;
|
|
}
|
|
// Firstly, determine if this is a command that we can handle.
|
|
if (!DownloadsViewUI.isCommandName(aCommand)) {
|
|
return false;
|
|
}
|
|
// Strip off any :modifier suffix before checking if the command name is
|
|
// a method on our view
|
|
let [command] = aCommand.split(":");
|
|
if (!(command in this) && !(command in DownloadsViewItem.prototype)) {
|
|
return false;
|
|
}
|
|
// The currently supported commands depend on whether the blocked subview is
|
|
// showing. If it is, then take the following path.
|
|
if (DownloadsView.subViewOpen) {
|
|
let blockedSubviewCmds = [
|
|
"downloadsCmd_unblockAndOpen",
|
|
"cmd_delete",
|
|
"downloadsCmd_unblockAndSave",
|
|
];
|
|
return blockedSubviewCmds.includes(aCommand);
|
|
}
|
|
// If the blocked subview is not showing, then determine if focus is on a
|
|
// control in the downloads list.
|
|
let element = document.commandDispatcher.focusedElement;
|
|
while (element && element != DownloadsView.richListBox) {
|
|
element = element.parentNode;
|
|
}
|
|
// We should handle the command only if the downloads list is among the
|
|
// ancestors of the focused element.
|
|
return !!element;
|
|
},
|
|
|
|
isCommandEnabled(aCommand) {
|
|
// Handle commands that are not selection-specific.
|
|
if (aCommand == "downloadsCmd_clearList") {
|
|
return DownloadsCommon.getData(window).canRemoveFinished;
|
|
}
|
|
|
|
// Other commands are selection-specific.
|
|
let element = DownloadsView.richListBox.selectedItem;
|
|
return (
|
|
element &&
|
|
DownloadsView.itemForElement(element).isCommandEnabled(aCommand)
|
|
);
|
|
},
|
|
|
|
doCommand(aCommand) {
|
|
// If this command is not selection-specific, execute it.
|
|
if (aCommand in this) {
|
|
this[aCommand]();
|
|
return;
|
|
}
|
|
|
|
// Other commands are selection-specific.
|
|
let element = DownloadsView.richListBox.selectedItem;
|
|
if (element) {
|
|
// The doCommand function also checks if the command is enabled.
|
|
DownloadsView.itemForElement(element).doCommand(aCommand);
|
|
}
|
|
},
|
|
|
|
onEvent() {},
|
|
|
|
// Other functions
|
|
|
|
updateCommands() {
|
|
function updateCommandsForObject(object) {
|
|
for (let name in object) {
|
|
if (DownloadsViewUI.isCommandName(name)) {
|
|
goUpdateCommand(name);
|
|
}
|
|
}
|
|
}
|
|
updateCommandsForObject(this);
|
|
updateCommandsForObject(DownloadsViewItem.prototype);
|
|
},
|
|
|
|
// Selection-independent commands
|
|
|
|
downloadsCmd_clearList() {
|
|
DownloadsCommon.getData(window).removeFinished();
|
|
},
|
|
};
|
|
|
|
XPCOMUtils.defineConstant(
|
|
this,
|
|
"DownloadsViewController",
|
|
DownloadsViewController
|
|
);
|
|
|
|
// DownloadsSummary
|
|
|
|
/**
|
|
* Manages the summary at the bottom of the downloads panel list if the number
|
|
* of items in the list exceeds the panels limit.
|
|
*/
|
|
var DownloadsSummary = {
|
|
/**
|
|
* Sets the active state of the summary. When active, the summary subscribes
|
|
* to the DownloadsCommon DownloadsSummaryData singleton.
|
|
*
|
|
* @param aActive
|
|
* Set to true to activate the summary.
|
|
*/
|
|
set active(aActive) {
|
|
if (aActive == this._active || !this._summaryNode) {
|
|
return;
|
|
}
|
|
if (aActive) {
|
|
DownloadsCommon.getSummary(
|
|
window,
|
|
DownloadsView.kItemCountLimit
|
|
).refreshView(this);
|
|
} else {
|
|
DownloadsFooter.showingSummary = false;
|
|
}
|
|
|
|
this._active = aActive;
|
|
},
|
|
|
|
/**
|
|
* Returns the active state of the downloads summary.
|
|
*/
|
|
get active() {
|
|
return this._active;
|
|
},
|
|
|
|
_active: false,
|
|
|
|
/**
|
|
* Sets whether or not we show the progress bar.
|
|
*
|
|
* @param aShowingProgress
|
|
* True if we should show the progress bar.
|
|
*/
|
|
set showingProgress(aShowingProgress) {
|
|
if (aShowingProgress) {
|
|
this._summaryNode.setAttribute("inprogress", "true");
|
|
} else {
|
|
this._summaryNode.removeAttribute("inprogress");
|
|
}
|
|
// If progress isn't being shown, then we simply do not show the summary.
|
|
DownloadsFooter.showingSummary = aShowingProgress;
|
|
},
|
|
|
|
/**
|
|
* Sets the amount of progress that is visible in the progress bar.
|
|
*
|
|
* @param aValue
|
|
* A value between 0 and 100 to represent the progress of the
|
|
* summarized downloads.
|
|
*/
|
|
set percentComplete(aValue) {
|
|
if (this._progressNode) {
|
|
this._progressNode.setAttribute("value", aValue);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Sets the description for the download summary.
|
|
*
|
|
* @param aValue
|
|
* A string representing the description of the summarized
|
|
* downloads.
|
|
*/
|
|
set description(aValue) {
|
|
if (this._descriptionNode) {
|
|
this._descriptionNode.setAttribute("value", aValue);
|
|
this._descriptionNode.setAttribute("tooltiptext", aValue);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Sets the details for the download summary, such as the time remaining,
|
|
* the amount of bytes transferred, etc.
|
|
*
|
|
* @param aValue
|
|
* A string representing the details of the summarized
|
|
* downloads.
|
|
*/
|
|
set details(aValue) {
|
|
if (this._detailsNode) {
|
|
this._detailsNode.setAttribute("value", aValue);
|
|
this._detailsNode.setAttribute("tooltiptext", aValue);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Focuses the root element of the summary.
|
|
*/
|
|
focus(focusOptions) {
|
|
if (this._summaryNode) {
|
|
this._summaryNode.focus(focusOptions);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Respond to keydown events on the Downloads Summary node.
|
|
*
|
|
* @param aEvent
|
|
* The keydown event being handled.
|
|
*/
|
|
onKeyDown(aEvent) {
|
|
if (
|
|
aEvent.charCode == " ".charCodeAt(0) ||
|
|
aEvent.keyCode == KeyEvent.DOM_VK_RETURN
|
|
) {
|
|
DownloadsPanel.showDownloadsHistory();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Respond to click events on the Downloads Summary node.
|
|
*
|
|
* @param aEvent
|
|
* The click event being handled.
|
|
*/
|
|
onClick(aEvent) {
|
|
DownloadsPanel.showDownloadsHistory();
|
|
},
|
|
|
|
/**
|
|
* Element corresponding to the root of the downloads summary.
|
|
*/
|
|
get _summaryNode() {
|
|
let node = document.getElementById("downloadsSummary");
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
delete this._summaryNode;
|
|
return (this._summaryNode = node);
|
|
},
|
|
|
|
/**
|
|
* Element corresponding to the progress bar in the downloads summary.
|
|
*/
|
|
get _progressNode() {
|
|
let node = document.getElementById("downloadsSummaryProgress");
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
delete this._progressNode;
|
|
return (this._progressNode = node);
|
|
},
|
|
|
|
/**
|
|
* Element corresponding to the main description of the downloads
|
|
* summary.
|
|
*/
|
|
get _descriptionNode() {
|
|
let node = document.getElementById("downloadsSummaryDescription");
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
delete this._descriptionNode;
|
|
return (this._descriptionNode = node);
|
|
},
|
|
|
|
/**
|
|
* Element corresponding to the secondary description of the downloads
|
|
* summary.
|
|
*/
|
|
get _detailsNode() {
|
|
let node = document.getElementById("downloadsSummaryDetails");
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
delete this._detailsNode;
|
|
return (this._detailsNode = node);
|
|
},
|
|
};
|
|
|
|
XPCOMUtils.defineConstant(this, "DownloadsSummary", DownloadsSummary);
|
|
|
|
// DownloadsFooter
|
|
|
|
/**
|
|
* Manages events sent to to the footer vbox, which contains both the
|
|
* DownloadsSummary as well as the "Show all downloads" button.
|
|
*/
|
|
var DownloadsFooter = {
|
|
/**
|
|
* Focuses the appropriate element within the footer. If the summary
|
|
* is visible, focus it. If not, focus the "Show all downloads"
|
|
* button.
|
|
*/
|
|
focus(focusOptions) {
|
|
if (this._showingSummary) {
|
|
DownloadsSummary.focus(focusOptions);
|
|
} else {
|
|
DownloadsView.downloadsHistory.focus(focusOptions);
|
|
}
|
|
},
|
|
|
|
_showingSummary: false,
|
|
|
|
/**
|
|
* Sets whether or not the Downloads Summary should be displayed in the
|
|
* footer. If not, the "Show all downloads" button is shown instead.
|
|
*/
|
|
set showingSummary(aValue) {
|
|
if (this._footerNode) {
|
|
if (aValue) {
|
|
this._footerNode.setAttribute("showingsummary", "true");
|
|
} else {
|
|
this._footerNode.removeAttribute("showingsummary");
|
|
}
|
|
this._showingSummary = aValue;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Element corresponding to the footer of the downloads panel.
|
|
*/
|
|
get _footerNode() {
|
|
let node = document.getElementById("downloadsFooter");
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
delete this._footerNode;
|
|
return (this._footerNode = node);
|
|
},
|
|
};
|
|
|
|
XPCOMUtils.defineConstant(this, "DownloadsFooter", DownloadsFooter);
|
|
|
|
// DownloadsBlockedSubview
|
|
|
|
/**
|
|
* Manages the blocked subview that slides in when you click a blocked download.
|
|
*/
|
|
var DownloadsBlockedSubview = {
|
|
/**
|
|
* Elements in the subview.
|
|
*/
|
|
get elements() {
|
|
let idSuffixes = [
|
|
"title",
|
|
"details1",
|
|
"details2",
|
|
"unblockButton",
|
|
"deleteButton",
|
|
];
|
|
let elements = idSuffixes.reduce((memo, s) => {
|
|
memo[s] = document.getElementById("downloadsPanel-blockedSubview-" + s);
|
|
return memo;
|
|
}, {});
|
|
delete this.elements;
|
|
return (this.elements = elements);
|
|
},
|
|
|
|
/**
|
|
* The blocked-download richlistitem element that was clicked to show the
|
|
* subview. If the subview is not showing, this is undefined.
|
|
*/
|
|
element: undefined,
|
|
|
|
/**
|
|
* Slides in the blocked subview.
|
|
*
|
|
* @param element
|
|
* The blocked-download richlistitem element that was clicked.
|
|
* @param title
|
|
* The title to show in the subview.
|
|
* @param details
|
|
* An array of strings with information about the block.
|
|
*/
|
|
toggle(element, title, details) {
|
|
DownloadsView.subViewOpen = true;
|
|
DownloadsViewController.updateCommands();
|
|
const { download } = DownloadsView.itemForElement(element);
|
|
|
|
let e = this.elements;
|
|
let s = DownloadsCommon.strings;
|
|
|
|
title.l10n
|
|
? document.l10n.setAttributes(e.title, title.l10n.id, title.l10n.args)
|
|
: (e.title.textContent = title);
|
|
|
|
details[0].l10n
|
|
? document.l10n.setAttributes(
|
|
e.details1,
|
|
details[0].l10n.id,
|
|
details[0].l10n.args
|
|
)
|
|
: (e.details1.textContent = details[0]);
|
|
|
|
e.details2.textContent = details[1];
|
|
|
|
if (download.launchWhenSucceeded) {
|
|
e.unblockButton.label = s.unblockButtonOpen;
|
|
e.unblockButton.command = "downloadsCmd_unblockAndOpen";
|
|
} else {
|
|
e.unblockButton.label = s.unblockButtonUnblock;
|
|
e.unblockButton.command = "downloadsCmd_unblockAndSave";
|
|
}
|
|
|
|
e.deleteButton.label = s.unblockButtonConfirmBlock;
|
|
|
|
let verdict = element.getAttribute("verdict");
|
|
this.subview.setAttribute("verdict", verdict);
|
|
|
|
this.mainView.addEventListener("ViewShown", this);
|
|
DownloadsPanel.panel.addEventListener("popuphidden", this);
|
|
this.panelMultiView.showSubView(this.subview);
|
|
|
|
// Without this, the mainView is more narrow than the panel once all
|
|
// downloads are removed from the panel.
|
|
this.mainView.style.minWidth = window.getComputedStyle(this.subview).width;
|
|
},
|
|
|
|
handleEvent(event) {
|
|
// This is called when the main view is shown or the panel is hidden.
|
|
DownloadsView.subViewOpen = false;
|
|
this.mainView.removeEventListener("ViewShown", this);
|
|
DownloadsPanel.panel.removeEventListener("popuphidden", this);
|
|
// Focus the proper element if we're going back to the main panel.
|
|
if (event.type == "ViewShown") {
|
|
DownloadsPanel.showPanel();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Deletes the download and hides the entire panel.
|
|
*/
|
|
confirmBlock() {
|
|
goDoCommand("cmd_delete");
|
|
DownloadsPanel.hidePanel();
|
|
},
|
|
};
|
|
|
|
XPCOMUtils.defineLazyGetter(DownloadsBlockedSubview, "panelMultiView", () =>
|
|
document.getElementById("downloadsPanel-multiView")
|
|
);
|
|
XPCOMUtils.defineLazyGetter(DownloadsBlockedSubview, "mainView", () =>
|
|
document.getElementById("downloadsPanel-mainView")
|
|
);
|
|
XPCOMUtils.defineLazyGetter(DownloadsBlockedSubview, "subview", () =>
|
|
document.getElementById("downloadsPanel-blockedSubview")
|
|
);
|
|
|
|
XPCOMUtils.defineConstant(
|
|
this,
|
|
"DownloadsBlockedSubview",
|
|
DownloadsBlockedSubview
|
|
);
|