browser/components/downloads/content/indicator.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     2 /* vim: set ts=2 et sw=2 tw=80: */
     3 /* This Source Code Form is subject to the terms of the Mozilla Public
     4  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     5  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     7 /**
     8  * Handles the indicator that displays the progress of ongoing downloads, which
     9  * is also used as the anchor for the downloads panel.
    10  *
    11  * This module includes the following constructors and global objects:
    12  *
    13  * DownloadsButton
    14  * Main entry point for the downloads indicator.  Depending on how the toolbars
    15  * have been customized, this object determines if we should show a fully
    16  * functional indicator, a placeholder used during customization and in the
    17  * customization palette, or a neutral view as a temporary anchor for the
    18  * downloads panel.
    19  *
    20  * DownloadsIndicatorView
    21  * Builds and updates the actual downloads status widget, responding to changes
    22  * in the global status data, or provides a neutral view if the indicator is
    23  * removed from the toolbars and only used as a temporary anchor.  In addition,
    24  * handles the user interaction events raised by the widget.
    25  */
    27 "use strict";
    29 ////////////////////////////////////////////////////////////////////////////////
    30 //// DownloadsButton
    32 /**
    33  * Main entry point for the downloads indicator.  Depending on how the toolbars
    34  * have been customized, this object determines if we should show a fully
    35  * functional indicator, a placeholder used during customization and in the
    36  * customization palette, or a neutral view as a temporary anchor for the
    37  * downloads panel.
    38  */
    39 const DownloadsButton = {
    40   /**
    41    * Location of the indicator overlay.
    42    */
    43   get kIndicatorOverlay()
    44       "chrome://browser/content/downloads/indicatorOverlay.xul",
    46   /**
    47    * Returns a reference to the downloads button position placeholder, or null
    48    * if not available because it has been removed from the toolbars.
    49    */
    50   get _placeholder()
    51   {
    52     return document.getElementById("downloads-button");
    53   },
    55   /**
    56    * This function is called asynchronously just after window initialization.
    57    *
    58    * NOTE: This function should limit the input/output it performs to improve
    59    *       startup time.
    60    */
    61   initializeIndicator: function DB_initializeIndicator()
    62   {
    63     DownloadsIndicatorView.ensureInitialized();
    64   },
    66   /**
    67    * Indicates whether toolbar customization is in progress.
    68    */
    69   _customizing: false,
    71   /**
    72    * This function is called when toolbar customization starts.
    73    *
    74    * During customization, we never show the actual download progress indication
    75    * or the event notifications, but we show a neutral placeholder.  The neutral
    76    * placeholder is an ordinary button defined in the browser window that can be
    77    * moved freely between the toolbars and the customization palette.
    78    */
    79   customizeStart: function DB_customizeStart()
    80   {
    81     // Prevent the indicator from being displayed as a temporary anchor
    82     // during customization, even if requested using the getAnchor method.
    83     this._customizing = true;
    84     this._anchorRequested = false;
    85   },
    87   /**
    88    * This function is called when toolbar customization ends.
    89    */
    90   customizeDone: function DB_customizeDone()
    91   {
    92     this._customizing = false;
    93     DownloadsIndicatorView.afterCustomize();
    94   },
    96   /**
    97    * Determines the position where the indicator should appear, and moves its
    98    * associated element to the new position.
    99    *
   100    * @return Anchor element, or null if the indicator is not visible.
   101    */
   102   _getAnchorInternal: function DB_getAnchorInternal()
   103   {
   104     let indicator = DownloadsIndicatorView.indicator;
   105     if (!indicator) {
   106       // Exit now if the indicator overlay isn't loaded yet, or if the button
   107       // is not in the document.
   108       return null;
   109     }
   111     indicator.open = this._anchorRequested;
   113     let widget = CustomizableUI.getWidget("downloads-button")
   114                                .forWindow(window);
   115      // Determine if the indicator is located on an invisible toolbar.
   116      if (!isElementVisible(indicator.parentNode) && !widget.overflowed) {
   117        return null;
   118      }
   120     return DownloadsIndicatorView.indicatorAnchor;
   121   },
   123   /**
   124    * Checks whether the indicator is, or will soon be visible in the browser
   125    * window.
   126    *
   127    * @param aCallback
   128    *        Called once the indicator overlay has loaded. Gets a boolean
   129    *        argument representing the indicator visibility.
   130    */
   131   checkIsVisible: function DB_checkIsVisible(aCallback)
   132   {
   133     function DB_CEV_callback() {
   134       if (!this._placeholder) {
   135         aCallback(false);
   136       } else {
   137         let element = DownloadsIndicatorView.indicator || this._placeholder;
   138         aCallback(isElementVisible(element.parentNode));
   139       }
   140     }
   141     DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay,
   142                                                DB_CEV_callback.bind(this));
   143   },
   145   /**
   146    * Indicates whether we should try and show the indicator temporarily as an
   147    * anchor for the panel, even if the indicator would be hidden by default.
   148    */
   149   _anchorRequested: false,
   151   /**
   152    * Ensures that there is an anchor available for the panel.
   153    *
   154    * @param aCallback
   155    *        Called when the anchor is available, passing the element where the
   156    *        panel should be anchored, or null if an anchor is not available (for
   157    *        example because both the tab bar and the navigation bar are hidden).
   158    */
   159   getAnchor: function DB_getAnchor(aCallback)
   160   {
   161     // Do not allow anchoring the panel to the element while customizing.
   162     if (this._customizing) {
   163       aCallback(null);
   164       return;
   165     }
   167     function DB_GA_callback() {
   168       this._anchorRequested = true;
   169       aCallback(this._getAnchorInternal());
   170     }
   172     DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay,
   173                                                DB_GA_callback.bind(this));
   174   },
   176   /**
   177    * Allows the temporary anchor to be hidden.
   178    */
   179   releaseAnchor: function DB_releaseAnchor()
   180   {
   181     this._anchorRequested = false;
   182     this._getAnchorInternal();
   183   },
   185   get _tabsToolbar()
   186   {
   187     delete this._tabsToolbar;
   188     return this._tabsToolbar = document.getElementById("TabsToolbar");
   189   },
   191   get _navBar()
   192   {
   193     delete this._navBar;
   194     return this._navBar = document.getElementById("nav-bar");
   195   }
   196 };
   198 ////////////////////////////////////////////////////////////////////////////////
   199 //// DownloadsIndicatorView
   201 /**
   202  * Builds and updates the actual downloads status widget, responding to changes
   203  * in the global status data, or provides a neutral view if the indicator is
   204  * removed from the toolbars and only used as a temporary anchor.  In addition,
   205  * handles the user interaction events raised by the widget.
   206  */
   207 const DownloadsIndicatorView = {
   208   /**
   209    * True when the view is connected with the underlying downloads data.
   210    */
   211   _initialized: false,
   213   /**
   214    * True when the user interface elements required to display the indicator
   215    * have finished loading in the browser window, and can be referenced.
   216    */
   217   _operational: false,
   219   /**
   220    * Prepares the downloads indicator to be displayed.
   221    */
   222   ensureInitialized: function DIV_ensureInitialized()
   223   {
   224     if (this._initialized) {
   225       return;
   226     }
   227     this._initialized = true;
   229     window.addEventListener("unload", this.onWindowUnload, false);
   230     DownloadsCommon.getIndicatorData(window).addView(this);
   231   },
   233   /**
   234    * Frees the internal resources related to the indicator.
   235    */
   236   ensureTerminated: function DIV_ensureTerminated()
   237   {
   238     if (!this._initialized) {
   239       return;
   240     }
   241     this._initialized = false;
   243     window.removeEventListener("unload", this.onWindowUnload, false);
   244     DownloadsCommon.getIndicatorData(window).removeView(this);
   246     // Reset the view properties, so that a neutral indicator is displayed if we
   247     // are visible only temporarily as an anchor.
   248     this.counter = "";
   249     this.percentComplete = 0;
   250     this.paused = false;
   251     this.attention = false;
   252   },
   254   /**
   255    * Ensures that the user interface elements required to display the indicator
   256    * are loaded, then invokes the given callback.
   257    */
   258   _ensureOperational: function DIV_ensureOperational(aCallback)
   259   {
   260     if (this._operational) {
   261       if (aCallback) {
   262         aCallback();
   263       }
   264       return;
   265     }
   267     // If we don't have a _placeholder, there's no chance that the overlay
   268     // will load correctly: bail (and don't set _operational to true!)
   269     if (!DownloadsButton._placeholder) {
   270       return;
   271     }
   273     function DIV_EO_callback() {
   274       this._operational = true;
   276       // If the view is initialized, we need to update the elements now that
   277       // they are finally available in the document.
   278       if (this._initialized) {
   279         DownloadsCommon.getIndicatorData(window).refreshView(this);
   280       }
   282       if (aCallback) {
   283         aCallback();
   284       }
   285     }
   287     DownloadsOverlayLoader.ensureOverlayLoaded(
   288                                  DownloadsButton.kIndicatorOverlay,
   289                                  DIV_EO_callback.bind(this));
   290   },
   292   //////////////////////////////////////////////////////////////////////////////
   293   //// Direct control functions
   295   /**
   296    * Set while we are waiting for a notification to fade out.
   297    */
   298   _notificationTimeout: null,
   300   /**
   301    * Check if the panel containing aNode is open.
   302    * @param aNode
   303    *        the node whose panel we're interested in.
   304    */
   305   _isAncestorPanelOpen: function DIV_isAncestorPanelOpen(aNode)
   306   {
   307     while (aNode && aNode.localName != "panel") {
   308       aNode = aNode.parentNode;
   309     }
   310     return aNode && aNode.state == "open";
   311   },
   313   /**
   314    * If the status indicator is visible in its assigned position, shows for a
   315    * brief time a visual notification of a relevant event, like a new download.
   316    *
   317    * @param aType
   318    *        Set to "start" for new downloads, "finish" for completed downloads.
   319    */
   320   showEventNotification: function DIV_showEventNotification(aType)
   321   {
   322     if (!this._initialized) {
   323       return;
   324     }
   326     if (!DownloadsCommon.animateNotifications) {
   327       return;
   328     }
   330     // No need to show visual notification if the panel is visible.
   331     if (DownloadsPanel.isPanelShowing) {
   332       return;
   333     }
   335     let anchor = DownloadsButton._placeholder;
   336     let widgetGroup = CustomizableUI.getWidget("downloads-button");
   337     let widget = widgetGroup.forWindow(window);
   338     if (widget.overflowed || widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
   339       if (anchor && this._isAncestorPanelOpen(anchor)) {
   340         // If the containing panel is open, don't do anything, because the
   341         // notification would appear under the open panel. See
   342         // https://bugzilla.mozilla.org/show_bug.cgi?id=984023
   343         return;
   344       }
   346       // Otherwise, try to use the anchor of the panel:
   347       anchor = widget.anchor;
   348     }
   349     if (!anchor || !isElementVisible(anchor.parentNode)) {
   350       // Our container isn't visible, so can't show the animation:
   351       return;
   352     }
   354     if (this._notificationTimeout) {
   355       clearTimeout(this._notificationTimeout);
   356     }
   358     // The notification element is positioned to show in the same location as
   359     // the downloads button. It's not in the downloads button itself in order to
   360     // be able to anchor the notification elsewhere if required, and to ensure
   361     // the notification isn't clipped by overflow properties of the anchor's
   362     // container.
   363     let notifier = this.notifier;
   364     if (notifier.style.transform == '') {
   365       let anchorRect = anchor.getBoundingClientRect();
   366       let notifierRect = notifier.getBoundingClientRect();
   367       let topDiff = anchorRect.top - notifierRect.top;
   368       let leftDiff = anchorRect.left - notifierRect.left;
   369       let heightDiff = anchorRect.height - notifierRect.height;
   370       let widthDiff = anchorRect.width - notifierRect.width;
   371       let translateX = (leftDiff + .5 * widthDiff) + "px";
   372       let translateY = (topDiff + .5 * heightDiff) + "px";
   373       notifier.style.transform = "translate(" +  translateX + ", " + translateY + ")";
   374     }
   375     notifier.setAttribute("notification", aType);
   376     this._notificationTimeout = setTimeout(function () {
   377       notifier.removeAttribute("notification");
   378       notifier.style.transform = '';
   379     }, 1000);
   380   },
   382   //////////////////////////////////////////////////////////////////////////////
   383   //// Callback functions from DownloadsIndicatorData
   385   /**
   386    * Indicates whether the indicator should be shown because there are some
   387    * downloads to be displayed.
   388    */
   389   set hasDownloads(aValue)
   390   {
   391     if (this._hasDownloads != aValue || (!this._operational && aValue)) {
   392       this._hasDownloads = aValue;
   394       // If there is at least one download, ensure that the view elements are
   395       if (aValue) {
   396         this._ensureOperational();
   397       }
   398     }
   399     return aValue;
   400   },
   401   get hasDownloads()
   402   {
   403     return this._hasDownloads;
   404   },
   405   _hasDownloads: false,
   407   /**
   408    * Status text displayed in the indicator.  If this is set to an empty value,
   409    * then the small downloads icon is displayed instead of the text.
   410    */
   411   set counter(aValue)
   412   {
   413     if (!this._operational) {
   414       return this._counter;
   415     }
   417     if (this._counter !== aValue) {
   418       this._counter = aValue;
   419       if (this._counter)
   420         this.indicator.setAttribute("counter", "true");
   421       else
   422         this.indicator.removeAttribute("counter");
   423       // We have to set the attribute instead of using the property because the
   424       // XBL binding isn't applied if the element is invisible for any reason.
   425       this._indicatorCounter.setAttribute("value", aValue);
   426     }
   427     return aValue;
   428   },
   429   _counter: null,
   431   /**
   432    * Progress indication to display, from 0 to 100, or -1 if unknown.  The
   433    * progress bar is hidden if the current progress is unknown and no status
   434    * text is set in the "counter" property.
   435    */
   436   set percentComplete(aValue)
   437   {
   438     if (!this._operational) {
   439       return this._percentComplete;
   440     }
   442     if (this._percentComplete !== aValue) {
   443       this._percentComplete = aValue;
   444       if (this._percentComplete >= 0)
   445         this.indicator.setAttribute("progress", "true");
   446       else
   447         this.indicator.removeAttribute("progress");
   448       // We have to set the attribute instead of using the property because the
   449       // XBL binding isn't applied if the element is invisible for any reason.
   450       this._indicatorProgress.setAttribute("value", Math.max(aValue, 0));
   451     }
   452     return aValue;
   453   },
   454   _percentComplete: null,
   456   /**
   457    * Indicates whether the progress won't advance because of a paused state.
   458    * Setting this property forces a paused progress bar to be displayed, even if
   459    * the current progress information is unavailable.
   460    */
   461   set paused(aValue)
   462   {
   463     if (!this._operational) {
   464       return this._paused;
   465     }
   467     if (this._paused != aValue) {
   468       this._paused = aValue;
   469       if (this._paused) {
   470         this.indicator.setAttribute("paused", "true")
   471       } else {
   472         this.indicator.removeAttribute("paused");
   473       }
   474     }
   475     return aValue;
   476   },
   477   _paused: false,
   479   /**
   480    * Set when the indicator should draw user attention to itself.
   481    */
   482   set attention(aValue)
   483   {
   484     if (!this._operational) {
   485       return this._attention;
   486     }
   488     if (this._attention != aValue) {
   489       this._attention = aValue;
   490       if (aValue) {
   491         this.indicator.setAttribute("attention", "true");
   492       } else {
   493         this.indicator.removeAttribute("attention");
   494       }
   495     }
   496     return aValue;
   497   },
   498   _attention: false,
   500   //////////////////////////////////////////////////////////////////////////////
   501   //// User interface event functions
   503   onWindowUnload: function DIV_onWindowUnload()
   504   {
   505     // This function is registered as an event listener, we can't use "this".
   506     DownloadsIndicatorView.ensureTerminated();
   507   },
   509   onCommand: function DIV_onCommand(aEvent)
   510   {
   511     // If the downloads button is in the menu panel, open the Library
   512     let widgetGroup = CustomizableUI.getWidget("downloads-button");
   513     if (widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
   514       DownloadsPanel.showDownloadsHistory();
   515     } else {
   516       DownloadsPanel.showPanel();
   517     }
   519     aEvent.stopPropagation();
   520   },
   522   onDragOver: function DIV_onDragOver(aEvent)
   523   {
   524     browserDragAndDrop.dragOver(aEvent);
   525   },
   527   onDrop: function DIV_onDrop(aEvent)
   528   {
   529     let dt = aEvent.dataTransfer;
   530     // If dragged item is from our source, do not try to
   531     // redownload already downloaded file.
   532     if (dt.mozGetDataAt("application/x-moz-file", 0))
   533       return;
   535     let name = {};
   536     let url = browserDragAndDrop.drop(aEvent, name);
   537     if (url) {
   538       if (url.startsWith("about:")) {
   539         return;
   540       }
   542       let sourceDoc = dt.mozSourceNode ? dt.mozSourceNode.ownerDocument : document;
   543       saveURL(url, name.value, null, true, true, null, sourceDoc);
   544       aEvent.preventDefault();
   545     }
   546   },
   548   _indicator: null,
   549   __indicatorCounter: null,
   550   __indicatorProgress: null,
   552   /**
   553    * Returns a reference to the main indicator element, or null if the element
   554    * is not present in the browser window yet.
   555    */
   556   get indicator()
   557   {
   558     if (this._indicator) {
   559       return this._indicator;
   560     }
   562     let indicator = document.getElementById("downloads-button");
   563     if (!indicator || indicator.getAttribute("indicator") != "true") {
   564       return null;
   565     }
   567     return this._indicator = indicator;
   568   },
   570   get indicatorAnchor()
   571   {
   572     let widget = CustomizableUI.getWidget("downloads-button")
   573                                .forWindow(window);
   574     if (widget.overflowed) {
   575       return widget.anchor;
   576     }
   577     return document.getElementById("downloads-indicator-anchor");
   578   },
   580   get _indicatorCounter()
   581   {
   582     return this.__indicatorCounter ||
   583       (this.__indicatorCounter = document.getElementById("downloads-indicator-counter"));
   584   },
   586   get _indicatorProgress()
   587   {
   588     return this.__indicatorProgress ||
   589       (this.__indicatorProgress = document.getElementById("downloads-indicator-progress"));
   590   },
   592   get notifier()
   593   {
   594     return this._notifier ||
   595       (this._notifier = document.getElementById("downloads-notification-anchor"));
   596   },
   598   _onCustomizedAway: function() {
   599     this._indicator = null;
   600     this.__indicatorCounter = null;
   601     this.__indicatorProgress = null;
   602   },
   604   afterCustomize: function() {
   605     // If the cached indicator is not the one currently in the document,
   606     // invalidate our references
   607     if (this._indicator != document.getElementById("downloads-button")) {
   608       this._onCustomizedAway();
   609       this._operational = false;
   610       this.ensureTerminated();
   611       this.ensureInitialized();
   612     }
   613   }
   614 };

mercurial