michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * Handles the indicator that displays the progress of ongoing downloads, which michael@0: * is also used as the anchor for the downloads panel. michael@0: * michael@0: * This module includes the following constructors and global objects: michael@0: * michael@0: * DownloadsButton michael@0: * Main entry point for the downloads indicator. Depending on how the toolbars michael@0: * have been customized, this object determines if we should show a fully michael@0: * functional indicator, a placeholder used during customization and in the michael@0: * customization palette, or a neutral view as a temporary anchor for the michael@0: * downloads panel. michael@0: * michael@0: * DownloadsIndicatorView michael@0: * Builds and updates the actual downloads status widget, responding to changes michael@0: * in the global status data, or provides a neutral view if the indicator is michael@0: * removed from the toolbars and only used as a temporary anchor. In addition, michael@0: * handles the user interaction events raised by the widget. michael@0: */ michael@0: michael@0: "use strict"; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadsButton michael@0: michael@0: /** michael@0: * Main entry point for the downloads indicator. Depending on how the toolbars michael@0: * have been customized, this object determines if we should show a fully michael@0: * functional indicator, a placeholder used during customization and in the michael@0: * customization palette, or a neutral view as a temporary anchor for the michael@0: * downloads panel. michael@0: */ michael@0: const DownloadsButton = { michael@0: /** michael@0: * Location of the indicator overlay. michael@0: */ michael@0: get kIndicatorOverlay() michael@0: "chrome://browser/content/downloads/indicatorOverlay.xul", michael@0: michael@0: /** michael@0: * Returns a reference to the downloads button position placeholder, or null michael@0: * if not available because it has been removed from the toolbars. michael@0: */ michael@0: get _placeholder() michael@0: { michael@0: return document.getElementById("downloads-button"); michael@0: }, michael@0: michael@0: /** michael@0: * This function is called asynchronously just after window initialization. michael@0: * michael@0: * NOTE: This function should limit the input/output it performs to improve michael@0: * startup time. michael@0: */ michael@0: initializeIndicator: function DB_initializeIndicator() michael@0: { michael@0: DownloadsIndicatorView.ensureInitialized(); michael@0: }, michael@0: michael@0: /** michael@0: * Indicates whether toolbar customization is in progress. michael@0: */ michael@0: _customizing: false, michael@0: michael@0: /** michael@0: * This function is called when toolbar customization starts. michael@0: * michael@0: * During customization, we never show the actual download progress indication michael@0: * or the event notifications, but we show a neutral placeholder. The neutral michael@0: * placeholder is an ordinary button defined in the browser window that can be michael@0: * moved freely between the toolbars and the customization palette. michael@0: */ michael@0: customizeStart: function DB_customizeStart() michael@0: { michael@0: // Prevent the indicator from being displayed as a temporary anchor michael@0: // during customization, even if requested using the getAnchor method. michael@0: this._customizing = true; michael@0: this._anchorRequested = false; michael@0: }, michael@0: michael@0: /** michael@0: * This function is called when toolbar customization ends. michael@0: */ michael@0: customizeDone: function DB_customizeDone() michael@0: { michael@0: this._customizing = false; michael@0: DownloadsIndicatorView.afterCustomize(); michael@0: }, michael@0: michael@0: /** michael@0: * Determines the position where the indicator should appear, and moves its michael@0: * associated element to the new position. michael@0: * michael@0: * @return Anchor element, or null if the indicator is not visible. michael@0: */ michael@0: _getAnchorInternal: function DB_getAnchorInternal() michael@0: { michael@0: let indicator = DownloadsIndicatorView.indicator; michael@0: if (!indicator) { michael@0: // Exit now if the indicator overlay isn't loaded yet, or if the button michael@0: // is not in the document. michael@0: return null; michael@0: } michael@0: michael@0: indicator.open = this._anchorRequested; michael@0: michael@0: let widget = CustomizableUI.getWidget("downloads-button") michael@0: .forWindow(window); michael@0: // Determine if the indicator is located on an invisible toolbar. michael@0: if (!isElementVisible(indicator.parentNode) && !widget.overflowed) { michael@0: return null; michael@0: } michael@0: michael@0: return DownloadsIndicatorView.indicatorAnchor; michael@0: }, michael@0: michael@0: /** michael@0: * Checks whether the indicator is, or will soon be visible in the browser michael@0: * window. michael@0: * michael@0: * @param aCallback michael@0: * Called once the indicator overlay has loaded. Gets a boolean michael@0: * argument representing the indicator visibility. michael@0: */ michael@0: checkIsVisible: function DB_checkIsVisible(aCallback) michael@0: { michael@0: function DB_CEV_callback() { michael@0: if (!this._placeholder) { michael@0: aCallback(false); michael@0: } else { michael@0: let element = DownloadsIndicatorView.indicator || this._placeholder; michael@0: aCallback(isElementVisible(element.parentNode)); michael@0: } michael@0: } michael@0: DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay, michael@0: DB_CEV_callback.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Indicates whether we should try and show the indicator temporarily as an michael@0: * anchor for the panel, even if the indicator would be hidden by default. michael@0: */ michael@0: _anchorRequested: false, michael@0: michael@0: /** michael@0: * Ensures that there is an anchor available for the panel. michael@0: * michael@0: * @param aCallback michael@0: * Called when the anchor is available, passing the element where the michael@0: * panel should be anchored, or null if an anchor is not available (for michael@0: * example because both the tab bar and the navigation bar are hidden). michael@0: */ michael@0: getAnchor: function DB_getAnchor(aCallback) michael@0: { michael@0: // Do not allow anchoring the panel to the element while customizing. michael@0: if (this._customizing) { michael@0: aCallback(null); michael@0: return; michael@0: } michael@0: michael@0: function DB_GA_callback() { michael@0: this._anchorRequested = true; michael@0: aCallback(this._getAnchorInternal()); michael@0: } michael@0: michael@0: DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay, michael@0: DB_GA_callback.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Allows the temporary anchor to be hidden. michael@0: */ michael@0: releaseAnchor: function DB_releaseAnchor() michael@0: { michael@0: this._anchorRequested = false; michael@0: this._getAnchorInternal(); michael@0: }, michael@0: michael@0: get _tabsToolbar() michael@0: { michael@0: delete this._tabsToolbar; michael@0: return this._tabsToolbar = document.getElementById("TabsToolbar"); michael@0: }, michael@0: michael@0: get _navBar() michael@0: { michael@0: delete this._navBar; michael@0: return this._navBar = document.getElementById("nav-bar"); michael@0: } michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadsIndicatorView michael@0: michael@0: /** michael@0: * Builds and updates the actual downloads status widget, responding to changes michael@0: * in the global status data, or provides a neutral view if the indicator is michael@0: * removed from the toolbars and only used as a temporary anchor. In addition, michael@0: * handles the user interaction events raised by the widget. michael@0: */ michael@0: const DownloadsIndicatorView = { michael@0: /** michael@0: * True when the view is connected with the underlying downloads data. michael@0: */ michael@0: _initialized: false, michael@0: michael@0: /** michael@0: * True when the user interface elements required to display the indicator michael@0: * have finished loading in the browser window, and can be referenced. michael@0: */ michael@0: _operational: false, michael@0: michael@0: /** michael@0: * Prepares the downloads indicator to be displayed. michael@0: */ michael@0: ensureInitialized: function DIV_ensureInitialized() michael@0: { michael@0: if (this._initialized) { michael@0: return; michael@0: } michael@0: this._initialized = true; michael@0: michael@0: window.addEventListener("unload", this.onWindowUnload, false); michael@0: DownloadsCommon.getIndicatorData(window).addView(this); michael@0: }, michael@0: michael@0: /** michael@0: * Frees the internal resources related to the indicator. michael@0: */ michael@0: ensureTerminated: function DIV_ensureTerminated() michael@0: { michael@0: if (!this._initialized) { michael@0: return; michael@0: } michael@0: this._initialized = false; michael@0: michael@0: window.removeEventListener("unload", this.onWindowUnload, false); michael@0: DownloadsCommon.getIndicatorData(window).removeView(this); michael@0: michael@0: // Reset the view properties, so that a neutral indicator is displayed if we michael@0: // are visible only temporarily as an anchor. michael@0: this.counter = ""; michael@0: this.percentComplete = 0; michael@0: this.paused = false; michael@0: this.attention = false; michael@0: }, michael@0: michael@0: /** michael@0: * Ensures that the user interface elements required to display the indicator michael@0: * are loaded, then invokes the given callback. michael@0: */ michael@0: _ensureOperational: function DIV_ensureOperational(aCallback) michael@0: { michael@0: if (this._operational) { michael@0: if (aCallback) { michael@0: aCallback(); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: // If we don't have a _placeholder, there's no chance that the overlay michael@0: // will load correctly: bail (and don't set _operational to true!) michael@0: if (!DownloadsButton._placeholder) { michael@0: return; michael@0: } michael@0: michael@0: function DIV_EO_callback() { michael@0: this._operational = true; michael@0: michael@0: // If the view is initialized, we need to update the elements now that michael@0: // they are finally available in the document. michael@0: if (this._initialized) { michael@0: DownloadsCommon.getIndicatorData(window).refreshView(this); michael@0: } michael@0: michael@0: if (aCallback) { michael@0: aCallback(); michael@0: } michael@0: } michael@0: michael@0: DownloadsOverlayLoader.ensureOverlayLoaded( michael@0: DownloadsButton.kIndicatorOverlay, michael@0: DIV_EO_callback.bind(this)); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Direct control functions michael@0: michael@0: /** michael@0: * Set while we are waiting for a notification to fade out. michael@0: */ michael@0: _notificationTimeout: null, michael@0: michael@0: /** michael@0: * Check if the panel containing aNode is open. michael@0: * @param aNode michael@0: * the node whose panel we're interested in. michael@0: */ michael@0: _isAncestorPanelOpen: function DIV_isAncestorPanelOpen(aNode) michael@0: { michael@0: while (aNode && aNode.localName != "panel") { michael@0: aNode = aNode.parentNode; michael@0: } michael@0: return aNode && aNode.state == "open"; michael@0: }, michael@0: michael@0: /** michael@0: * If the status indicator is visible in its assigned position, shows for a michael@0: * brief time a visual notification of a relevant event, like a new download. michael@0: * michael@0: * @param aType michael@0: * Set to "start" for new downloads, "finish" for completed downloads. michael@0: */ michael@0: showEventNotification: function DIV_showEventNotification(aType) michael@0: { michael@0: if (!this._initialized) { michael@0: return; michael@0: } michael@0: michael@0: if (!DownloadsCommon.animateNotifications) { michael@0: return; michael@0: } michael@0: michael@0: // No need to show visual notification if the panel is visible. michael@0: if (DownloadsPanel.isPanelShowing) { michael@0: return; michael@0: } michael@0: michael@0: let anchor = DownloadsButton._placeholder; michael@0: let widgetGroup = CustomizableUI.getWidget("downloads-button"); michael@0: let widget = widgetGroup.forWindow(window); michael@0: if (widget.overflowed || widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) { michael@0: if (anchor && this._isAncestorPanelOpen(anchor)) { michael@0: // If the containing panel is open, don't do anything, because the michael@0: // notification would appear under the open panel. See michael@0: // https://bugzilla.mozilla.org/show_bug.cgi?id=984023 michael@0: return; michael@0: } michael@0: michael@0: // Otherwise, try to use the anchor of the panel: michael@0: anchor = widget.anchor; michael@0: } michael@0: if (!anchor || !isElementVisible(anchor.parentNode)) { michael@0: // Our container isn't visible, so can't show the animation: michael@0: return; michael@0: } michael@0: michael@0: if (this._notificationTimeout) { michael@0: clearTimeout(this._notificationTimeout); michael@0: } michael@0: michael@0: // The notification element is positioned to show in the same location as michael@0: // the downloads button. It's not in the downloads button itself in order to michael@0: // be able to anchor the notification elsewhere if required, and to ensure michael@0: // the notification isn't clipped by overflow properties of the anchor's michael@0: // container. michael@0: let notifier = this.notifier; michael@0: if (notifier.style.transform == '') { michael@0: let anchorRect = anchor.getBoundingClientRect(); michael@0: let notifierRect = notifier.getBoundingClientRect(); michael@0: let topDiff = anchorRect.top - notifierRect.top; michael@0: let leftDiff = anchorRect.left - notifierRect.left; michael@0: let heightDiff = anchorRect.height - notifierRect.height; michael@0: let widthDiff = anchorRect.width - notifierRect.width; michael@0: let translateX = (leftDiff + .5 * widthDiff) + "px"; michael@0: let translateY = (topDiff + .5 * heightDiff) + "px"; michael@0: notifier.style.transform = "translate(" + translateX + ", " + translateY + ")"; michael@0: } michael@0: notifier.setAttribute("notification", aType); michael@0: this._notificationTimeout = setTimeout(function () { michael@0: notifier.removeAttribute("notification"); michael@0: notifier.style.transform = ''; michael@0: }, 1000); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Callback functions from DownloadsIndicatorData michael@0: michael@0: /** michael@0: * Indicates whether the indicator should be shown because there are some michael@0: * downloads to be displayed. michael@0: */ michael@0: set hasDownloads(aValue) michael@0: { michael@0: if (this._hasDownloads != aValue || (!this._operational && aValue)) { michael@0: this._hasDownloads = aValue; michael@0: michael@0: // If there is at least one download, ensure that the view elements are michael@0: if (aValue) { michael@0: this._ensureOperational(); michael@0: } michael@0: } michael@0: return aValue; michael@0: }, michael@0: get hasDownloads() michael@0: { michael@0: return this._hasDownloads; michael@0: }, michael@0: _hasDownloads: false, michael@0: michael@0: /** michael@0: * Status text displayed in the indicator. If this is set to an empty value, michael@0: * then the small downloads icon is displayed instead of the text. michael@0: */ michael@0: set counter(aValue) michael@0: { michael@0: if (!this._operational) { michael@0: return this._counter; michael@0: } michael@0: michael@0: if (this._counter !== aValue) { michael@0: this._counter = aValue; michael@0: if (this._counter) michael@0: this.indicator.setAttribute("counter", "true"); michael@0: else michael@0: this.indicator.removeAttribute("counter"); michael@0: // We have to set the attribute instead of using the property because the michael@0: // XBL binding isn't applied if the element is invisible for any reason. michael@0: this._indicatorCounter.setAttribute("value", aValue); michael@0: } michael@0: return aValue; michael@0: }, michael@0: _counter: null, michael@0: michael@0: /** michael@0: * Progress indication to display, from 0 to 100, or -1 if unknown. The michael@0: * progress bar is hidden if the current progress is unknown and no status michael@0: * text is set in the "counter" property. michael@0: */ michael@0: set percentComplete(aValue) michael@0: { michael@0: if (!this._operational) { michael@0: return this._percentComplete; michael@0: } michael@0: michael@0: if (this._percentComplete !== aValue) { michael@0: this._percentComplete = aValue; michael@0: if (this._percentComplete >= 0) michael@0: this.indicator.setAttribute("progress", "true"); michael@0: else michael@0: this.indicator.removeAttribute("progress"); michael@0: // We have to set the attribute instead of using the property because the michael@0: // XBL binding isn't applied if the element is invisible for any reason. michael@0: this._indicatorProgress.setAttribute("value", Math.max(aValue, 0)); michael@0: } michael@0: return aValue; michael@0: }, michael@0: _percentComplete: null, michael@0: michael@0: /** michael@0: * Indicates whether the progress won't advance because of a paused state. michael@0: * Setting this property forces a paused progress bar to be displayed, even if michael@0: * the current progress information is unavailable. michael@0: */ michael@0: set paused(aValue) michael@0: { michael@0: if (!this._operational) { michael@0: return this._paused; michael@0: } michael@0: michael@0: if (this._paused != aValue) { michael@0: this._paused = aValue; michael@0: if (this._paused) { michael@0: this.indicator.setAttribute("paused", "true") michael@0: } else { michael@0: this.indicator.removeAttribute("paused"); michael@0: } michael@0: } michael@0: return aValue; michael@0: }, michael@0: _paused: false, michael@0: michael@0: /** michael@0: * Set when the indicator should draw user attention to itself. michael@0: */ michael@0: set attention(aValue) michael@0: { michael@0: if (!this._operational) { michael@0: return this._attention; michael@0: } michael@0: michael@0: if (this._attention != aValue) { michael@0: this._attention = aValue; michael@0: if (aValue) { michael@0: this.indicator.setAttribute("attention", "true"); michael@0: } else { michael@0: this.indicator.removeAttribute("attention"); michael@0: } michael@0: } michael@0: return aValue; michael@0: }, michael@0: _attention: false, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// User interface event functions michael@0: michael@0: onWindowUnload: function DIV_onWindowUnload() michael@0: { michael@0: // This function is registered as an event listener, we can't use "this". michael@0: DownloadsIndicatorView.ensureTerminated(); michael@0: }, michael@0: michael@0: onCommand: function DIV_onCommand(aEvent) michael@0: { michael@0: // If the downloads button is in the menu panel, open the Library michael@0: let widgetGroup = CustomizableUI.getWidget("downloads-button"); michael@0: if (widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) { michael@0: DownloadsPanel.showDownloadsHistory(); michael@0: } else { michael@0: DownloadsPanel.showPanel(); michael@0: } michael@0: michael@0: aEvent.stopPropagation(); michael@0: }, michael@0: michael@0: onDragOver: function DIV_onDragOver(aEvent) michael@0: { michael@0: browserDragAndDrop.dragOver(aEvent); michael@0: }, michael@0: michael@0: onDrop: function DIV_onDrop(aEvent) michael@0: { michael@0: let dt = aEvent.dataTransfer; michael@0: // If dragged item is from our source, do not try to michael@0: // redownload already downloaded file. michael@0: if (dt.mozGetDataAt("application/x-moz-file", 0)) michael@0: return; michael@0: michael@0: let name = {}; michael@0: let url = browserDragAndDrop.drop(aEvent, name); michael@0: if (url) { michael@0: if (url.startsWith("about:")) { michael@0: return; michael@0: } michael@0: michael@0: let sourceDoc = dt.mozSourceNode ? dt.mozSourceNode.ownerDocument : document; michael@0: saveURL(url, name.value, null, true, true, null, sourceDoc); michael@0: aEvent.preventDefault(); michael@0: } michael@0: }, michael@0: michael@0: _indicator: null, michael@0: __indicatorCounter: null, michael@0: __indicatorProgress: null, michael@0: michael@0: /** michael@0: * Returns a reference to the main indicator element, or null if the element michael@0: * is not present in the browser window yet. michael@0: */ michael@0: get indicator() michael@0: { michael@0: if (this._indicator) { michael@0: return this._indicator; michael@0: } michael@0: michael@0: let indicator = document.getElementById("downloads-button"); michael@0: if (!indicator || indicator.getAttribute("indicator") != "true") { michael@0: return null; michael@0: } michael@0: michael@0: return this._indicator = indicator; michael@0: }, michael@0: michael@0: get indicatorAnchor() michael@0: { michael@0: let widget = CustomizableUI.getWidget("downloads-button") michael@0: .forWindow(window); michael@0: if (widget.overflowed) { michael@0: return widget.anchor; michael@0: } michael@0: return document.getElementById("downloads-indicator-anchor"); michael@0: }, michael@0: michael@0: get _indicatorCounter() michael@0: { michael@0: return this.__indicatorCounter || michael@0: (this.__indicatorCounter = document.getElementById("downloads-indicator-counter")); michael@0: }, michael@0: michael@0: get _indicatorProgress() michael@0: { michael@0: return this.__indicatorProgress || michael@0: (this.__indicatorProgress = document.getElementById("downloads-indicator-progress")); michael@0: }, michael@0: michael@0: get notifier() michael@0: { michael@0: return this._notifier || michael@0: (this._notifier = document.getElementById("downloads-notification-anchor")); michael@0: }, michael@0: michael@0: _onCustomizedAway: function() { michael@0: this._indicator = null; michael@0: this.__indicatorCounter = null; michael@0: this.__indicatorProgress = null; michael@0: }, michael@0: michael@0: afterCustomize: function() { michael@0: // If the cached indicator is not the one currently in the document, michael@0: // invalidate our references michael@0: if (this._indicator != document.getElementById("downloads-button")) { michael@0: this._onCustomizedAway(); michael@0: this._operational = false; michael@0: this.ensureTerminated(); michael@0: this.ensureInitialized(); michael@0: } michael@0: } michael@0: }; michael@0: