1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/components/downloads/content/indicator.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,615 @@ 1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.8 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +/** 1.11 + * Handles the indicator that displays the progress of ongoing downloads, which 1.12 + * is also used as the anchor for the downloads panel. 1.13 + * 1.14 + * This module includes the following constructors and global objects: 1.15 + * 1.16 + * DownloadsButton 1.17 + * Main entry point for the downloads indicator. Depending on how the toolbars 1.18 + * have been customized, this object determines if we should show a fully 1.19 + * functional indicator, a placeholder used during customization and in the 1.20 + * customization palette, or a neutral view as a temporary anchor for the 1.21 + * downloads panel. 1.22 + * 1.23 + * DownloadsIndicatorView 1.24 + * Builds and updates the actual downloads status widget, responding to changes 1.25 + * in the global status data, or provides a neutral view if the indicator is 1.26 + * removed from the toolbars and only used as a temporary anchor. In addition, 1.27 + * handles the user interaction events raised by the widget. 1.28 + */ 1.29 + 1.30 +"use strict"; 1.31 + 1.32 +//////////////////////////////////////////////////////////////////////////////// 1.33 +//// DownloadsButton 1.34 + 1.35 +/** 1.36 + * Main entry point for the downloads indicator. Depending on how the toolbars 1.37 + * have been customized, this object determines if we should show a fully 1.38 + * functional indicator, a placeholder used during customization and in the 1.39 + * customization palette, or a neutral view as a temporary anchor for the 1.40 + * downloads panel. 1.41 + */ 1.42 +const DownloadsButton = { 1.43 + /** 1.44 + * Location of the indicator overlay. 1.45 + */ 1.46 + get kIndicatorOverlay() 1.47 + "chrome://browser/content/downloads/indicatorOverlay.xul", 1.48 + 1.49 + /** 1.50 + * Returns a reference to the downloads button position placeholder, or null 1.51 + * if not available because it has been removed from the toolbars. 1.52 + */ 1.53 + get _placeholder() 1.54 + { 1.55 + return document.getElementById("downloads-button"); 1.56 + }, 1.57 + 1.58 + /** 1.59 + * This function is called asynchronously just after window initialization. 1.60 + * 1.61 + * NOTE: This function should limit the input/output it performs to improve 1.62 + * startup time. 1.63 + */ 1.64 + initializeIndicator: function DB_initializeIndicator() 1.65 + { 1.66 + DownloadsIndicatorView.ensureInitialized(); 1.67 + }, 1.68 + 1.69 + /** 1.70 + * Indicates whether toolbar customization is in progress. 1.71 + */ 1.72 + _customizing: false, 1.73 + 1.74 + /** 1.75 + * This function is called when toolbar customization starts. 1.76 + * 1.77 + * During customization, we never show the actual download progress indication 1.78 + * or the event notifications, but we show a neutral placeholder. The neutral 1.79 + * placeholder is an ordinary button defined in the browser window that can be 1.80 + * moved freely between the toolbars and the customization palette. 1.81 + */ 1.82 + customizeStart: function DB_customizeStart() 1.83 + { 1.84 + // Prevent the indicator from being displayed as a temporary anchor 1.85 + // during customization, even if requested using the getAnchor method. 1.86 + this._customizing = true; 1.87 + this._anchorRequested = false; 1.88 + }, 1.89 + 1.90 + /** 1.91 + * This function is called when toolbar customization ends. 1.92 + */ 1.93 + customizeDone: function DB_customizeDone() 1.94 + { 1.95 + this._customizing = false; 1.96 + DownloadsIndicatorView.afterCustomize(); 1.97 + }, 1.98 + 1.99 + /** 1.100 + * Determines the position where the indicator should appear, and moves its 1.101 + * associated element to the new position. 1.102 + * 1.103 + * @return Anchor element, or null if the indicator is not visible. 1.104 + */ 1.105 + _getAnchorInternal: function DB_getAnchorInternal() 1.106 + { 1.107 + let indicator = DownloadsIndicatorView.indicator; 1.108 + if (!indicator) { 1.109 + // Exit now if the indicator overlay isn't loaded yet, or if the button 1.110 + // is not in the document. 1.111 + return null; 1.112 + } 1.113 + 1.114 + indicator.open = this._anchorRequested; 1.115 + 1.116 + let widget = CustomizableUI.getWidget("downloads-button") 1.117 + .forWindow(window); 1.118 + // Determine if the indicator is located on an invisible toolbar. 1.119 + if (!isElementVisible(indicator.parentNode) && !widget.overflowed) { 1.120 + return null; 1.121 + } 1.122 + 1.123 + return DownloadsIndicatorView.indicatorAnchor; 1.124 + }, 1.125 + 1.126 + /** 1.127 + * Checks whether the indicator is, or will soon be visible in the browser 1.128 + * window. 1.129 + * 1.130 + * @param aCallback 1.131 + * Called once the indicator overlay has loaded. Gets a boolean 1.132 + * argument representing the indicator visibility. 1.133 + */ 1.134 + checkIsVisible: function DB_checkIsVisible(aCallback) 1.135 + { 1.136 + function DB_CEV_callback() { 1.137 + if (!this._placeholder) { 1.138 + aCallback(false); 1.139 + } else { 1.140 + let element = DownloadsIndicatorView.indicator || this._placeholder; 1.141 + aCallback(isElementVisible(element.parentNode)); 1.142 + } 1.143 + } 1.144 + DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay, 1.145 + DB_CEV_callback.bind(this)); 1.146 + }, 1.147 + 1.148 + /** 1.149 + * Indicates whether we should try and show the indicator temporarily as an 1.150 + * anchor for the panel, even if the indicator would be hidden by default. 1.151 + */ 1.152 + _anchorRequested: false, 1.153 + 1.154 + /** 1.155 + * Ensures that there is an anchor available for the panel. 1.156 + * 1.157 + * @param aCallback 1.158 + * Called when the anchor is available, passing the element where the 1.159 + * panel should be anchored, or null if an anchor is not available (for 1.160 + * example because both the tab bar and the navigation bar are hidden). 1.161 + */ 1.162 + getAnchor: function DB_getAnchor(aCallback) 1.163 + { 1.164 + // Do not allow anchoring the panel to the element while customizing. 1.165 + if (this._customizing) { 1.166 + aCallback(null); 1.167 + return; 1.168 + } 1.169 + 1.170 + function DB_GA_callback() { 1.171 + this._anchorRequested = true; 1.172 + aCallback(this._getAnchorInternal()); 1.173 + } 1.174 + 1.175 + DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay, 1.176 + DB_GA_callback.bind(this)); 1.177 + }, 1.178 + 1.179 + /** 1.180 + * Allows the temporary anchor to be hidden. 1.181 + */ 1.182 + releaseAnchor: function DB_releaseAnchor() 1.183 + { 1.184 + this._anchorRequested = false; 1.185 + this._getAnchorInternal(); 1.186 + }, 1.187 + 1.188 + get _tabsToolbar() 1.189 + { 1.190 + delete this._tabsToolbar; 1.191 + return this._tabsToolbar = document.getElementById("TabsToolbar"); 1.192 + }, 1.193 + 1.194 + get _navBar() 1.195 + { 1.196 + delete this._navBar; 1.197 + return this._navBar = document.getElementById("nav-bar"); 1.198 + } 1.199 +}; 1.200 + 1.201 +//////////////////////////////////////////////////////////////////////////////// 1.202 +//// DownloadsIndicatorView 1.203 + 1.204 +/** 1.205 + * Builds and updates the actual downloads status widget, responding to changes 1.206 + * in the global status data, or provides a neutral view if the indicator is 1.207 + * removed from the toolbars and only used as a temporary anchor. In addition, 1.208 + * handles the user interaction events raised by the widget. 1.209 + */ 1.210 +const DownloadsIndicatorView = { 1.211 + /** 1.212 + * True when the view is connected with the underlying downloads data. 1.213 + */ 1.214 + _initialized: false, 1.215 + 1.216 + /** 1.217 + * True when the user interface elements required to display the indicator 1.218 + * have finished loading in the browser window, and can be referenced. 1.219 + */ 1.220 + _operational: false, 1.221 + 1.222 + /** 1.223 + * Prepares the downloads indicator to be displayed. 1.224 + */ 1.225 + ensureInitialized: function DIV_ensureInitialized() 1.226 + { 1.227 + if (this._initialized) { 1.228 + return; 1.229 + } 1.230 + this._initialized = true; 1.231 + 1.232 + window.addEventListener("unload", this.onWindowUnload, false); 1.233 + DownloadsCommon.getIndicatorData(window).addView(this); 1.234 + }, 1.235 + 1.236 + /** 1.237 + * Frees the internal resources related to the indicator. 1.238 + */ 1.239 + ensureTerminated: function DIV_ensureTerminated() 1.240 + { 1.241 + if (!this._initialized) { 1.242 + return; 1.243 + } 1.244 + this._initialized = false; 1.245 + 1.246 + window.removeEventListener("unload", this.onWindowUnload, false); 1.247 + DownloadsCommon.getIndicatorData(window).removeView(this); 1.248 + 1.249 + // Reset the view properties, so that a neutral indicator is displayed if we 1.250 + // are visible only temporarily as an anchor. 1.251 + this.counter = ""; 1.252 + this.percentComplete = 0; 1.253 + this.paused = false; 1.254 + this.attention = false; 1.255 + }, 1.256 + 1.257 + /** 1.258 + * Ensures that the user interface elements required to display the indicator 1.259 + * are loaded, then invokes the given callback. 1.260 + */ 1.261 + _ensureOperational: function DIV_ensureOperational(aCallback) 1.262 + { 1.263 + if (this._operational) { 1.264 + if (aCallback) { 1.265 + aCallback(); 1.266 + } 1.267 + return; 1.268 + } 1.269 + 1.270 + // If we don't have a _placeholder, there's no chance that the overlay 1.271 + // will load correctly: bail (and don't set _operational to true!) 1.272 + if (!DownloadsButton._placeholder) { 1.273 + return; 1.274 + } 1.275 + 1.276 + function DIV_EO_callback() { 1.277 + this._operational = true; 1.278 + 1.279 + // If the view is initialized, we need to update the elements now that 1.280 + // they are finally available in the document. 1.281 + if (this._initialized) { 1.282 + DownloadsCommon.getIndicatorData(window).refreshView(this); 1.283 + } 1.284 + 1.285 + if (aCallback) { 1.286 + aCallback(); 1.287 + } 1.288 + } 1.289 + 1.290 + DownloadsOverlayLoader.ensureOverlayLoaded( 1.291 + DownloadsButton.kIndicatorOverlay, 1.292 + DIV_EO_callback.bind(this)); 1.293 + }, 1.294 + 1.295 + ////////////////////////////////////////////////////////////////////////////// 1.296 + //// Direct control functions 1.297 + 1.298 + /** 1.299 + * Set while we are waiting for a notification to fade out. 1.300 + */ 1.301 + _notificationTimeout: null, 1.302 + 1.303 + /** 1.304 + * Check if the panel containing aNode is open. 1.305 + * @param aNode 1.306 + * the node whose panel we're interested in. 1.307 + */ 1.308 + _isAncestorPanelOpen: function DIV_isAncestorPanelOpen(aNode) 1.309 + { 1.310 + while (aNode && aNode.localName != "panel") { 1.311 + aNode = aNode.parentNode; 1.312 + } 1.313 + return aNode && aNode.state == "open"; 1.314 + }, 1.315 + 1.316 + /** 1.317 + * If the status indicator is visible in its assigned position, shows for a 1.318 + * brief time a visual notification of a relevant event, like a new download. 1.319 + * 1.320 + * @param aType 1.321 + * Set to "start" for new downloads, "finish" for completed downloads. 1.322 + */ 1.323 + showEventNotification: function DIV_showEventNotification(aType) 1.324 + { 1.325 + if (!this._initialized) { 1.326 + return; 1.327 + } 1.328 + 1.329 + if (!DownloadsCommon.animateNotifications) { 1.330 + return; 1.331 + } 1.332 + 1.333 + // No need to show visual notification if the panel is visible. 1.334 + if (DownloadsPanel.isPanelShowing) { 1.335 + return; 1.336 + } 1.337 + 1.338 + let anchor = DownloadsButton._placeholder; 1.339 + let widgetGroup = CustomizableUI.getWidget("downloads-button"); 1.340 + let widget = widgetGroup.forWindow(window); 1.341 + if (widget.overflowed || widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) { 1.342 + if (anchor && this._isAncestorPanelOpen(anchor)) { 1.343 + // If the containing panel is open, don't do anything, because the 1.344 + // notification would appear under the open panel. See 1.345 + // https://bugzilla.mozilla.org/show_bug.cgi?id=984023 1.346 + return; 1.347 + } 1.348 + 1.349 + // Otherwise, try to use the anchor of the panel: 1.350 + anchor = widget.anchor; 1.351 + } 1.352 + if (!anchor || !isElementVisible(anchor.parentNode)) { 1.353 + // Our container isn't visible, so can't show the animation: 1.354 + return; 1.355 + } 1.356 + 1.357 + if (this._notificationTimeout) { 1.358 + clearTimeout(this._notificationTimeout); 1.359 + } 1.360 + 1.361 + // The notification element is positioned to show in the same location as 1.362 + // the downloads button. It's not in the downloads button itself in order to 1.363 + // be able to anchor the notification elsewhere if required, and to ensure 1.364 + // the notification isn't clipped by overflow properties of the anchor's 1.365 + // container. 1.366 + let notifier = this.notifier; 1.367 + if (notifier.style.transform == '') { 1.368 + let anchorRect = anchor.getBoundingClientRect(); 1.369 + let notifierRect = notifier.getBoundingClientRect(); 1.370 + let topDiff = anchorRect.top - notifierRect.top; 1.371 + let leftDiff = anchorRect.left - notifierRect.left; 1.372 + let heightDiff = anchorRect.height - notifierRect.height; 1.373 + let widthDiff = anchorRect.width - notifierRect.width; 1.374 + let translateX = (leftDiff + .5 * widthDiff) + "px"; 1.375 + let translateY = (topDiff + .5 * heightDiff) + "px"; 1.376 + notifier.style.transform = "translate(" + translateX + ", " + translateY + ")"; 1.377 + } 1.378 + notifier.setAttribute("notification", aType); 1.379 + this._notificationTimeout = setTimeout(function () { 1.380 + notifier.removeAttribute("notification"); 1.381 + notifier.style.transform = ''; 1.382 + }, 1000); 1.383 + }, 1.384 + 1.385 + ////////////////////////////////////////////////////////////////////////////// 1.386 + //// Callback functions from DownloadsIndicatorData 1.387 + 1.388 + /** 1.389 + * Indicates whether the indicator should be shown because there are some 1.390 + * downloads to be displayed. 1.391 + */ 1.392 + set hasDownloads(aValue) 1.393 + { 1.394 + if (this._hasDownloads != aValue || (!this._operational && aValue)) { 1.395 + this._hasDownloads = aValue; 1.396 + 1.397 + // If there is at least one download, ensure that the view elements are 1.398 + if (aValue) { 1.399 + this._ensureOperational(); 1.400 + } 1.401 + } 1.402 + return aValue; 1.403 + }, 1.404 + get hasDownloads() 1.405 + { 1.406 + return this._hasDownloads; 1.407 + }, 1.408 + _hasDownloads: false, 1.409 + 1.410 + /** 1.411 + * Status text displayed in the indicator. If this is set to an empty value, 1.412 + * then the small downloads icon is displayed instead of the text. 1.413 + */ 1.414 + set counter(aValue) 1.415 + { 1.416 + if (!this._operational) { 1.417 + return this._counter; 1.418 + } 1.419 + 1.420 + if (this._counter !== aValue) { 1.421 + this._counter = aValue; 1.422 + if (this._counter) 1.423 + this.indicator.setAttribute("counter", "true"); 1.424 + else 1.425 + this.indicator.removeAttribute("counter"); 1.426 + // We have to set the attribute instead of using the property because the 1.427 + // XBL binding isn't applied if the element is invisible for any reason. 1.428 + this._indicatorCounter.setAttribute("value", aValue); 1.429 + } 1.430 + return aValue; 1.431 + }, 1.432 + _counter: null, 1.433 + 1.434 + /** 1.435 + * Progress indication to display, from 0 to 100, or -1 if unknown. The 1.436 + * progress bar is hidden if the current progress is unknown and no status 1.437 + * text is set in the "counter" property. 1.438 + */ 1.439 + set percentComplete(aValue) 1.440 + { 1.441 + if (!this._operational) { 1.442 + return this._percentComplete; 1.443 + } 1.444 + 1.445 + if (this._percentComplete !== aValue) { 1.446 + this._percentComplete = aValue; 1.447 + if (this._percentComplete >= 0) 1.448 + this.indicator.setAttribute("progress", "true"); 1.449 + else 1.450 + this.indicator.removeAttribute("progress"); 1.451 + // We have to set the attribute instead of using the property because the 1.452 + // XBL binding isn't applied if the element is invisible for any reason. 1.453 + this._indicatorProgress.setAttribute("value", Math.max(aValue, 0)); 1.454 + } 1.455 + return aValue; 1.456 + }, 1.457 + _percentComplete: null, 1.458 + 1.459 + /** 1.460 + * Indicates whether the progress won't advance because of a paused state. 1.461 + * Setting this property forces a paused progress bar to be displayed, even if 1.462 + * the current progress information is unavailable. 1.463 + */ 1.464 + set paused(aValue) 1.465 + { 1.466 + if (!this._operational) { 1.467 + return this._paused; 1.468 + } 1.469 + 1.470 + if (this._paused != aValue) { 1.471 + this._paused = aValue; 1.472 + if (this._paused) { 1.473 + this.indicator.setAttribute("paused", "true") 1.474 + } else { 1.475 + this.indicator.removeAttribute("paused"); 1.476 + } 1.477 + } 1.478 + return aValue; 1.479 + }, 1.480 + _paused: false, 1.481 + 1.482 + /** 1.483 + * Set when the indicator should draw user attention to itself. 1.484 + */ 1.485 + set attention(aValue) 1.486 + { 1.487 + if (!this._operational) { 1.488 + return this._attention; 1.489 + } 1.490 + 1.491 + if (this._attention != aValue) { 1.492 + this._attention = aValue; 1.493 + if (aValue) { 1.494 + this.indicator.setAttribute("attention", "true"); 1.495 + } else { 1.496 + this.indicator.removeAttribute("attention"); 1.497 + } 1.498 + } 1.499 + return aValue; 1.500 + }, 1.501 + _attention: false, 1.502 + 1.503 + ////////////////////////////////////////////////////////////////////////////// 1.504 + //// User interface event functions 1.505 + 1.506 + onWindowUnload: function DIV_onWindowUnload() 1.507 + { 1.508 + // This function is registered as an event listener, we can't use "this". 1.509 + DownloadsIndicatorView.ensureTerminated(); 1.510 + }, 1.511 + 1.512 + onCommand: function DIV_onCommand(aEvent) 1.513 + { 1.514 + // If the downloads button is in the menu panel, open the Library 1.515 + let widgetGroup = CustomizableUI.getWidget("downloads-button"); 1.516 + if (widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) { 1.517 + DownloadsPanel.showDownloadsHistory(); 1.518 + } else { 1.519 + DownloadsPanel.showPanel(); 1.520 + } 1.521 + 1.522 + aEvent.stopPropagation(); 1.523 + }, 1.524 + 1.525 + onDragOver: function DIV_onDragOver(aEvent) 1.526 + { 1.527 + browserDragAndDrop.dragOver(aEvent); 1.528 + }, 1.529 + 1.530 + onDrop: function DIV_onDrop(aEvent) 1.531 + { 1.532 + let dt = aEvent.dataTransfer; 1.533 + // If dragged item is from our source, do not try to 1.534 + // redownload already downloaded file. 1.535 + if (dt.mozGetDataAt("application/x-moz-file", 0)) 1.536 + return; 1.537 + 1.538 + let name = {}; 1.539 + let url = browserDragAndDrop.drop(aEvent, name); 1.540 + if (url) { 1.541 + if (url.startsWith("about:")) { 1.542 + return; 1.543 + } 1.544 + 1.545 + let sourceDoc = dt.mozSourceNode ? dt.mozSourceNode.ownerDocument : document; 1.546 + saveURL(url, name.value, null, true, true, null, sourceDoc); 1.547 + aEvent.preventDefault(); 1.548 + } 1.549 + }, 1.550 + 1.551 + _indicator: null, 1.552 + __indicatorCounter: null, 1.553 + __indicatorProgress: null, 1.554 + 1.555 + /** 1.556 + * Returns a reference to the main indicator element, or null if the element 1.557 + * is not present in the browser window yet. 1.558 + */ 1.559 + get indicator() 1.560 + { 1.561 + if (this._indicator) { 1.562 + return this._indicator; 1.563 + } 1.564 + 1.565 + let indicator = document.getElementById("downloads-button"); 1.566 + if (!indicator || indicator.getAttribute("indicator") != "true") { 1.567 + return null; 1.568 + } 1.569 + 1.570 + return this._indicator = indicator; 1.571 + }, 1.572 + 1.573 + get indicatorAnchor() 1.574 + { 1.575 + let widget = CustomizableUI.getWidget("downloads-button") 1.576 + .forWindow(window); 1.577 + if (widget.overflowed) { 1.578 + return widget.anchor; 1.579 + } 1.580 + return document.getElementById("downloads-indicator-anchor"); 1.581 + }, 1.582 + 1.583 + get _indicatorCounter() 1.584 + { 1.585 + return this.__indicatorCounter || 1.586 + (this.__indicatorCounter = document.getElementById("downloads-indicator-counter")); 1.587 + }, 1.588 + 1.589 + get _indicatorProgress() 1.590 + { 1.591 + return this.__indicatorProgress || 1.592 + (this.__indicatorProgress = document.getElementById("downloads-indicator-progress")); 1.593 + }, 1.594 + 1.595 + get notifier() 1.596 + { 1.597 + return this._notifier || 1.598 + (this._notifier = document.getElementById("downloads-notification-anchor")); 1.599 + }, 1.600 + 1.601 + _onCustomizedAway: function() { 1.602 + this._indicator = null; 1.603 + this.__indicatorCounter = null; 1.604 + this.__indicatorProgress = null; 1.605 + }, 1.606 + 1.607 + afterCustomize: function() { 1.608 + // If the cached indicator is not the one currently in the document, 1.609 + // invalidate our references 1.610 + if (this._indicator != document.getElementById("downloads-button")) { 1.611 + this._onCustomizedAway(); 1.612 + this._operational = false; 1.613 + this.ensureTerminated(); 1.614 + this.ensureInitialized(); 1.615 + } 1.616 + } 1.617 +}; 1.618 +