/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* 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() "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, and in particular should not cause the Download Manager * service to start. */ initializeIndicator: function() { this._update(); }, /** * 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: function() { // Hide the indicator and prevent it to be displayed as a temporary anchor // during customization, even if requested using the getAnchor method. this._customizing = true; this._anchorRequested = false; let indicator = DownloadsIndicatorView.indicator; if (indicator) { indicator.collapsed = true; } let placeholder = this._placeholder; if (placeholder) { placeholder.collapsed = false; } }, /** * This function is called when toolbar customization ends. */ customizeDone: function() { this._customizing = false; this._update(); }, /** * This function is called during initialization or when toolbar customization * ends. It determines if we should enable or disable the object that keeps * the indicator updated, and ensures that the placeholder is hidden unless it * has been moved to the customization palette. * * NOTE: This function is also called on startup, thus it should limit the * input/output it performs, and in particular should not cause the * Download Manager service to start. */ _update: function() { this._updatePositionInternal(); if (!DownloadsCommon.useToolkitUI) { DownloadsIndicatorView.ensureInitialized(); } else { DownloadsIndicatorView.ensureTerminated(); } }, /** * Determines the position where the indicator should appear, and moves its * associated element to the new position. This does not happen if the * indicator is currently being used as the anchor for the panel, to ensure * that the panel doesn't flicker because we move the DOM element to which * it's anchored. */ updatePosition: function() { if (!this._anchorRequested) { this._updatePositionInternal(); } }, /** * 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. */ _updatePositionInternal: function() { let indicator = DownloadsIndicatorView.indicator; if (!indicator) { // Exit now if the indicator overlay isn't loaded yet. return null; } let placeholder = this._placeholder; if (!placeholder) { // The placeholder has been removed from the browser window. indicator.collapsed = true; // Move the indicator to a safe position on the toolbar, since otherwise // it may break the merge of adjacent items, like back/forward + urlbar. indicator.parentNode.appendChild(indicator); return null; } // Position the indicator where the placeholder is located. We should // update the position even if the placeholder is located on an invisible // toolbar, because the toolbar may be displayed later. placeholder.parentNode.insertBefore(indicator, placeholder); placeholder.collapsed = true; indicator.collapsed = false; indicator.open = this._anchorRequested; // Determine if the placeholder is located on an invisible toolbar. if (!isElementVisible(placeholder.parentNode)) { 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: function(aCallback) { function DB_CEV_callback() { if (!this._placeholder) { aCallback(false); } else { let element = DownloadsIndicatorView.indicator || this._placeholder; aCallback(isElementVisible(element.parentNode)); } } DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay, DB_CEV_callback.bind(this)); }, /** * 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: function(aCallback) { // Do not allow anchoring the panel to the element while customizing. if (this._customizing) { aCallback(null); return; } function DB_GA_callback() { this._anchorRequested = true; aCallback(this._updatePositionInternal()); } DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay, DB_GA_callback.bind(this)); }, /** * Allows the temporary anchor to be hidden. */ releaseAnchor: function() { this._anchorRequested = false; this._updatePositionInternal(); }, 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: function() { 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: function() { 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 = false; }, /** * Ensures that the user interface elements required to display the indicator * are loaded, then invokes the given callback. */ _ensureOperational: function(aCallback) { if (this._operational) { aCallback(); return; } function DIV_EO_callback() { 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); } aCallback(); } DownloadsOverlayLoader.ensureOverlayLoaded( DownloadsButton.kIndicatorOverlay, DIV_EO_callback.bind(this)); }, ////////////////////////////////////////////////////////////////////////////// //// Direct control functions /** * Set while we are waiting for a notification to fade out. */ _notificationTimeout: null, /** * 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: function(aType) { if (!this._initialized) { return; } if (!DownloadsCommon.animateNotifications) { return; } // No need to show visual notification if the panel is visible. if (DownloadsPanel.isPanelShowing) { return; } function DIV_SEN_callback() { if (this._notificationTimeout) { clearTimeout(this._notificationTimeout); } // Now that the overlay is loaded, place the indicator in its final // position. DownloadsButton.updatePosition(); let indicator = this.indicator; indicator.setAttribute("notification", aType); this._notificationTimeout = setTimeout( function() indicator.removeAttribute("notification"), 1000); } this._ensureOperational(DIV_SEN_callback.bind(this)); }, ////////////////////////////////////////////////////////////////////////////// //// 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._hasDownloads = aValue; // If there is at least one download, ensure that the view elements are // loaded before determining the position of the downloads button. if (aValue) { this._ensureOperational(function() DownloadsButton.updatePosition()); } else { DownloadsButton.updatePosition(); } } 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; if (aValue) { this.indicator.setAttribute("attention", "true"); } else { this.indicator.removeAttribute("attention"); } } return aValue; }, _attention: false, ////////////////////////////////////////////////////////////////////////////// //// User interface event functions onWindowUnload: function() { // This function is registered as an event listener, we can't use "this". DownloadsIndicatorView.ensureTerminated(); }, onCommand: function(aEvent) { if (DownloadsCommon.useToolkitUI) { // The panel won't suppress attention for us, we need to clear now. DownloadsCommon.getIndicatorData(window).attention = false; BrowserDownloadsUI(); } else { DownloadsPanel.showPanel(); } aEvent.stopPropagation(); }, onDragOver: function(aEvent) { browserDragAndDrop.dragOver(aEvent); }, onDrop: function(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(); } }, /** * Returns a reference to the main indicator element, or null if the element * is not present in the browser window yet. */ get indicator() { let indicator = document.getElementById("downloads-indicator"); if (!indicator) { return null; } // Once the element is loaded, it will never be unloaded. delete this.indicator; return this.indicator = indicator; }, get indicatorAnchor() { delete this.indicatorAnchor; return this.indicatorAnchor = document.getElementById("downloads-indicator-anchor"); }, get _indicatorCounter() { delete this._indicatorCounter; return this._indicatorCounter = document.getElementById("downloads-indicator-counter"); }, get _indicatorProgress() { delete this._indicatorProgress; return this._indicatorProgress = document.getElementById("downloads-indicator-progress"); } }; Object.defineProperty(this, "DownloadsIndicatorView", { value: DownloadsIndicatorView, enumerable: true, writable: false });