mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-09 21:00:42 +02:00
***
Bug 1514594: Part 3a - Change ChromeUtils.import to return an exports object; not pollute global. r=mccr8
This changes the behavior of ChromeUtils.import() to return an exports object,
rather than a module global, in all cases except when `null` is passed as a
second argument, and changes the default behavior not to pollute the global
scope with the module's exports. Thus, the following code written for the old
model:
ChromeUtils.import("resource://gre/modules/Services.jsm");
is approximately the same as the following, in the new model:
var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
Since the two behaviors are mutually incompatible, this patch will land with a
scripted rewrite to update all existing callers to use the new model rather
than the old.
***
Bug 1514594: Part 3b - Mass rewrite all JS code to use the new ChromeUtils.import API. rs=Gijs
This was done using the followng script:
https://bitbucket.org/kmaglione/m-c-rewrites/src/tip/processors/cu-import-exports.jsm
***
Bug 1514594: Part 3c - Update ESLint plugin for ChromeUtils.import API changes. r=Standard8
Differential Revision: https://phabricator.services.mozilla.com/D16747
***
Bug 1514594: Part 3d - Remove/fix hundreds of duplicate imports from sync tests. r=Gijs
Differential Revision: https://phabricator.services.mozilla.com/D16748
***
Bug 1514594: Part 3e - Remove no-op ChromeUtils.import() calls. r=Gijs
Differential Revision: https://phabricator.services.mozilla.com/D16749
***
Bug 1514594: Part 3f.1 - Cleanup various test corner cases after mass rewrite. r=Gijs
***
Bug 1514594: Part 3f.2 - Cleanup various non-test corner cases after mass rewrite. r=Gijs
Differential Revision: https://phabricator.services.mozilla.com/D16750
--HG--
extra : rebase_source : 359574ee3064c90f33bf36c2ebe3159a24cc8895
extra : histedit_source : b93c8f42808b1599f9122d7842d2c0b3e656a594%2C64a3a4e3359dc889e2ab2b49461bab9e27fc10a7
520 lines
17 KiB
JavaScript
520 lines
17 KiB
JavaScript
/* 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/. */
|
|
|
|
"use strict";
|
|
|
|
var EXPORTED_SYMBOLS = [
|
|
"DownloadsSubview",
|
|
];
|
|
|
|
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
ChromeUtils.defineModuleGetter(this, "AppConstants",
|
|
"resource://gre/modules/AppConstants.jsm");
|
|
ChromeUtils.defineModuleGetter(this, "Downloads",
|
|
"resource://gre/modules/Downloads.jsm");
|
|
ChromeUtils.defineModuleGetter(this, "DownloadsCommon",
|
|
"resource:///modules/DownloadsCommon.jsm");
|
|
ChromeUtils.defineModuleGetter(this, "DownloadsViewUI",
|
|
"resource:///modules/DownloadsViewUI.jsm");
|
|
ChromeUtils.defineModuleGetter(this, "FileUtils",
|
|
"resource://gre/modules/FileUtils.jsm");
|
|
ChromeUtils.defineModuleGetter(this, "PlacesUtils",
|
|
"resource://gre/modules/PlacesUtils.jsm");
|
|
|
|
let gPanelViewInstances = new WeakMap();
|
|
const kRefreshBatchSize = 10;
|
|
const kMaxWaitForIdleMs = 200;
|
|
XPCOMUtils.defineLazyGetter(this, "kButtonLabels", () => {
|
|
return {
|
|
show: DownloadsCommon.strings[AppConstants.platform == "macosx" ? "showMacLabel" : "showLabel"],
|
|
open: DownloadsCommon.strings.openFileLabel,
|
|
retry: DownloadsCommon.strings.retryLabel,
|
|
};
|
|
});
|
|
|
|
class DownloadsSubview extends DownloadsViewUI.BaseView {
|
|
constructor(panelview) {
|
|
super();
|
|
this.document = panelview.ownerDocument;
|
|
this.window = panelview.ownerGlobal;
|
|
|
|
this.context = "panelDownloadsContextMenu";
|
|
|
|
this.panelview = panelview;
|
|
this.container = this.document.getElementById("panelMenu_downloadsMenu");
|
|
while (this.container.lastChild) {
|
|
this.container.lastChild.remove();
|
|
}
|
|
this.panelview.addEventListener("click", DownloadsSubview.onClick);
|
|
this.panelview.addEventListener("ViewHiding", DownloadsSubview.onViewHiding);
|
|
|
|
this._viewItemsForDownloads = new WeakMap();
|
|
|
|
let contextMenu = this.document.getElementById(this.context);
|
|
if (!contextMenu) {
|
|
contextMenu = this.document.getElementById("downloadsContextMenu").cloneNode(true);
|
|
contextMenu.setAttribute("closemenu", "none");
|
|
contextMenu.setAttribute("id", this.context);
|
|
contextMenu.removeAttribute("onpopupshown");
|
|
contextMenu.setAttribute("onpopupshowing",
|
|
"DownloadsSubview.updateContextMenu(document.popupNode, this);");
|
|
contextMenu.setAttribute("onpopuphidden", "DownloadsSubview.onContextMenuHidden(this);");
|
|
let clearButton = contextMenu.querySelector("menuitem[command='downloadsCmd_clearDownloads']");
|
|
clearButton.hidden = false;
|
|
clearButton.previousElementSibling.hidden = true;
|
|
contextMenu.querySelector("menuitem[command='cmd_delete']")
|
|
.setAttribute("command", "downloadsCmd_delete");
|
|
}
|
|
this.panelview.appendChild(contextMenu);
|
|
this.container.setAttribute("context", this.context);
|
|
|
|
this._downloadsData = DownloadsCommon.getData(this.window, true, true, true);
|
|
this._downloadsData.addView(this);
|
|
}
|
|
|
|
destructor(event) {
|
|
this.panelview.removeEventListener("click", DownloadsSubview.onClick);
|
|
this.panelview.removeEventListener("ViewHiding", DownloadsSubview.onViewHiding);
|
|
this._downloadsData.removeView(this);
|
|
gPanelViewInstances.delete(this);
|
|
this.destroyed = true;
|
|
}
|
|
|
|
/**
|
|
* DataView handler; invoked when a batch of downloads is being passed in -
|
|
* usually when this instance is added as a view in the constructor.
|
|
*/
|
|
onDownloadBatchStarting() {
|
|
this.window.clearTimeout(this._batchTimeout);
|
|
}
|
|
|
|
/**
|
|
* DataView handler; invoked when the view stopped feeding its current list of
|
|
* downloads.
|
|
*/
|
|
onDownloadBatchEnded() {
|
|
let {window} = this;
|
|
window.clearTimeout(this._batchTimeout);
|
|
// If there are no downloads to display, wait a bit to dispatch the load
|
|
// completion event, because another batch may start right away.
|
|
this._batchTimeout = window.setTimeout(() => {
|
|
this._updateStatsFromDisk();
|
|
this.panelview.dispatchEvent(new window.CustomEvent("DownloadsLoaded"));
|
|
}, this.container.childElementCount ? 0 : 200);
|
|
}
|
|
|
|
/**
|
|
* DataView handler; invoked when a new download is added to the list.
|
|
*
|
|
* @param {Download} download
|
|
* @param {DOMNode} [options.insertBefore]
|
|
*/
|
|
onDownloadAdded(download, { insertBefore } = {}) {
|
|
let element = this.document.createXULElement("hbox");
|
|
let shell = new DownloadsSubview.Button(download, element);
|
|
this._viewItemsForDownloads.set(download, shell);
|
|
|
|
// Since newest downloads are displayed at the top, either prepend the new
|
|
// element or insert it after the one indicated by the insertBefore option.
|
|
if (insertBefore) {
|
|
this._viewItemsForDownloads.get(insertBefore)
|
|
.element.insertAdjacentElement("afterend", element);
|
|
} else {
|
|
this.container.prepend(element);
|
|
}
|
|
|
|
// After connecting to the document, trigger the code that updates all
|
|
// attributes to match the current state of the downloads.
|
|
shell.ensureActive();
|
|
}
|
|
|
|
/**
|
|
* DataView Handler; invoked when the state of a download changed.
|
|
*
|
|
* @param {Download} download
|
|
*/
|
|
onDownloadChanged(download) {
|
|
this._viewItemsForDownloads.get(download).onChanged();
|
|
}
|
|
|
|
/**
|
|
* DataView handler; invoked when a download is removed.
|
|
*
|
|
* @param {Download} download
|
|
*/
|
|
onDownloadRemoved(download) {
|
|
this._viewItemsForDownloads.get(download).element.remove();
|
|
}
|
|
|
|
/**
|
|
* Schedule a refresh of the downloads that were added, which is mainly about
|
|
* checking whether the target file still exists.
|
|
* We're doing this during idle time and in chunks.
|
|
*/
|
|
async _updateStatsFromDisk() {
|
|
if (this._updatingStats)
|
|
return;
|
|
|
|
this._updatingStats = true;
|
|
|
|
try {
|
|
let idleOptions = { timeout: kMaxWaitForIdleMs };
|
|
// Start with getting an idle moment to (maybe) refresh the list of downloads.
|
|
await new Promise(resolve => this.window.requestIdleCallback(resolve), idleOptions);
|
|
// In the meantime, this instance could have been destroyed, so take note.
|
|
if (this.destroyed)
|
|
return;
|
|
|
|
let count = 0;
|
|
for (let button of this.container.children) {
|
|
if (this.destroyed)
|
|
return;
|
|
if (!button._shell)
|
|
continue;
|
|
|
|
await button._shell.refresh();
|
|
|
|
// Make sure to request a new idle moment every `kRefreshBatchSize` buttons.
|
|
if (++count % kRefreshBatchSize === 0) {
|
|
await new Promise(resolve => this.window.requestIdleCallback(resolve, idleOptions));
|
|
}
|
|
}
|
|
} catch (ex) {
|
|
Cu.reportError(ex);
|
|
} finally {
|
|
this._updatingStats = false;
|
|
}
|
|
}
|
|
|
|
// ----- Static methods. -----
|
|
|
|
/**
|
|
* Show the Downloads subview panel and listen for events that will trigger
|
|
* building the dynamic part of the view.
|
|
*
|
|
* @param {DOMNode} anchor The button that was commanded to trigger this function.
|
|
*/
|
|
static show(anchor) {
|
|
let document = anchor.ownerDocument;
|
|
let window = anchor.ownerGlobal;
|
|
|
|
let panelview = document.getElementById("PanelUI-downloads");
|
|
anchor.setAttribute("closemenu", "none");
|
|
gPanelViewInstances.set(panelview, new DownloadsSubview(panelview));
|
|
|
|
// Since the DownloadsLists are propagated asynchronously, we need to wait a
|
|
// little to get the view propagated.
|
|
panelview.addEventListener("ViewShowing", event => {
|
|
event.detail.addBlocker(new Promise(resolve => {
|
|
panelview.addEventListener("DownloadsLoaded", resolve, { once: true });
|
|
}));
|
|
}, { once: true });
|
|
|
|
window.PanelUI.showSubView("PanelUI-downloads", anchor);
|
|
}
|
|
|
|
/**
|
|
* Handler method; reveal the users' download directory using the OS specific
|
|
* method.
|
|
*/
|
|
static async onShowDownloads() {
|
|
// Retrieve the user's default download directory.
|
|
let preferredDir = await Downloads.getPreferredDownloadsDirectory();
|
|
DownloadsCommon.showDirectory(new FileUtils.File(preferredDir));
|
|
}
|
|
|
|
/**
|
|
* Handler method; clear the list downloads finished and old(er) downloads,
|
|
* just like in the Library.
|
|
*
|
|
* @param {DOMNode} button Button that was clicked to call this method.
|
|
*/
|
|
static onClearDownloads(button) {
|
|
let instance = gPanelViewInstances.get(button.closest("panelview"));
|
|
if (!instance)
|
|
return;
|
|
instance._downloadsData.removeFinished();
|
|
PlacesUtils.history.removeVisitsByFilter({
|
|
transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD,
|
|
}).catch(Cu.reportError);
|
|
}
|
|
|
|
/**
|
|
* Just before showing the context menu, anchored to a download item, we need
|
|
* to set the right properties to make sure the right menu-items are visible.
|
|
*
|
|
* @param {DOMNode} button The Button the context menu will be anchored to.
|
|
* @param {DOMNode} menu The context menu.
|
|
*/
|
|
static updateContextMenu(button, menu) {
|
|
while (!button._shell) {
|
|
button = button.parentNode;
|
|
}
|
|
menu.setAttribute("state", button.getAttribute("state"));
|
|
if (button.hasAttribute("exists"))
|
|
menu.setAttribute("exists", button.getAttribute("exists"));
|
|
else
|
|
menu.removeAttribute("exists");
|
|
menu.classList.toggle("temporary-block", button.classList.contains("temporary-block"));
|
|
for (let menuitem of menu.getElementsByTagName("menuitem")) {
|
|
let command = menuitem.getAttribute("command");
|
|
if (!command)
|
|
continue;
|
|
if (command == "downloadsCmd_clearDownloads") {
|
|
menuitem.disabled = !DownloadsSubview.canClearDownloads(button);
|
|
} else {
|
|
menuitem.disabled = !button._shell.isCommandEnabled(command);
|
|
}
|
|
}
|
|
|
|
// The menu anchorNode property is not available long enough to be used elsewhere,
|
|
// so tack it another property name.
|
|
menu._anchorNode = button;
|
|
}
|
|
|
|
/**
|
|
* Right after the context menu was hidden, perform a bit of cleanup.
|
|
*
|
|
* @param {DOMNode} menu The context menu.
|
|
*/
|
|
static onContextMenuHidden(menu) {
|
|
delete menu._anchorNode;
|
|
}
|
|
|
|
/**
|
|
* Static version of DownloadsSubview#canClearDownloads().
|
|
*
|
|
* @param {DOMNode} button Button that we'll use to find the right
|
|
* DownloadsSubview instance.
|
|
*/
|
|
static canClearDownloads(button) {
|
|
let instance = gPanelViewInstances.get(button.closest("panelview"));
|
|
if (!instance)
|
|
return false;
|
|
return instance.canClearDownloads(instance.container);
|
|
}
|
|
|
|
/**
|
|
* Handler method; invoked when the Downloads panel is hidden and should be
|
|
* torn down & cleaned up.
|
|
*
|
|
* @param {DOMEvent} event
|
|
*/
|
|
static onViewHiding(event) {
|
|
let instance = gPanelViewInstances.get(event.target);
|
|
if (!instance)
|
|
return;
|
|
instance.destructor(event);
|
|
}
|
|
|
|
/**
|
|
* Handler method; invoked when anything is clicked inside the Downloads panel.
|
|
* Depending on the context, it will find the appropriate command to invoke.
|
|
*
|
|
* We don't have a command dispatcher registered for this view, so we don't go
|
|
* through the goDoCommand path like we do for the other views.
|
|
*
|
|
* @param {DOMMouseEvent} event
|
|
*/
|
|
static onClick(event) {
|
|
// Middle clicks fall through and are regarded as left clicks.
|
|
if (event.button > 1)
|
|
return;
|
|
|
|
let button =
|
|
event.target.closest(".subviewbutton,toolbarbutton,menuitem,panelview");
|
|
if (!button || button.localName == "panelview") {
|
|
return;
|
|
}
|
|
|
|
let item = button.closest(".subviewbutton.download");
|
|
|
|
let command = "downloadsCmd_open";
|
|
if (button.classList.contains("action-button")) {
|
|
command = item.hasAttribute("canShow") ? "downloadsCmd_show"
|
|
: "downloadsCmd_retry";
|
|
} else if (button.localName == "menuitem") {
|
|
command = button.getAttribute("command");
|
|
if (command == "downloadsCmd_clearDownloads") {
|
|
DownloadsSubview.onClearDownloads(button);
|
|
return;
|
|
}
|
|
item = button.parentNode._anchorNode;
|
|
}
|
|
|
|
if (item && item._shell.isCommandEnabled(command)) {
|
|
item._shell[command]();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Associates each document with a pre-built DOM fragment representing the
|
|
* download list item. This is then cloned to create each individual list item.
|
|
* This is stored on the document to prevent leaks that would occur if a single
|
|
* instance created by one document's DOMParser was stored globally.
|
|
*/
|
|
var gDownloadsSubviewItemFragments = new WeakMap();
|
|
|
|
DownloadsSubview.Button = class extends DownloadsViewUI.DownloadElementShell {
|
|
constructor(download, element) {
|
|
super();
|
|
this.download = download;
|
|
this.element = element;
|
|
this.element._shell = this;
|
|
|
|
this.element.classList.add("subviewbutton", "subviewbutton-iconic",
|
|
"download", "download-state", "navigable");
|
|
|
|
let hover = event => {
|
|
if (event.originalTarget.classList.contains("action-button")) {
|
|
this.element.classList.toggle("downloadHoveringButton",
|
|
event.type == "mouseover");
|
|
}
|
|
};
|
|
this.element.addEventListener("mouseover", hover);
|
|
this.element.addEventListener("mouseout", hover);
|
|
}
|
|
|
|
get browserWindow() {
|
|
return this.element.ownerGlobal;
|
|
}
|
|
|
|
async refresh() {
|
|
if (this._targetFileChecked)
|
|
return;
|
|
|
|
try {
|
|
await this.download.refresh();
|
|
} catch (ex) {
|
|
Cu.reportError(ex);
|
|
} finally {
|
|
this._targetFileChecked = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle state changes of a download.
|
|
*/
|
|
onStateChanged() {
|
|
// Since the state changed, we may need to check the target file again.
|
|
this._targetFileChecked = false;
|
|
|
|
this._updateState();
|
|
}
|
|
|
|
/**
|
|
* Handler method; invoked when any state attribute of a download changed.
|
|
*/
|
|
onChanged() {
|
|
let newState = DownloadsCommon.stateOfDownload(this.download);
|
|
if (this._downloadState !== newState) {
|
|
this._downloadState = newState;
|
|
this.onStateChanged();
|
|
} else {
|
|
this._updateState();
|
|
}
|
|
}
|
|
|
|
// DownloadElementShell
|
|
connect() {
|
|
let document = this.element.ownerDocument;
|
|
let downloadsSubviewItemFragment =
|
|
gDownloadsSubviewItemFragments.get(document);
|
|
if (!downloadsSubviewItemFragment) {
|
|
let MozXULElement = document.defaultView.MozXULElement;
|
|
downloadsSubviewItemFragment = MozXULElement.parseXULToFragment(`
|
|
<image class="toolbarbutton-icon" validate="always"/>
|
|
<vbox class="toolbarbutton-text" flex="1">
|
|
<label crop="end"/>
|
|
<label class="status-text status-full" crop="end"/>
|
|
<label class="status-text status-open" crop="end"/>
|
|
<label class="status-text status-retry" crop="end"/>
|
|
<label class="status-text status-show" crop="end"/>
|
|
</vbox>
|
|
<toolbarbutton class="action-button"/>
|
|
`);
|
|
gDownloadsSubviewItemFragments.set(document,
|
|
downloadsSubviewItemFragment);
|
|
}
|
|
this.element.appendChild(downloadsSubviewItemFragment.cloneNode(true));
|
|
for (let [propertyName, selector] of [
|
|
["_downloadTypeIcon", ".toolbarbutton-icon"],
|
|
["_downloadTarget", "label"],
|
|
["_downloadStatus", ".status-full"],
|
|
["_downloadButton", ".action-button"],
|
|
]) {
|
|
this[propertyName] = this.element.querySelector(selector);
|
|
}
|
|
|
|
for (let [label, selector] of [
|
|
[kButtonLabels.open, ".status-open"],
|
|
[kButtonLabels.retry, ".status-retry"],
|
|
[kButtonLabels.show, ".status-show"],
|
|
]) {
|
|
this.element.querySelector(selector).value = label;
|
|
}
|
|
}
|
|
|
|
// DownloadElementShell
|
|
showDisplayNameAndIcon(displayName, icon) {
|
|
this._downloadTarget.value = displayName;
|
|
this._downloadTypeIcon.src = icon;
|
|
}
|
|
|
|
// DownloadElementShell
|
|
showProgress() {}
|
|
|
|
// DownloadElementShell
|
|
showStatus(status) {
|
|
this._downloadStatus.value = status;
|
|
this.element.tooltipText = status;
|
|
}
|
|
|
|
// DownloadElementShell
|
|
showButton() {}
|
|
|
|
// DownloadElementShell
|
|
hideButton() {}
|
|
|
|
// DownloadElementShell
|
|
_updateState() {
|
|
// This view only show completed and failed downloads.
|
|
let state = DownloadsCommon.stateOfDownload(this.download);
|
|
let shouldDisplay = state == DownloadsCommon.DOWNLOAD_FINISHED ||
|
|
state == DownloadsCommon.DOWNLOAD_FAILED;
|
|
this.element.hidden = !shouldDisplay;
|
|
if (!shouldDisplay) {
|
|
return;
|
|
}
|
|
|
|
super._updateState();
|
|
|
|
if (this.isCommandEnabled("downloadsCmd_show")) {
|
|
this.element.setAttribute("canShow", "true");
|
|
this.element.removeAttribute("canRetry");
|
|
} else if (this.isCommandEnabled("downloadsCmd_retry")) {
|
|
this.element.setAttribute("canRetry", "true");
|
|
this.element.removeAttribute("canShow");
|
|
} else {
|
|
this.element.removeAttribute("canRetry");
|
|
this.element.removeAttribute("canShow");
|
|
}
|
|
}
|
|
|
|
// DownloadElementShell
|
|
_updateStateInner() {
|
|
if (!this.element.hidden) {
|
|
super._updateStateInner();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Command handler; copy the download URL to the OS general clipboard.
|
|
*/
|
|
downloadsCmd_copyLocation() {
|
|
DownloadsCommon.copyDownloadLink(this.download);
|
|
}
|
|
};
|