forked from mirrors/gecko-dev
This is part of the work to remove XUL overlays. The overlay was originally created to help startup performance, but inlining the overlay seems to no longer have detrimental effects to performance and is more straightforward. MozReview-Commit-ID: 7sfVel6qvgv --HG-- extra : rebase_source : b6311cf1fb98851b4f4203c314ae9ecc8160d766
643 lines
20 KiB
JavaScript
643 lines
20 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 indicator that displays the progress of ongoing downloads, which
|
|
* is also used as the anchor for the downloads panel.
|
|
*
|
|
* This module includes the following constructors and global objects:
|
|
*
|
|
* DownloadsButton
|
|
* Main entry point for the downloads indicator. Depending on how the toolbars
|
|
* have been customized, this object determines if we should show a fully
|
|
* functional indicator, a placeholder used during customization and in the
|
|
* customization palette, or a neutral view as a temporary anchor for the
|
|
* downloads panel.
|
|
*
|
|
* DownloadsIndicatorView
|
|
* Builds and updates the actual downloads status widget, responding to changes
|
|
* in the global status data, or provides a neutral view if the indicator is
|
|
* removed from the toolbars and only used as a temporary anchor. In addition,
|
|
* handles the user interaction events raised by the widget.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
|
|
|
|
// DownloadsButton
|
|
|
|
/**
|
|
* Main entry point for the downloads indicator. Depending on how the toolbars
|
|
* have been customized, this object determines if we should show a fully
|
|
* functional indicator, a placeholder used during customization and in the
|
|
* customization palette, or a neutral view as a temporary anchor for the
|
|
* downloads panel.
|
|
*/
|
|
const DownloadsButton = {
|
|
/**
|
|
* Returns a reference to the downloads button position placeholder, or null
|
|
* if not available because it has been removed from the toolbars.
|
|
*/
|
|
get _placeholder() {
|
|
return document.getElementById("downloads-button");
|
|
},
|
|
|
|
/**
|
|
* Indicates whether toolbar customization is in progress.
|
|
*/
|
|
_customizing: false,
|
|
|
|
/**
|
|
* This function is called asynchronously just after window initialization.
|
|
*
|
|
* NOTE: This function should limit the input/output it performs to improve
|
|
* startup time.
|
|
*/
|
|
initializeIndicator() {
|
|
DownloadsIndicatorView.ensureInitialized();
|
|
},
|
|
|
|
/**
|
|
* Determines the position where the indicator should appear, and moves its
|
|
* associated element to the new position.
|
|
*
|
|
* @return Anchor element, or null if the indicator is not visible.
|
|
*/
|
|
_getAnchorInternal() {
|
|
let indicator = DownloadsIndicatorView.indicator;
|
|
if (!indicator) {
|
|
// Exit now if the button is not in the document.
|
|
return null;
|
|
}
|
|
|
|
indicator.open = this._anchorRequested;
|
|
|
|
let widget = CustomizableUI.getWidget("downloads-button");
|
|
// Determine if the indicator is located on an invisible toolbar.
|
|
if (!isElementVisible(indicator.parentNode) &&
|
|
widget.areaType == CustomizableUI.TYPE_TOOLBAR) {
|
|
return null;
|
|
}
|
|
|
|
return DownloadsIndicatorView.indicatorAnchor;
|
|
},
|
|
|
|
/**
|
|
* Indicates whether we should try and show the indicator temporarily as an
|
|
* anchor for the panel, even if the indicator would be hidden by default.
|
|
*/
|
|
_anchorRequested: false,
|
|
|
|
/**
|
|
* Ensures that there is an anchor available for the panel.
|
|
*
|
|
* @return Anchor element where the panel should be anchored, or null if an
|
|
* anchor is not available (for example because both the tab bar and
|
|
* the navigation bar are hidden).
|
|
*/
|
|
getAnchor() {
|
|
// Do not allow anchoring the panel to the element while customizing.
|
|
if (this._customizing) {
|
|
return null;
|
|
}
|
|
|
|
this._anchorRequested = true;
|
|
return this._getAnchorInternal();
|
|
},
|
|
|
|
/**
|
|
* Allows the temporary anchor to be hidden.
|
|
*/
|
|
releaseAnchor() {
|
|
this._anchorRequested = false;
|
|
this._getAnchorInternal();
|
|
},
|
|
|
|
/**
|
|
* Unhide the button. Generally, this only needs to use the placeholder.
|
|
* However, when starting customize mode, if the button is in the palette,
|
|
* we need to unhide it before customize mode is entered, otherwise it
|
|
* gets ignored by customize mode. To do this, we pass true for
|
|
* `includePalette`. We don't always look in the palette because it's
|
|
* inefficient (compared to getElementById), shouldn't be necessary, and
|
|
* if _placeholder returned the node even if in the palette, other checks
|
|
* would break.
|
|
*
|
|
* @param includePalette whether to search the palette, too. Defaults to false.
|
|
*/
|
|
unhide(includePalette = false) {
|
|
let button = this._placeholder;
|
|
if (!button && includePalette) {
|
|
button = gNavToolbox.palette.querySelector("#downloads-button");
|
|
}
|
|
if (button && button.hasAttribute("hidden")) {
|
|
button.removeAttribute("hidden");
|
|
if (this._navBar.contains(button)) {
|
|
this._navBar.setAttribute("downloadsbuttonshown", "true");
|
|
}
|
|
}
|
|
},
|
|
|
|
hide() {
|
|
let button = this._placeholder;
|
|
if (this.autoHideDownloadsButton && button && button.closest("toolbar")) {
|
|
DownloadsPanel.hidePanel();
|
|
button.setAttribute("hidden", "true");
|
|
this._navBar.removeAttribute("downloadsbuttonshown");
|
|
}
|
|
},
|
|
|
|
startAutoHide() {
|
|
if (DownloadsIndicatorView.hasDownloads) {
|
|
this.unhide();
|
|
} else {
|
|
this.hide();
|
|
}
|
|
},
|
|
|
|
checkForAutoHide() {
|
|
let button = this._placeholder;
|
|
if (!this._customizing && this.autoHideDownloadsButton &&
|
|
button && button.closest("toolbar")) {
|
|
this.startAutoHide();
|
|
} else {
|
|
this.unhide();
|
|
}
|
|
},
|
|
|
|
// Callback from CustomizableUI when nodes get moved around.
|
|
// We use this to track whether our node has moved somewhere
|
|
// where we should (not) autohide it.
|
|
onWidgetAfterDOMChange(node) {
|
|
if (node == this._placeholder) {
|
|
this.checkForAutoHide();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* This function is called when toolbar customization starts.
|
|
*
|
|
* During customization, we never show the actual download progress indication
|
|
* or the event notifications, but we show a neutral placeholder. The neutral
|
|
* placeholder is an ordinary button defined in the browser window that can be
|
|
* moved freely between the toolbars and the customization palette.
|
|
*/
|
|
onCustomizeStart(win) {
|
|
if (win == window) {
|
|
// Prevent the indicator from being displayed as a temporary anchor
|
|
// during customization, even if requested using the getAnchor method.
|
|
this._customizing = true;
|
|
this._anchorRequested = false;
|
|
this.unhide(true);
|
|
}
|
|
},
|
|
|
|
onCustomizeEnd(win) {
|
|
if (win == window) {
|
|
this._customizing = false;
|
|
this.checkForAutoHide();
|
|
DownloadsIndicatorView.afterCustomize();
|
|
}
|
|
},
|
|
|
|
init() {
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
this, "autoHideDownloadsButton", "browser.download.autohideButton",
|
|
true, this.checkForAutoHide.bind(this));
|
|
|
|
CustomizableUI.addListener(this);
|
|
this.checkForAutoHide();
|
|
},
|
|
|
|
uninit() {
|
|
CustomizableUI.removeListener(this);
|
|
},
|
|
|
|
get _tabsToolbar() {
|
|
delete this._tabsToolbar;
|
|
return this._tabsToolbar = document.getElementById("TabsToolbar");
|
|
},
|
|
|
|
get _navBar() {
|
|
delete this._navBar;
|
|
return this._navBar = document.getElementById("nav-bar");
|
|
}
|
|
};
|
|
|
|
Object.defineProperty(this, "DownloadsButton", {
|
|
value: DownloadsButton,
|
|
enumerable: true,
|
|
writable: false
|
|
});
|
|
|
|
// DownloadsIndicatorView
|
|
|
|
/**
|
|
* Builds and updates the actual downloads status widget, responding to changes
|
|
* in the global status data, or provides a neutral view if the indicator is
|
|
* removed from the toolbars and only used as a temporary anchor. In addition,
|
|
* handles the user interaction events raised by the widget.
|
|
*/
|
|
const DownloadsIndicatorView = {
|
|
/**
|
|
* True when the view is connected with the underlying downloads data.
|
|
*/
|
|
_initialized: false,
|
|
|
|
/**
|
|
* True when the user interface elements required to display the indicator
|
|
* have finished loading in the browser window, and can be referenced.
|
|
*/
|
|
_operational: false,
|
|
|
|
/**
|
|
* Prepares the downloads indicator to be displayed.
|
|
*/
|
|
ensureInitialized() {
|
|
if (this._initialized) {
|
|
return;
|
|
}
|
|
this._initialized = true;
|
|
|
|
window.addEventListener("unload", this.onWindowUnload);
|
|
DownloadsCommon.getIndicatorData(window).addView(this);
|
|
},
|
|
|
|
/**
|
|
* Frees the internal resources related to the indicator.
|
|
*/
|
|
ensureTerminated() {
|
|
if (!this._initialized) {
|
|
return;
|
|
}
|
|
this._initialized = false;
|
|
|
|
window.removeEventListener("unload", this.onWindowUnload);
|
|
DownloadsCommon.getIndicatorData(window).removeView(this);
|
|
|
|
// Reset the view properties, so that a neutral indicator is displayed if we
|
|
// are visible only temporarily as an anchor.
|
|
this.percentComplete = 0;
|
|
this.attention = DownloadsCommon.ATTENTION_NONE;
|
|
},
|
|
|
|
/**
|
|
* Ensures that the user interface elements required to display the indicator
|
|
* are loaded.
|
|
*/
|
|
_ensureOperational() {
|
|
if (this._operational) {
|
|
return;
|
|
}
|
|
|
|
// If we don't have a _placeholder, there's no chance that everything
|
|
// will load correctly: bail (and don't set _operational to true!)
|
|
if (!DownloadsButton._placeholder) {
|
|
return;
|
|
}
|
|
|
|
this._operational = true;
|
|
|
|
// If the view is initialized, we need to update the elements now that
|
|
// they are finally available in the document.
|
|
if (this._initialized) {
|
|
DownloadsCommon.getIndicatorData(window).refreshView(this);
|
|
}
|
|
},
|
|
|
|
// Direct control functions
|
|
|
|
/**
|
|
* Set to the type ("start" or "finish") when display of a notification is in-progress
|
|
*/
|
|
_currentNotificationType: null,
|
|
|
|
/**
|
|
* Set to the type ("start" or "finish") when a notification arrives while we
|
|
* are waiting for the timeout of the previous notification
|
|
*/
|
|
_nextNotificationType: null,
|
|
|
|
/**
|
|
* Check if the panel containing aNode is open.
|
|
* @param aNode
|
|
* the node whose panel we're interested in.
|
|
*/
|
|
_isAncestorPanelOpen(aNode) {
|
|
while (aNode && aNode.localName != "panel") {
|
|
aNode = aNode.parentNode;
|
|
}
|
|
return aNode && aNode.state == "open";
|
|
},
|
|
|
|
/**
|
|
* Display or enqueue a visual notification of a relevant event, like a new download.
|
|
*
|
|
* @param aType
|
|
* Set to "start" for new downloads, "finish" for completed downloads.
|
|
*/
|
|
showEventNotification(aType) {
|
|
if (!this._initialized) {
|
|
return;
|
|
}
|
|
|
|
if (!DownloadsCommon.animateNotifications) {
|
|
return;
|
|
}
|
|
|
|
// enqueue this notification while the current one is being displayed
|
|
if (this._currentNotificationType) {
|
|
// only queue up the notification if it is different to the current one
|
|
if (this._currentNotificationType != aType) {
|
|
this._nextNotificationType = aType;
|
|
}
|
|
} else {
|
|
this._showNotification(aType);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* If the status indicator is visible in its assigned position, shows for a
|
|
* brief time a visual notification of a relevant event, like a new download.
|
|
*
|
|
* @param aType
|
|
* Set to "start" for new downloads, "finish" for completed downloads.
|
|
*/
|
|
_showNotification(aType) {
|
|
// No need to show visual notification if the panel is visible.
|
|
if (DownloadsPanel.isPanelShowing) {
|
|
return;
|
|
}
|
|
|
|
let anchor = DownloadsButton._placeholder;
|
|
let widgetGroup = CustomizableUI.getWidget("downloads-button");
|
|
let widget = widgetGroup.forWindow(window);
|
|
if (widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
|
|
if (anchor && this._isAncestorPanelOpen(anchor)) {
|
|
// If the containing panel is open, don't do anything, because the
|
|
// notification would appear under the open panel. See
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=984023
|
|
return;
|
|
}
|
|
|
|
// Otherwise, try to use the anchor of the panel:
|
|
anchor = widget.anchor;
|
|
}
|
|
if (!anchor || !isElementVisible(anchor.parentNode)) {
|
|
// Our container isn't visible, so can't show the animation:
|
|
return;
|
|
}
|
|
|
|
// The notification element is positioned to show in the same location as
|
|
// the downloads button. It's not in the downloads button itself in order to
|
|
// be able to anchor the notification elsewhere if required, and to ensure
|
|
// the notification isn't clipped by overflow properties of the anchor's
|
|
// container.
|
|
// Note: no notifier animation for download finished in Photon
|
|
let notifier = this.notifier;
|
|
|
|
if (aType == "start") {
|
|
// Show the notifier before measuring for size/placement. Being hidden by default
|
|
// avoids the interference with scrolling/APZ when the notifier element is
|
|
// tall enough to overlap the tabbrowser element
|
|
notifier.removeAttribute("hidden");
|
|
|
|
// the anchor height may vary if font-size is changed or
|
|
// compact/tablet mode is selected so recalculate this each time
|
|
let anchorRect = anchor.getBoundingClientRect();
|
|
let notifierRect = notifier.getBoundingClientRect();
|
|
let topDiff = anchorRect.top - notifierRect.top;
|
|
let leftDiff = anchorRect.left - notifierRect.left;
|
|
let heightDiff = anchorRect.height - notifierRect.height;
|
|
let widthDiff = anchorRect.width - notifierRect.width;
|
|
let translateX = (leftDiff + .5 * widthDiff) + "px";
|
|
let translateY = (topDiff + .5 * heightDiff) + "px";
|
|
notifier.style.transform = "translate(" + translateX + ", " + translateY + ")";
|
|
notifier.setAttribute("notification", aType);
|
|
}
|
|
anchor.setAttribute("notification", aType);
|
|
|
|
let animationDuration;
|
|
// This value is determined by the overall duration of animation in CSS.
|
|
animationDuration = aType == "start" ? 760 : 850;
|
|
|
|
this._currentNotificationType = aType;
|
|
|
|
setTimeout(() => {
|
|
requestAnimationFrame(() => {
|
|
notifier.setAttribute("hidden", "true");
|
|
notifier.removeAttribute("notification");
|
|
notifier.style.transform = "";
|
|
anchor.removeAttribute("notification");
|
|
|
|
requestAnimationFrame(() => {
|
|
let nextType = this._nextNotificationType;
|
|
this._currentNotificationType = null;
|
|
this._nextNotificationType = null;
|
|
if (nextType) {
|
|
this._showNotification(nextType);
|
|
}
|
|
});
|
|
});
|
|
}, animationDuration);
|
|
},
|
|
|
|
// Callback functions from DownloadsIndicatorData
|
|
|
|
/**
|
|
* Indicates whether the indicator should be shown because there are some
|
|
* downloads to be displayed.
|
|
*/
|
|
set hasDownloads(aValue) {
|
|
if (this._hasDownloads != aValue || (!this._operational && aValue)) {
|
|
this._hasDownloads = aValue;
|
|
|
|
// If there is at least one download, ensure that the view elements are
|
|
// operational
|
|
if (aValue) {
|
|
DownloadsButton.unhide();
|
|
this._ensureOperational();
|
|
} else {
|
|
DownloadsButton.checkForAutoHide();
|
|
}
|
|
}
|
|
return aValue;
|
|
},
|
|
get hasDownloads() {
|
|
return this._hasDownloads;
|
|
},
|
|
_hasDownloads: false,
|
|
|
|
/**
|
|
* Progress indication to display, from 0 to 100, or -1 if unknown.
|
|
* Progress is not visible if the current progress is unknown.
|
|
*/
|
|
set percentComplete(aValue) {
|
|
if (!this._operational) {
|
|
return this._percentComplete;
|
|
}
|
|
|
|
if (this._percentComplete !== aValue) {
|
|
this._percentComplete = aValue;
|
|
this._refreshAttention();
|
|
|
|
if (this._percentComplete >= 0) {
|
|
this.indicator.setAttribute("progress", "true");
|
|
// For arrow type only:
|
|
// We set animationDelay to a minus value (0s ~ -100s) to show the
|
|
// corresponding frame needed for progress.
|
|
this._progressIcon.style.animationDelay = (-this._percentComplete) + "s";
|
|
} else {
|
|
this.indicator.removeAttribute("progress");
|
|
this._progressIcon.style.animationDelay = "1s";
|
|
}
|
|
}
|
|
return aValue;
|
|
},
|
|
_percentComplete: null,
|
|
|
|
/**
|
|
* Set when the indicator should draw user attention to itself.
|
|
*/
|
|
set attention(aValue) {
|
|
if (!this._operational) {
|
|
return this._attention;
|
|
}
|
|
if (this._attention != aValue) {
|
|
this._attention = aValue;
|
|
this._refreshAttention();
|
|
}
|
|
return this._attention;
|
|
},
|
|
|
|
_refreshAttention() {
|
|
// Check if the downloads button is in the menu panel, to determine which
|
|
// button needs to get a badge.
|
|
let widgetGroup = CustomizableUI.getWidget("downloads-button");
|
|
let inMenu = widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL;
|
|
|
|
// For arrow-Styled indicator, suppress success attention if we have
|
|
// progress in toolbar
|
|
let suppressAttention = !inMenu &&
|
|
this._attention == DownloadsCommon.ATTENTION_SUCCESS &&
|
|
this._percentComplete >= 0;
|
|
|
|
if (suppressAttention || this._attention == DownloadsCommon.ATTENTION_NONE) {
|
|
this.indicator.removeAttribute("attention");
|
|
} else {
|
|
this.indicator.setAttribute("attention", this._attention);
|
|
}
|
|
},
|
|
_attention: DownloadsCommon.ATTENTION_NONE,
|
|
|
|
// User interface event functions
|
|
|
|
onWindowUnload() {
|
|
// This function is registered as an event listener, we can't use "this".
|
|
DownloadsIndicatorView.ensureTerminated();
|
|
},
|
|
|
|
onCommand(aEvent) {
|
|
if (aEvent.type == "mousedown" && aEvent.button != 0) {
|
|
return;
|
|
}
|
|
|
|
DownloadsPanel.showPanel();
|
|
aEvent.stopPropagation();
|
|
},
|
|
|
|
onDragOver(aEvent) {
|
|
browserDragAndDrop.dragOver(aEvent);
|
|
},
|
|
|
|
onDrop(aEvent) {
|
|
let dt = aEvent.dataTransfer;
|
|
// If dragged item is from our source, do not try to
|
|
// redownload already downloaded file.
|
|
if (dt.mozGetDataAt("application/x-moz-file", 0))
|
|
return;
|
|
|
|
let links = browserDragAndDrop.dropLinks(aEvent);
|
|
if (!links.length)
|
|
return;
|
|
let sourceDoc = dt.mozSourceNode ? dt.mozSourceNode.ownerDocument : document;
|
|
let handled = false;
|
|
for (let link of links) {
|
|
if (link.url.startsWith("about:"))
|
|
continue;
|
|
saveURL(link.url, link.name, null, true, true, null, sourceDoc);
|
|
handled = true;
|
|
}
|
|
if (handled) {
|
|
aEvent.preventDefault();
|
|
}
|
|
},
|
|
|
|
_indicator: null,
|
|
__progressIcon: null,
|
|
|
|
/**
|
|
* Returns a reference to the main indicator element, or null if the element
|
|
* is not present in the browser window yet.
|
|
*/
|
|
get indicator() {
|
|
if (this._indicator) {
|
|
return this._indicator;
|
|
}
|
|
|
|
let indicator = document.getElementById("downloads-button");
|
|
if (!indicator || indicator.getAttribute("indicator") != "true") {
|
|
return null;
|
|
}
|
|
|
|
return this._indicator = indicator;
|
|
},
|
|
|
|
get indicatorAnchor() {
|
|
let widgetGroup = CustomizableUI.getWidget("downloads-button");
|
|
if (widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
|
|
let overflowIcon = widgetGroup.forWindow(window).anchor;
|
|
return document.getAnonymousElementByAttribute(overflowIcon, "class", "toolbarbutton-icon");
|
|
}
|
|
|
|
return document.getAnonymousElementByAttribute(this.indicator, "class",
|
|
"toolbarbutton-badge-stack");
|
|
},
|
|
|
|
get _progressIcon() {
|
|
return this.__progressIcon ||
|
|
(this.__progressIcon = document.getElementById("downloads-indicator-progress-inner"));
|
|
},
|
|
|
|
get notifier() {
|
|
return this._notifier ||
|
|
(this._notifier = document.getElementById("downloads-notification-anchor"));
|
|
},
|
|
|
|
_onCustomizedAway() {
|
|
this._indicator = null;
|
|
this.__progressIcon = null;
|
|
},
|
|
|
|
afterCustomize() {
|
|
// If the cached indicator is not the one currently in the document,
|
|
// invalidate our references
|
|
if (this._indicator != document.getElementById("downloads-button")) {
|
|
this._onCustomizedAway();
|
|
this._operational = false;
|
|
this.ensureTerminated();
|
|
this.ensureInitialized();
|
|
}
|
|
},
|
|
};
|
|
|
|
Object.defineProperty(this, "DownloadsIndicatorView", {
|
|
value: DownloadsIndicatorView,
|
|
enumerable: true,
|
|
writable: false
|
|
});
|