/* -*- 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"; Components.utils.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 = { /** * 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; // Determine if the indicator is located on an invisible toolbar. if (!window.toolbar.visible) { 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. * * @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()); }); }, unhideAndPlace() { if (this._placeholder.hasAttribute("hidden")) { this._navBar.setAttribute("hasdownloads", "true"); this._placeholder.removeAttribute("hidden"); let insertionPoint = document.getElementById("urlbar-container"); while (insertionPoint && insertionPoint.nextElementSibling) { let next = insertionPoint.nextElementSibling; if (!next.hidden && (next.nodeName == "toolbarspring" || next.id == "downloads-button" || /* also have to ignore ourselves... */ next.id == "search-container" || next.id == "urlbar-search-splitter")) { insertionPoint = next; } else { break; } } if (insertionPoint && insertionPoint.nextElementSibling != this._placeholder && insertionPoint.nextElementSibling != this._placeholder.nextElementSibling) { insertionPoint.insertAdjacentElement("afterend", this._placeholder); } } }, hide() { DownloadsPanel.hidePanel(); this._navBar.removeAttribute("hasdownloads"); this._placeholder.setAttribute("hidden", "true"); }, /** * 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); 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, then invokes the given callback. */ _ensureOperational(aCallback) { if (this._operational) { if (aCallback) { aCallback(); } 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 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; } if (!window.toolbar.visible) { // 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; let anchor = DownloadsButton._placeholder; 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.unhideAndPlace(); this._ensureOperational(); } } 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; 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; if (this._attention == DownloadsCommon.ATTENTION_NONE) { this.indicator.removeAttribute("attention"); } else { this.indicator.setAttribute("attention", this._attention); } } return 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) { 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() { return document.getElementById("downloads-indicator-anchor"); }, 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 });