diff options
Diffstat (limited to 'browser/components/downloads/content/indicator.js')
-rw-r--r-- | browser/components/downloads/content/indicator.js | 606 |
1 files changed, 606 insertions, 0 deletions
diff --git a/browser/components/downloads/content/indicator.js b/browser/components/downloads/content/indicator.js new file mode 100644 index 000000000..4c22a6e5d --- /dev/null +++ b/browser/components/downloads/content/indicator.js @@ -0,0 +1,606 @@ +/* -*- 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/. */ + +/** + * 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"; + +//////////////////////////////////////////////////////////////////////////////// +//// 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 = { + /** + * Location of the indicator overlay. + */ + get kIndicatorOverlay() { + return "chrome://browser/content/downloads/indicatorOverlay.xul"; + }, + + /** + * 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"); + }, + + /** + * 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(); + }, + + /** + * Indicates whether toolbar customization is in progress. + */ + _customizing: false, + + /** + * 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. + */ + customizeStart() { + // 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 function is called when toolbar customization ends. + */ + customizeDone() { + this._customizing = false; + DownloadsIndicatorView.afterCustomize(); + }, + + /** + * 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 indicator overlay isn't loaded yet, or if the button + // is not in the document. + return null; + } + + indicator.open = this._anchorRequested; + + let widget = CustomizableUI.getWidget("downloads-button") + .forWindow(window); + // Determine if the indicator is located on an invisible toolbar. + if (!isElementVisible(indicator.parentNode) && !widget.overflowed) { + return null; + } + + return DownloadsIndicatorView.indicatorAnchor; + }, + + /** + * Checks whether the indicator is, or will soon be visible in the browser + * window. + * + * @param aCallback + * Called once the indicator overlay has loaded. Gets a boolean + * argument representing the indicator visibility. + */ + checkIsVisible(aCallback) { + DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay, () => { + if (!this._placeholder) { + aCallback(false); + } else { + let element = DownloadsIndicatorView.indicator || this._placeholder; + aCallback(isElementVisible(element.parentNode)); + } + }); + }, + + /** + * 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. + * + * @param aCallback + * Called when the anchor is available, passing the 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(aCallback) { + // Do not allow anchoring the panel to the element while customizing. + if (this._customizing) { + aCallback(null); + return; + } + + DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay, () => { + this._anchorRequested = true; + aCallback(this._getAnchorInternal()); + }); + }, + + /** + * Allows the temporary anchor to be hidden. + */ + releaseAnchor() { + this._anchorRequested = false; + this._getAnchorInternal(); + }, + + 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, false); + 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, false); + 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.counter = ""; + this.percentComplete = 0; + this.paused = false; + this.attention = DownloadsCommon.ATTENTION_NONE; + }, + + /** + * Ensures that the user interface elements required to display the indicator + * are loaded, then invokes the given callback. + */ + _ensureOperational(aCallback) { + if (this._operational) { + if (aCallback) { + aCallback(); + } + return; + } + + // If we don't have a _placeholder, there's no chance that the overlay + // will load correctly: bail (and don't set _operational to true!) + if (!DownloadsButton._placeholder) { + return; + } + + DownloadsOverlayLoader.ensureOverlayLoaded( + DownloadsButton.kIndicatorOverlay, + () => { + 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); + } + + if (aCallback) { + aCallback(); + } + }); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// Direct control functions + + /** + * Set while we are waiting for a notification to fade out. + */ + _notificationTimeout: 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"; + }, + + /** + * 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. + */ + showEventNotification(aType) { + if (!this._initialized) { + return; + } + + if (!DownloadsCommon.animateNotifications) { + return; + } + + // 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 (widget.overflowed || 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; + } + + if (this._notificationTimeout) { + clearTimeout(this._notificationTimeout); + } + + // 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. + let notifier = this.notifier; + if (notifier.style.transform == '') { + 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); + this._notificationTimeout = setTimeout(() => { + notifier.removeAttribute("notification"); + notifier.style.transform = ''; + }, 1000); + }, + + ////////////////////////////////////////////////////////////////////////////// + //// 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 + if (aValue) { + this._ensureOperational(); + } + } + return aValue; + }, + get hasDownloads() { + return this._hasDownloads; + }, + _hasDownloads: false, + + /** + * Status text displayed in the indicator. If this is set to an empty value, + * then the small downloads icon is displayed instead of the text. + */ + set counter(aValue) { + if (!this._operational) { + return this._counter; + } + + if (this._counter !== aValue) { + this._counter = aValue; + if (this._counter) + this.indicator.setAttribute("counter", "true"); + else + this.indicator.removeAttribute("counter"); + // We have to set the attribute instead of using the property because the + // XBL binding isn't applied if the element is invisible for any reason. + this._indicatorCounter.setAttribute("value", aValue); + } + return aValue; + }, + _counter: null, + + /** + * Progress indication to display, from 0 to 100, or -1 if unknown. The + * progress bar is hidden if the current progress is unknown and no status + * text is set in the "counter" property. + */ + set percentComplete(aValue) { + if (!this._operational) { + return this._percentComplete; + } + + if (this._percentComplete !== aValue) { + this._percentComplete = aValue; + if (this._percentComplete >= 0) + this.indicator.setAttribute("progress", "true"); + else + this.indicator.removeAttribute("progress"); + // We have to set the attribute instead of using the property because the + // XBL binding isn't applied if the element is invisible for any reason. + this._indicatorProgress.setAttribute("value", Math.max(aValue, 0)); + } + return aValue; + }, + _percentComplete: null, + + /** + * Indicates whether the progress won't advance because of a paused state. + * Setting this property forces a paused progress bar to be displayed, even if + * the current progress information is unavailable. + */ + set paused(aValue) { + if (!this._operational) { + return this._paused; + } + + if (this._paused != aValue) { + this._paused = aValue; + if (this._paused) { + this.indicator.setAttribute("paused", "true") + } else { + this.indicator.removeAttribute("paused"); + } + } + return aValue; + }, + _paused: false, + + /** + * 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; + + // 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; + + if (aValue == DownloadsCommon.ATTENTION_NONE) { + this.indicator.removeAttribute("attention"); + if (inMenu) { + gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD); + } + } else { + this.indicator.setAttribute("attention", aValue); + if (inMenu) { + let badgeClass = "download-" + aValue; + gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD, badgeClass); + } + } + } + return aValue; + }, + _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 the downloads button is in the menu panel, open the Library + let widgetGroup = CustomizableUI.getWidget("downloads-button"); + if (widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) { + DownloadsPanel.showDownloadsHistory(); + } else { + 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, + __indicatorCounter: null, + __indicatorProgress: 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 widget = CustomizableUI.getWidget("downloads-button") + .forWindow(window); + if (widget.overflowed) { + return widget.anchor; + } + return document.getElementById("downloads-indicator-anchor"); + }, + + get _indicatorCounter() { + return this.__indicatorCounter || + (this.__indicatorCounter = document.getElementById("downloads-indicator-counter")); + }, + + get _indicatorProgress() { + return this.__indicatorProgress || + (this.__indicatorProgress = document.getElementById("downloads-indicator-progress")); + }, + + get notifier() { + return this._notifier || + (this._notifier = document.getElementById("downloads-notification-anchor")); + }, + + _onCustomizedAway() { + this._indicator = null; + this.__indicatorCounter = null; + this.__indicatorProgress = 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 +}); |