toolkit/modules/PopupNotifications.jsm

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 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 this.EXPORTED_SYMBOLS = ["PopupNotifications"];
     7 var Cc = Components.classes, Ci = Components.interfaces, Cu = Components.utils;
     9 Cu.import("resource://gre/modules/Services.jsm");
    11 const NOTIFICATION_EVENT_DISMISSED = "dismissed";
    12 const NOTIFICATION_EVENT_REMOVED = "removed";
    13 const NOTIFICATION_EVENT_SHOWING = "showing";
    14 const NOTIFICATION_EVENT_SHOWN = "shown";
    15 const NOTIFICATION_EVENT_SWAPPING = "swapping";
    17 const ICON_SELECTOR = ".notification-anchor-icon";
    18 const ICON_ATTRIBUTE_SHOWING = "showing";
    20 const PREF_SECURITY_DELAY = "security.notification_enable_delay";
    22 let popupNotificationsMap = new WeakMap();
    23 let gNotificationParents = new WeakMap;
    25 function getAnchorFromBrowser(aBrowser) {
    26   let anchor = aBrowser.getAttribute("popupnotificationanchor") ||
    27                 aBrowser.popupnotificationanchor;
    28   if (anchor) {
    29     if (anchor instanceof Ci.nsIDOMXULElement) {
    30       return anchor;
    31     }
    32     return aBrowser.ownerDocument.getElementById(anchor);
    33   }
    34   return null;
    35 }
    37 /**
    38  * Notification object describes a single popup notification.
    39  *
    40  * @see PopupNotifications.show()
    41  */
    42 function Notification(id, message, anchorID, mainAction, secondaryActions,
    43                       browser, owner, options) {
    44   this.id = id;
    45   this.message = message;
    46   this.anchorID = anchorID;
    47   this.mainAction = mainAction;
    48   this.secondaryActions = secondaryActions || [];
    49   this.browser = browser;
    50   this.owner = owner;
    51   this.options = options || {};
    52 }
    54 Notification.prototype = {
    56   id: null,
    57   message: null,
    58   anchorID: null,
    59   mainAction: null,
    60   secondaryActions: null,
    61   browser: null,
    62   owner: null,
    63   options: null,
    64   timeShown: null,
    66   /**
    67    * Removes the notification and updates the popup accordingly if needed.
    68    */
    69   remove: function Notification_remove() {
    70     this.owner.remove(this);
    71   },
    73   get anchorElement() {
    74     let iconBox = this.owner.iconBox;
    76     let anchorElement = getAnchorFromBrowser(this.browser);
    78     if (!iconBox)
    79       return anchorElement;
    81     if (!anchorElement && this.anchorID)
    82       anchorElement = iconBox.querySelector("#"+this.anchorID);
    84     // Use a default anchor icon if it's available
    85     if (!anchorElement)
    86       anchorElement = iconBox.querySelector("#default-notification-icon") ||
    87                       iconBox;
    89     return anchorElement;
    90   },
    92   reshow: function() {
    93     this.owner._reshowNotifications(this.anchorElement, this.browser);
    94   }
    95 };
    97 /**
    98  * The PopupNotifications object manages popup notifications for a given browser
    99  * window.
   100  * @param tabbrowser
   101  *        window's <xul:tabbrowser/>. Used to observe tab switching events and
   102  *        for determining the active browser element.
   103  * @param panel
   104  *        The <xul:panel/> element to use for notifications. The panel is
   105  *        populated with <popupnotification> children and displayed it as
   106  *        needed.
   107  * @param iconBox
   108  *        Reference to a container element that should be hidden or
   109  *        unhidden when notifications are hidden or shown. It should be the
   110  *        parent of anchor elements whose IDs are passed to show().
   111  *        It is used as a fallback popup anchor if notifications specify
   112  *        invalid or non-existent anchor IDs.
   113  */
   114 this.PopupNotifications = function PopupNotifications(tabbrowser, panel, iconBox) {
   115   if (!(tabbrowser instanceof Ci.nsIDOMXULElement))
   116     throw "Invalid tabbrowser";
   117   if (iconBox && !(iconBox instanceof Ci.nsIDOMXULElement))
   118     throw "Invalid iconBox";
   119   if (!(panel instanceof Ci.nsIDOMXULElement))
   120     throw "Invalid panel";
   122   this.window = tabbrowser.ownerDocument.defaultView;
   123   this.panel = panel;
   124   this.tabbrowser = tabbrowser;
   125   this.iconBox = iconBox;
   126   this.buttonDelay = Services.prefs.getIntPref(PREF_SECURITY_DELAY);
   128   this.panel.addEventListener("popuphidden", this, true);
   130   this.window.addEventListener("activate", this, true);
   131   if (this.tabbrowser.tabContainer)
   132     this.tabbrowser.tabContainer.addEventListener("TabSelect", this, true);
   133 }
   135 PopupNotifications.prototype = {
   137   window: null,
   138   panel: null,
   139   tabbrowser: null,
   141   _iconBox: null,
   142   set iconBox(iconBox) {
   143     // Remove the listeners on the old iconBox, if needed
   144     if (this._iconBox) {
   145       this._iconBox.removeEventListener("click", this, false);
   146       this._iconBox.removeEventListener("keypress", this, false);
   147     }
   148     this._iconBox = iconBox;
   149     if (iconBox) {
   150       iconBox.addEventListener("click", this, false);
   151       iconBox.addEventListener("keypress", this, false);
   152     }
   153   },
   154   get iconBox() {
   155     return this._iconBox;
   156   },
   158   /**
   159    * Enable or disable the opening/closing transition.
   160    * @param state
   161    *        Boolean state
   162    */
   163   set transitionsEnabled(state) {
   164     if (state) {
   165       this.panel.removeAttribute("animate");
   166     }
   167     else {
   168       this.panel.setAttribute("animate", "false");
   169     }
   170   },
   172   /**
   173    * Retrieve a Notification object associated with the browser/ID pair.
   174    * @param id
   175    *        The Notification ID to search for.
   176    * @param browser
   177    *        The browser whose notifications should be searched. If null, the
   178    *        currently selected browser's notifications will be searched.
   179    *
   180    * @returns the corresponding Notification object, or null if no such
   181    *          notification exists.
   182    */
   183   getNotification: function PopupNotifications_getNotification(id, browser) {
   184     let n = null;
   185     let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser);
   186     notifications.some(function(x) x.id == id && (n = x));
   187     return n;
   188   },
   190   /**
   191    * Adds a new popup notification.
   192    * @param browser
   193    *        The <xul:browser> element associated with the notification. Must not
   194    *        be null.
   195    * @param id
   196    *        A unique ID that identifies the type of notification (e.g.
   197    *        "geolocation"). Only one notification with a given ID can be visible
   198    *        at a time. If a notification already exists with the given ID, it
   199    *        will be replaced.
   200    * @param message
   201    *        The text to be displayed in the notification.
   202    * @param anchorID
   203    *        The ID of the element that should be used as this notification
   204    *        popup's anchor. May be null, in which case the notification will be
   205    *        anchored to the iconBox.
   206    * @param mainAction
   207    *        A JavaScript object literal describing the notification button's
   208    *        action. If present, it must have the following properties:
   209    *          - label (string): the button's label.
   210    *          - accessKey (string): the button's accessKey.
   211    *          - callback (function): a callback to be invoked when the button is
   212    *            pressed.
   213    *          - [optional] dismiss (boolean): If this is true, the notification
   214    *            will be dismissed instead of removed after running the callback.
   215    *        If null, the notification will not have a button, and
   216    *        secondaryActions will be ignored.
   217    * @param secondaryActions
   218    *        An optional JavaScript array describing the notification's alternate
   219    *        actions. The array should contain objects with the same properties
   220    *        as mainAction. These are used to populate the notification button's
   221    *        dropdown menu.
   222    * @param options
   223    *        An options JavaScript object holding additional properties for the
   224    *        notification. The following properties are currently supported:
   225    *        persistence: An integer. The notification will not automatically
   226    *                     dismiss for this many page loads.
   227    *        timeout:     A time in milliseconds. The notification will not
   228    *                     automatically dismiss before this time.
   229    *        persistWhileVisible:
   230    *                     A boolean. If true, a visible notification will always
   231    *                     persist across location changes.
   232    *        dismissed:   Whether the notification should be added as a dismissed
   233    *                     notification. Dismissed notifications can be activated
   234    *                     by clicking on their anchorElement.
   235    *        eventCallback:
   236    *                     Callback to be invoked when the notification changes
   237    *                     state. The callback's first argument is a string
   238    *                     identifying the state change:
   239    *                     "dismissed": notification has been dismissed by the
   240    *                                  user (e.g. by clicking away or switching
   241    *                                  tabs)
   242    *                     "removed": notification has been removed (due to
   243    *                                location change or user action)
   244    *                     "showing": notification is about to be shown
   245    *                                (this can be fired multiple times as
   246    *                                 notifications are dismissed and re-shown)
   247    *                                If the callback returns true, the notification
   248    *                                will be dismissed.
   249    *                     "shown": notification has been shown (this can be fired
   250    *                              multiple times as notifications are dismissed
   251    *                              and re-shown)
   252    *                     "swapping": the docshell of the browser that created
   253    *                                 the notification is about to be swapped to
   254    *                                 another browser. A second parameter contains
   255    *                                 the browser that is receiving the docshell,
   256    *                                 so that the event callback can transfer stuff
   257    *                                 specific to this notification.
   258    *                                 If the callback returns true, the notification
   259    *                                 will be moved to the new browser.
   260    *                                 If the callback isn't implemented, returns false,
   261    *                                 or doesn't return any value, the notification
   262    *                                 will be removed.
   263    *        neverShow:   Indicate that no popup should be shown for this
   264    *                     notification. Useful for just showing the anchor icon.
   265    *        removeOnDismissal:
   266    *                     Notifications with this parameter set to true will be
   267    *                     removed when they would have otherwise been dismissed
   268    *                     (i.e. any time the popup is closed due to user
   269    *                     interaction).
   270    *        hideNotNow:  If true, indicates that the 'Not Now' menuitem should
   271    *                     not be shown. If 'Not Now' is hidden, it needs to be
   272    *                     replaced by another 'do nothing' item, so providing at
   273    *                     least one secondary action is required; and one of the
   274    *                     actions needs to have the 'dismiss' property set to true.
   275    *        popupIconURL:
   276    *                     A string. URL of the image to be displayed in the popup.
   277    *                     Normally specified in CSS using list-style-image and the
   278    *                     .popup-notification-icon[popupid=...] selector.
   279    *        learnMoreURL:
   280    *                     A string URL. Setting this property will make the
   281    *                     prompt display a "Learn More" link that, when clicked,
   282    *                     opens the URL in a new tab.
   283    * @returns the Notification object corresponding to the added notification.
   284    */
   285   show: function PopupNotifications_show(browser, id, message, anchorID,
   286                                          mainAction, secondaryActions, options) {
   287     function isInvalidAction(a) {
   288       return !a || !(typeof(a.callback) == "function") || !a.label || !a.accessKey;
   289     }
   291     if (!browser)
   292       throw "PopupNotifications_show: invalid browser";
   293     if (!id)
   294       throw "PopupNotifications_show: invalid ID";
   295     if (mainAction && isInvalidAction(mainAction))
   296       throw "PopupNotifications_show: invalid mainAction";
   297     if (secondaryActions && secondaryActions.some(isInvalidAction))
   298       throw "PopupNotifications_show: invalid secondaryActions";
   299     if (options && options.hideNotNow &&
   300         (!secondaryActions || !secondaryActions.length ||
   301          !secondaryActions.concat(mainAction).some(action => action.dismiss)))
   302       throw "PopupNotifications_show: 'Not Now' item hidden without replacement";
   304     let notification = new Notification(id, message, anchorID, mainAction,
   305                                         secondaryActions, browser, this, options);
   307     if (options && options.dismissed)
   308       notification.dismissed = true;
   310     let existingNotification = this.getNotification(id, browser);
   311     if (existingNotification)
   312       this._remove(existingNotification);
   314     let notifications = this._getNotificationsForBrowser(browser);
   315     notifications.push(notification);
   317     let isActive = this._isActiveBrowser(browser);
   318     let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
   319     if (isActive && fm.activeWindow == this.window) {
   320       // show panel now
   321       this._update(notifications, notification.anchorElement, true);
   322     } else {
   323       // Otherwise, update() will display the notification the next time the
   324       // relevant tab/window is selected.
   326       // If the tab is selected but the window is in the background, let the OS
   327       // tell the user that there's a notification waiting in that window.
   328       // At some point we might want to do something about background tabs here
   329       // too. When the user switches to this window, we'll show the panel if
   330       // this browser is a tab (thus showing the anchor icon). For
   331       // non-tabbrowser browsers, we need to make the icon visible now or the
   332       // user will not be able to open the panel.
   333       if (!notification.dismissed && isActive) {
   334         this.window.getAttention();
   335         if (notification.anchorElement.parentNode != this.iconBox) {
   336           this._updateAnchorIcon(notifications, notification.anchorElement);
   337         }
   338       }
   340       // Notify observers that we're not showing the popup (useful for testing)
   341       this._notify("backgroundShow");
   342     }
   344     return notification;
   345   },
   347   /**
   348    * Returns true if the notification popup is currently being displayed.
   349    */
   350   get isPanelOpen() {
   351     let panelState = this.panel.state;
   353     return panelState == "showing" || panelState == "open";
   354   },
   356   /**
   357    * Called by the consumer to indicate that a browser's location has changed,
   358    * so that we can update the active notifications accordingly.
   359    */
   360   locationChange: function PopupNotifications_locationChange(aBrowser) {
   361     if (!aBrowser)
   362       throw "PopupNotifications_locationChange: invalid browser";
   364     let notifications = this._getNotificationsForBrowser(aBrowser);
   366     notifications = notifications.filter(function (notification) {
   367       // The persistWhileVisible option allows an open notification to persist
   368       // across location changes
   369       if (notification.options.persistWhileVisible &&
   370           this.isPanelOpen) {
   371         if ("persistence" in notification.options &&
   372           notification.options.persistence)
   373           notification.options.persistence--;
   374         return true;
   375       }
   377       // The persistence option allows a notification to persist across multiple
   378       // page loads
   379       if ("persistence" in notification.options &&
   380           notification.options.persistence) {
   381         notification.options.persistence--;
   382         return true;
   383       }
   385       // The timeout option allows a notification to persist until a certain time
   386       if ("timeout" in notification.options &&
   387           Date.now() <= notification.options.timeout) {
   388         return true;
   389       }
   391       this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED);
   392       return false;
   393     }, this);
   395     this._setNotificationsForBrowser(aBrowser, notifications);
   397     if (this._isActiveBrowser(aBrowser)) {
   398       // get the anchor element if the browser has defined one so it will
   399       // _update will handle both the tabs iconBox and non-tab permission
   400       // anchors.
   401       let anchorElement = notifications.length > 0 ? notifications[0].anchorElement : null;
   402       if (!anchorElement)
   403         anchorElement = getAnchorFromBrowser(aBrowser);
   404       this._update(notifications, anchorElement);
   405     }
   406   },
   408   /**
   409    * Removes a Notification.
   410    * @param notification
   411    *        The Notification object to remove.
   412    */
   413   remove: function PopupNotifications_remove(notification) {
   414     this._remove(notification);
   416     if (this._isActiveBrowser(notification.browser)) {
   417       let notifications = this._getNotificationsForBrowser(notification.browser);
   418       this._update(notifications, notification.anchorElement);
   419     }
   420   },
   422   handleEvent: function (aEvent) {
   423     switch (aEvent.type) {
   424       case "popuphidden":
   425         this._onPopupHidden(aEvent);
   426         break;
   427       case "activate":
   428       case "TabSelect":
   429         let self = this;
   430         // setTimeout(..., 0) needed, otherwise openPopup from "activate" event
   431         // handler results in the popup being hidden again for some reason...
   432         this.window.setTimeout(function () {
   433           self._update();
   434         }, 0);
   435         break;
   436       case "click":
   437       case "keypress":
   438         this._onIconBoxCommand(aEvent);
   439         break;
   440     }
   441   },
   443 ////////////////////////////////////////////////////////////////////////////////
   444 // Utility methods
   445 ////////////////////////////////////////////////////////////////////////////////
   447   _ignoreDismissal: null,
   448   _currentAnchorElement: null,
   450   /**
   451    * Gets notifications for the currently selected browser.
   452    */
   453   get _currentNotifications() {
   454     return this.tabbrowser.selectedBrowser ? this._getNotificationsForBrowser(this.tabbrowser.selectedBrowser) : [];
   455   },
   457   _remove: function PopupNotifications_removeHelper(notification) {
   458     // This notification may already be removed, in which case let's just fail
   459     // silently.
   460     let notifications = this._getNotificationsForBrowser(notification.browser);
   461     if (!notifications)
   462       return;
   464     var index = notifications.indexOf(notification);
   465     if (index == -1)
   466       return;
   468     if (this._isActiveBrowser(notification.browser))
   469       notification.anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
   471     // remove the notification
   472     notifications.splice(index, 1);
   473     this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED);
   474   },
   476   /**
   477    * Dismisses the notification without removing it.
   478    */
   479   _dismiss: function PopupNotifications_dismiss() {
   480     let browser = this.panel.firstChild &&
   481                   this.panel.firstChild.notification.browser;
   482     this.panel.hidePopup();
   483     if (browser)
   484       browser.focus();
   485   },
   487   /**
   488    * Hides the notification popup.
   489    */
   490   _hidePanel: function PopupNotifications_hide() {
   491     this._ignoreDismissal = true;
   492     this.panel.hidePopup();
   493     this._ignoreDismissal = false;
   494   },
   496   /**
   497    * Removes all notifications from the notification popup.
   498    */
   499   _clearPanel: function () {
   500     let popupnotification;
   501     while ((popupnotification = this.panel.lastChild)) {
   502       this.panel.removeChild(popupnotification);
   504       // If this notification was provided by the chrome document rather than
   505       // created ad hoc, move it back to where we got it from.
   506       let originalParent = gNotificationParents.get(popupnotification);
   507       if (originalParent) {
   508         popupnotification.notification = null;
   510         // Remove nodes dynamically added to the notification's menu button
   511         // in _refreshPanel. Keep popupnotificationcontent nodes; they are
   512         // provided by the chrome document.
   513         let contentNode = popupnotification.lastChild;
   514         while (contentNode) {
   515           let previousSibling = contentNode.previousSibling;
   516           if (contentNode.nodeName != "popupnotificationcontent")
   517             popupnotification.removeChild(contentNode);
   518           contentNode = previousSibling;
   519         }
   521         // Re-hide the notification such that it isn't rendered in the chrome
   522         // document. _refreshPanel will unhide it again when needed.
   523         popupnotification.hidden = true;
   525         originalParent.appendChild(popupnotification);
   526       }
   527     }
   528   },
   530   _refreshPanel: function PopupNotifications_refreshPanel(notificationsToShow) {
   531     this._clearPanel();
   533     const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
   535     notificationsToShow.forEach(function (n) {
   536       let doc = this.window.document;
   538       // Append "-notification" to the ID to try to avoid ID conflicts with other stuff
   539       // in the document.
   540       let popupnotificationID = n.id + "-notification";
   542       // If the chrome document provides a popupnotification with this id, use
   543       // that. Otherwise create it ad-hoc.
   544       let popupnotification = doc.getElementById(popupnotificationID);
   545       if (popupnotification)
   546         gNotificationParents.set(popupnotification, popupnotification.parentNode);
   547       else
   548         popupnotification = doc.createElementNS(XUL_NS, "popupnotification");
   550       popupnotification.setAttribute("label", n.message);
   551       popupnotification.setAttribute("id", popupnotificationID);
   552       popupnotification.setAttribute("popupid", n.id);
   553       popupnotification.setAttribute("closebuttoncommand", "PopupNotifications._dismiss();");
   554       if (n.mainAction) {
   555         popupnotification.setAttribute("buttonlabel", n.mainAction.label);
   556         popupnotification.setAttribute("buttonaccesskey", n.mainAction.accessKey);
   557         popupnotification.setAttribute("buttoncommand", "PopupNotifications._onButtonCommand(event);");
   558         popupnotification.setAttribute("menucommand", "PopupNotifications._onMenuCommand(event);");
   559         popupnotification.setAttribute("closeitemcommand", "PopupNotifications._dismiss();event.stopPropagation();");
   560       } else {
   561         popupnotification.removeAttribute("buttonlabel");
   562         popupnotification.removeAttribute("buttonaccesskey");
   563         popupnotification.removeAttribute("buttoncommand");
   564         popupnotification.removeAttribute("menucommand");
   565         popupnotification.removeAttribute("closeitemcommand");
   566       }
   568       if (n.options.popupIconURL)
   569         popupnotification.setAttribute("icon", n.options.popupIconURL);
   570       if (n.options.learnMoreURL)
   571         popupnotification.setAttribute("learnmoreurl", n.options.learnMoreURL);
   572       else
   573         popupnotification.removeAttribute("learnmoreurl");
   575       popupnotification.notification = n;
   577       if (n.secondaryActions) {
   578         n.secondaryActions.forEach(function (a) {
   579           let item = doc.createElementNS(XUL_NS, "menuitem");
   580           item.setAttribute("label", a.label);
   581           item.setAttribute("accesskey", a.accessKey);
   582           item.notification = n;
   583           item.action = a;
   585           popupnotification.appendChild(item);
   586         }, this);
   588         if (n.options.hideNotNow) {
   589           popupnotification.setAttribute("hidenotnow", "true");
   590         }
   591         else if (n.secondaryActions.length) {
   592           let closeItemSeparator = doc.createElementNS(XUL_NS, "menuseparator");
   593           popupnotification.appendChild(closeItemSeparator);
   594         }
   595       }
   597       this.panel.appendChild(popupnotification);
   599       // The popupnotification may be hidden if we got it from the chrome
   600       // document rather than creating it ad hoc.
   601       popupnotification.hidden = false;
   602     }, this);
   603   },
   605   _showPanel: function PopupNotifications_showPanel(notificationsToShow, anchorElement) {
   606     this.panel.hidden = false;
   608     notificationsToShow = notificationsToShow.filter(n => {
   609       let dismiss = this._fireCallback(n, NOTIFICATION_EVENT_SHOWING);
   610       if (dismiss)
   611         n.dismissed = true;
   612       return !dismiss;
   613     });
   614     if (!notificationsToShow.length)
   615       return;
   617     this._refreshPanel(notificationsToShow);
   619     if (this.isPanelOpen && this._currentAnchorElement == anchorElement)
   620       return;
   622     // If the panel is already open but we're changing anchors, we need to hide
   623     // it first.  Otherwise it can appear in the wrong spot.  (_hidePanel is
   624     // safe to call even if the panel is already hidden.)
   625     this._hidePanel();
   627     // If the anchor element is hidden or null, use the tab as the anchor. We
   628     // only ever show notifications for the current browser, so we can just use
   629     // the current tab.
   630     let selectedTab = this.tabbrowser.selectedTab;
   631     if (anchorElement) {
   632       let bo = anchorElement.boxObject;
   633       if (bo.height == 0 && bo.width == 0)
   634         anchorElement = selectedTab; // hidden
   635     } else {
   636       anchorElement = selectedTab; // null
   637     }
   639     this._currentAnchorElement = anchorElement;
   641     // On OS X and Linux we need a different panel arrow color for
   642     // click-to-play plugins, so copy the popupid and use css.
   643     this.panel.setAttribute("popupid", this.panel.firstChild.getAttribute("popupid"));
   644     notificationsToShow.forEach(function (n) {
   645       // Remember the time the notification was shown for the security delay.
   646       n.timeShown = this.window.performance.now();
   647     }, this);
   648     this.panel.openPopup(anchorElement, "bottomcenter topleft");
   649     notificationsToShow.forEach(function (n) {
   650       this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
   651     }, this);
   652   },
   654   /**
   655    * Updates the notification state in response to window activation or tab
   656    * selection changes.
   657    *
   658    * @param notifications an array of Notification instances. if null,
   659    *                      notifications will be retrieved off the current
   660    *                      browser tab
   661    * @param anchor is a XUL element that the notifications panel will be
   662    *                      anchored to
   663    * @param dismissShowing if true, dismiss any currently visible notifications
   664    *                       if there are no notifications to show. Otherwise,
   665    *                       currently displayed notifications will be left alone.
   666    */
   667   _update: function PopupNotifications_update(notifications, anchor, dismissShowing = false) {
   668     let useIconBox = this.iconBox && (!anchor || anchor.parentNode == this.iconBox);
   669     if (useIconBox) {
   670       // hide icons of the previous tab.
   671       this._hideIcons();
   672     }
   674     let anchorElement = anchor, notificationsToShow = [];
   675     if (!notifications)
   676       notifications = this._currentNotifications;
   677     let haveNotifications = notifications.length > 0;
   678     if (haveNotifications) {
   679       // Only show the notifications that have the passed-in anchor (or the
   680       // first notification's anchor, if none was passed in). Other
   681       // notifications will be shown once these are dismissed.
   682       anchorElement = anchor || notifications[0].anchorElement;
   684       if (useIconBox) {
   685         this._showIcons(notifications);
   686         this.iconBox.hidden = false;
   687       } else if (anchorElement) {
   688         this._updateAnchorIcon(notifications, anchorElement);
   689       }
   691       // Also filter out notifications that have been dismissed.
   692       notificationsToShow = notifications.filter(function (n) {
   693         return !n.dismissed && n.anchorElement == anchorElement &&
   694                !n.options.neverShow;
   695       });
   696     }
   698     if (notificationsToShow.length > 0) {
   699       this._showPanel(notificationsToShow, anchorElement);
   700     } else {
   701       // Notify observers that we're not showing the popup (useful for testing)
   702       this._notify("updateNotShowing");
   704       // Close the panel if there are no notifications to show.
   705       // When called from PopupNotifications.show() we should never close the
   706       // panel, however. It may just be adding a dismissed notification, in
   707       // which case we want to continue showing any existing notifications.
   708       if (!dismissShowing)
   709         this._dismiss();
   711       // Only hide the iconBox if we actually have no notifications (as opposed
   712       // to not having any showable notifications)
   713       if (!haveNotifications) {
   714         if (useIconBox)
   715           this.iconBox.hidden = true;
   716         else if (anchorElement)
   717           anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
   718       }
   719     }
   720   },
   722   _updateAnchorIcon: function PopupNotifications_updateAnchorIcon(notifications,
   723                                                                   anchorElement) {
   724     anchorElement.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
   725     // Use the anchorID as a class along with the default icon class as a
   726     // fallback if anchorID is not defined in CSS. We always use the first
   727     // notifications icon, so in the case of multiple notifications we'll
   728     // only use the default icon.
   729     if (anchorElement.classList.contains("notification-anchor-icon")) {
   730       // remove previous icon classes
   731       let className = anchorElement.className.replace(/([-\w]+-notification-icon\s?)/g,"")
   732       className = "default-notification-icon " + className;
   733       if (notifications.length == 1) {
   734         className = notifications[0].anchorID + " " + className;
   735       }
   736       anchorElement.className = className;
   737     }
   738   },
   740   _showIcons: function PopupNotifications_showIcons(aCurrentNotifications) {
   741     for (let notification of aCurrentNotifications) {
   742       let anchorElm = notification.anchorElement;
   743       if (anchorElm) {
   744         anchorElm.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
   745       }
   746     }
   747   },
   749   _hideIcons: function PopupNotifications_hideIcons() {
   750     let icons = this.iconBox.querySelectorAll(ICON_SELECTOR);
   751     for (let icon of icons) {
   752       icon.removeAttribute(ICON_ATTRIBUTE_SHOWING);
   753     }
   754   },
   756   /**
   757    * Gets and sets notifications for the browser.
   758    */
   759   _getNotificationsForBrowser: function PopupNotifications_getNotifications(browser) {
   760     let notifications = popupNotificationsMap.get(browser);
   761     if (!notifications) {
   762       // Initialize the WeakMap for the browser so callers can reference/manipulate the array.
   763       notifications = [];
   764       popupNotificationsMap.set(browser, notifications);
   765     }
   766     return notifications;
   767   },
   768   _setNotificationsForBrowser: function PopupNotifications_setNotifications(browser, notifications) {
   769     popupNotificationsMap.set(browser, notifications);
   770     return notifications;
   771   },
   773   _isActiveBrowser: function (browser) {
   774     // Note: This helper only exists, because in e10s builds,
   775     // we can't access the docShell of a browser from chrome.
   776     return browser.docShell
   777       ? browser.docShell.isActive
   778       : (this.window.gBrowser.selectedBrowser == browser);
   779   },
   781   _onIconBoxCommand: function PopupNotifications_onIconBoxCommand(event) {
   782     // Left click, space or enter only
   783     let type = event.type;
   784     if (type == "click" && event.button != 0)
   785       return;
   787     if (type == "keypress" &&
   788         !(event.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE ||
   789           event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN))
   790       return;
   792     if (this._currentNotifications.length == 0)
   793       return;
   795     // Get the anchor that is the immediate child of the icon box
   796     let anchor = event.target;
   797     while (anchor && anchor.parentNode != this.iconBox)
   798       anchor = anchor.parentNode;
   800     this._reshowNotifications(anchor);
   801   },
   803   _reshowNotifications: function PopupNotifications_reshowNotifications(anchor, browser) {
   804     // Mark notifications anchored to this anchor as un-dismissed
   805     let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser);
   806     notifications.forEach(function (n) {
   807       if (n.anchorElement == anchor)
   808         n.dismissed = false;
   809     });
   811     // ...and then show them.
   812     this._update(notifications, anchor);
   813   },
   815   _swapBrowserNotifications: function PopupNotifications_swapBrowserNoficications(ourBrowser, otherBrowser) {
   816     // When swaping browser docshells (e.g. dragging tab to new window) we need
   817     // to update our notification map.
   819     let ourNotifications = this._getNotificationsForBrowser(ourBrowser);
   820     let other = otherBrowser.ownerDocument.defaultView.PopupNotifications;
   821     if (!other) {
   822       if (ourNotifications.length > 0)
   823         Cu.reportError("unable to swap notifications: otherBrowser doesn't support notifications");
   824       return;
   825     }
   826     let otherNotifications = other._getNotificationsForBrowser(otherBrowser);
   827     if (ourNotifications.length < 1 && otherNotifications.length < 1) {
   828       // No notification to swap.
   829       return;
   830     }
   832     otherNotifications = otherNotifications.filter(n => {
   833       if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, ourBrowser)) {
   834         n.browser = ourBrowser;
   835         n.owner = this;
   836         return true;
   837       }
   838       other._fireCallback(n, NOTIFICATION_EVENT_REMOVED);
   839       return false;
   840     });
   842     ourNotifications = ourNotifications.filter(n => {
   843       if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, otherBrowser)) {
   844         n.browser = otherBrowser;
   845         n.owner = other;
   846         return true;
   847       }
   848       this._fireCallback(n, NOTIFICATION_EVENT_REMOVED);
   849       return false;
   850     });
   852     this._setNotificationsForBrowser(otherBrowser, ourNotifications);
   853     other._setNotificationsForBrowser(ourBrowser, otherNotifications);
   855     if (otherNotifications.length > 0)
   856       this._update(otherNotifications, otherNotifications[0].anchorElement);
   857     if (ourNotifications.length > 0)
   858       other._update(ourNotifications, ourNotifications[0].anchorElement);
   859   },
   861   _fireCallback: function PopupNotifications_fireCallback(n, event, ...args) {
   862     try {
   863       if (n.options.eventCallback)
   864         return n.options.eventCallback.call(n, event, ...args);
   865     } catch (error) {
   866       Cu.reportError(error);
   867     }
   868     return undefined;
   869   },
   871   _onPopupHidden: function PopupNotifications_onPopupHidden(event) {
   872     if (event.target != this.panel || this._ignoreDismissal)
   873       return;
   875     let browser = this.panel.firstChild &&
   876                   this.panel.firstChild.notification.browser;
   877     if (!browser)
   878       return;
   880     let notifications = this._getNotificationsForBrowser(browser);
   881     // Mark notifications as dismissed and call dismissal callbacks
   882     Array.forEach(this.panel.childNodes, function (nEl) {
   883       let notificationObj = nEl.notification;
   884       // Never call a dismissal handler on a notification that's been removed.
   885       if (notifications.indexOf(notificationObj) == -1)
   886         return;
   888       // Do not mark the notification as dismissed or fire NOTIFICATION_EVENT_DISMISSED
   889       // if the notification is removed.
   890       if (notificationObj.options.removeOnDismissal)
   891         this._remove(notificationObj);
   892       else {
   893         notificationObj.dismissed = true;
   894         this._fireCallback(notificationObj, NOTIFICATION_EVENT_DISMISSED);
   895       }
   896     }, this);
   898     this._clearPanel();
   900     this._update();
   901   },
   903   _onButtonCommand: function PopupNotifications_onButtonCommand(event) {
   904     // Need to find the associated notification object, which is a bit tricky
   905     // since it isn't associated with the button directly - this is kind of
   906     // gross and very dependent on the structure of the popupnotification
   907     // binding's content.
   908     let target = event.originalTarget;
   909     let notificationEl;
   910     let parent = target;
   911     while (parent && (parent = target.ownerDocument.getBindingParent(parent)))
   912       notificationEl = parent;
   914     if (!notificationEl)
   915       throw "PopupNotifications_onButtonCommand: couldn't find notification element";
   917     if (!notificationEl.notification)
   918       throw "PopupNotifications_onButtonCommand: couldn't find notification";
   920     let notification = notificationEl.notification;
   921     let timeSinceShown = this.window.performance.now() - notification.timeShown;
   923     // Only report the first time mainAction is triggered and remember that this occurred.
   924     if (!notification.timeMainActionFirstTriggered) {
   925       notification.timeMainActionFirstTriggered = timeSinceShown;
   926       Services.telemetry.getHistogramById("POPUP_NOTIFICATION_MAINACTION_TRIGGERED_MS").
   927                          add(timeSinceShown);
   928     }
   930     if (timeSinceShown < this.buttonDelay) {
   931       Services.console.logStringMessage("PopupNotifications_onButtonCommand: " +
   932                                         "Button click happened before the security delay: " +
   933                                         timeSinceShown + "ms");
   934       return;
   935     }
   936     try {
   937       notification.mainAction.callback.call();
   938     } catch(error) {
   939       Cu.reportError(error);
   940     }
   942     if (notification.mainAction.dismiss) {
   943       this._dismiss();
   944       return;
   945     }
   947     this._remove(notification);
   948     this._update();
   949   },
   951   _onMenuCommand: function PopupNotifications_onMenuCommand(event) {
   952     let target = event.originalTarget;
   953     if (!target.action || !target.notification)
   954       throw "menucommand target has no associated action/notification";
   956     event.stopPropagation();
   957     try {
   958       target.action.callback.call();
   959     } catch(error) {
   960       Cu.reportError(error);
   961     }
   963     if (target.action.dismiss) {
   964       this._dismiss();
   965       return;
   966     }
   968     this._remove(target.notification);
   969     this._update();
   970   },
   972   _notify: function PopupNotifications_notify(topic) {
   973     Services.obs.notifyObservers(null, "PopupNotifications-" + topic, "");
   974   },
   975 };

mercurial