Bug 1130356: Allow multiple anchors for multiple notifications without using an iconbox. r=florian
authorMike de Boer <mdeboer@mozilla.com>
Thu, 12 Feb 2015 11:42:42 +0100
changeset 242533 6a6bb80ab89661f6b9de4b63d492d64f2944e97f
parent 242532 2338f0e2a5203def18f021936dffffb40239496f
child 242534 8db110f4c10a91f0a2ce40de0b8958b0103c79c2
push id660
push usermichael.l.comella@gmail.com
push dateThu, 12 Feb 2015 18:55:31 +0000
reviewersflorian
bugs1130356
milestone38.0a1
Bug 1130356: Allow multiple anchors for multiple notifications without using an iconbox. r=florian
toolkit/modules/PopupNotifications.jsm
--- a/toolkit/modules/PopupNotifications.jsm
+++ b/toolkit/modules/PopupNotifications.jsm
@@ -306,23 +306,24 @@ PopupNotifications.prototype = {
 
     let isActiveBrowser = this._isActiveBrowser(browser);
     let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
     let isActiveWindow = fm.activeWindow == this.window;
 
     if (isActiveBrowser) {
       if (isActiveWindow) {
         // show panel now
-        this._update(notifications, notification.anchorElement, true);
+        this._update(notifications, new Set([notification.anchorElement]), true);
       } else {
         // indicate attention and update the icon if necessary
         if (!notification.dismissed) {
           this.window.getAttention();
         }
-        this._updateAnchorIcon(notifications, notification.anchorElement);
+        this._updateAnchorIcons(notifications, this._getAnchorsForNotifications(
+          notifications, notification.anchorElement));
         this._notify("backgroundShow");
       }
 
     } else {
       // Notify observers that we're not showing the popup (useful for testing)
       this._notify("backgroundShow");
     }
 
@@ -378,34 +379,32 @@ PopupNotifications.prototype = {
     }, this);
 
     this._setNotificationsForBrowser(aBrowser, notifications);
 
     if (this._isActiveBrowser(aBrowser)) {
       // get the anchor element if the browser has defined one so it will
       // _update will handle both the tabs iconBox and non-tab permission
       // anchors.
-      let anchorElement = notifications.length > 0 ? notifications[0].anchorElement : null;
-      if (!anchorElement)
-        anchorElement = getAnchorFromBrowser(aBrowser);
-      this._update(notifications, anchorElement);
+      this._update(notifications, this._getAnchorsForNotifications(notifications,
+        getAnchorFromBrowser(aBrowser)));
     }
   },
 
   /**
    * Removes a Notification.
    * @param notification
    *        The Notification object to remove.
    */
   remove: function PopupNotifications_remove(notification) {
     this._remove(notification);
 
     if (this._isActiveBrowser(notification.browser)) {
       let notifications = this._getNotificationsForBrowser(notification.browser);
-      this._update(notifications, notification.anchorElement);
+      this._update(notifications);
     }
   },
 
   handleEvent: function (aEvent) {
     switch (aEvent.type) {
       case "popuphidden":
         this._onPopupHidden(aEvent);
         break;
@@ -650,106 +649,122 @@ PopupNotifications.prototype = {
 
   /**
    * Updates the notification state in response to window activation or tab
    * selection changes.
    *
    * @param notifications an array of Notification instances. if null,
    *                      notifications will be retrieved off the current
    *                      browser tab
-   * @param anchor is a XUL element that the notifications panel will be
-   *                      anchored to
+   * @param anchors       is a XUL element or a Set of XUL elements that the
+   *                      notifications panel(s) will be anchored to.
    * @param dismissShowing if true, dismiss any currently visible notifications
    *                       if there are no notifications to show. Otherwise,
    *                       currently displayed notifications will be left alone.
    */
-  _update: function PopupNotifications_update(notifications, anchor, dismissShowing = false) {
-    let useIconBox = this.iconBox && (!anchor || anchor.parentNode == this.iconBox);
+  _update: function PopupNotifications_update(notifications, anchors = new Set(), dismissShowing = false) {
+    if (anchors instanceof Ci.nsIDOMXULElement)
+      anchors = new Set([anchors]);
+    if (!notifications)
+      notifications = this._currentNotifications;
+    let haveNotifications = notifications.length > 0;
+    if (!anchors.size && haveNotifications)
+      anchors = this._getAnchorsForNotifications(notifications);
+
+    let useIconBox = !!this.iconBox;
+    if (useIconBox && anchors.size) {
+      for (let anchor of anchors) {
+        if (anchor.parentNode == this.iconBox)
+          continue;
+        useIconBox = false;
+        break;
+      }
+    }
+
     if (useIconBox) {
       // hide icons of the previous tab.
       this._hideIcons();
     }
 
-    let anchorElement = anchor, notificationsToShow = [];
-    if (!notifications)
-      notifications = this._currentNotifications;
-    let haveNotifications = notifications.length > 0;
+    let notificationsToShow = [];
     if (haveNotifications) {
       // Filter out notifications that have been dismissed.
       notificationsToShow = notifications.filter(function (n) {
         return !n.dismissed && !n.options.neverShow;
       });
 
-      // If no anchor has been passed in, use the anchor of the first
-      // showable notification.
-      if (!anchorElement && notificationsToShow.length)
-        anchorElement = notificationsToShow[0].anchorElement;
-
       if (useIconBox) {
         this._showIcons(notifications);
         this.iconBox.hidden = false;
-      } else if (anchorElement) {
-        this._updateAnchorIcon(notifications, anchorElement);
+      } else if (anchors.size) {
+        this._updateAnchorIcons(notifications, anchors);
       }
 
       // Also filter out notifications that are for a different anchor.
       notificationsToShow = notificationsToShow.filter(function (n) {
-        return n.anchorElement == anchorElement;
+        return anchors.has(n.anchorElement);
       });
     }
 
     if (notificationsToShow.length > 0) {
-      this._showPanel(notificationsToShow, anchorElement);
+      for (let anchorElement of anchors) {
+        this._showPanel(notificationsToShow, anchorElement);
+        break;
+      }
     } else {
       // Notify observers that we're not showing the popup (useful for testing)
       this._notify("updateNotShowing");
 
       // Close the panel if there are no notifications to show.
       // When called from PopupNotifications.show() we should never close the
       // panel, however. It may just be adding a dismissed notification, in
       // which case we want to continue showing any existing notifications.
       if (!dismissShowing)
         this._dismiss();
 
       // Only hide the iconBox if we actually have no notifications (as opposed
       // to not having any showable notifications)
       if (!haveNotifications) {
-        if (useIconBox)
+        if (useIconBox) {
           this.iconBox.hidden = true;
-        else if (anchorElement)
-          anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
+        } else if (anchors.size) {
+          for (let anchorElement of anchors)
+            anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
+        }
       }
     }
   },
 
-  _updateAnchorIcon: function PopupNotifications_updateAnchorIcon(notifications,
-                                                                  anchorElement) {
-    anchorElement.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
-    // Use the anchorID as a class along with the default icon class as a
-    // fallback if anchorID is not defined in CSS. We always use the first
-    // notifications icon, so in the case of multiple notifications we'll
-    // only use the default icon.
-    if (anchorElement.classList.contains("notification-anchor-icon")) {
-      // remove previous icon classes
-      let className = anchorElement.className.replace(/([-\w]+-notification-icon\s?)/g,"")
-      className = "default-notification-icon " + className;
-      if (notifications.length > 0) {
-        // Find the first notification this anchor belongs to.
-        let notification = notifications[0];
-        for (let n of notifications) {
-          if (n.anchorElement == anchorElement) {
-            notification = n;
-            break;
+  _updateAnchorIcons: function PopupNotifications_updateAnchorIcons(notifications,
+                                                                    anchorElements) {
+    for (let anchorElement of anchorElements) {
+      anchorElement.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
+      // Use the anchorID as a class along with the default icon class as a
+      // fallback if anchorID is not defined in CSS. We always use the first
+      // notifications icon, so in the case of multiple notifications we'll
+      // only use the default icon.
+      if (anchorElement.classList.contains("notification-anchor-icon")) {
+        // remove previous icon classes
+        let className = anchorElement.className.replace(/([-\w]+-notification-icon\s?)/g,"")
+        className = "default-notification-icon " + className;
+        if (notifications.length > 0) {
+          // Find the first notification this anchor used for.
+          let notification = notifications[0];
+          for (let n of notifications) {
+            if (n.anchorElement == anchorElement) {
+              notification = n;
+              break;
+            }
           }
+          // With this notification we can better approximate the most fitting
+          // style.
+          className = notification.anchorID + " " + className;
         }
-        // With this notification we can better approximate the most fitting
-        // style.
-        className = notification.anchorID + " " + className;
+        anchorElement.className = className;
       }
-      anchorElement.className = className;
     }
   },
 
   _showIcons: function PopupNotifications_showIcons(aCurrentNotifications) {
     for (let notification of aCurrentNotifications) {
       let anchorElm = notification.anchorElement;
       if (anchorElm) {
         anchorElm.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
@@ -776,16 +791,27 @@ PopupNotifications.prototype = {
     }
     return notifications;
   },
   _setNotificationsForBrowser: function PopupNotifications_setNotifications(browser, notifications) {
     popupNotificationsMap.set(browser, notifications);
     return notifications;
   },
 
+  _getAnchorsForNotifications: function PopupNotifications_getAnchorsForNotifications(notifications, defaultAnchor) {
+    let anchors = new Set();
+    for (let notification of notifications) {
+      if (notification.anchorElement)
+        anchors.add(notification.anchorElement)
+    }
+    if (defaultAnchor && !anchors.size)
+      anchors.add(defaultAnchor);
+    return anchors;
+  },
+
   _isActiveBrowser: function (browser) {
     // Note: This helper only exists, because in e10s builds,
     // we can't access the docShell of a browser from chrome.
     return browser.docShell
       ? browser.docShell.isActive
       : (this.window.gBrowser.selectedBrowser == browser);
   },
 
@@ -869,19 +895,19 @@ PopupNotifications.prototype = {
       this._fireCallback(n, NOTIFICATION_EVENT_REMOVED);
       return false;
     });
 
     this._setNotificationsForBrowser(otherBrowser, ourNotifications);
     other._setNotificationsForBrowser(ourBrowser, otherNotifications);
 
     if (otherNotifications.length > 0)
-      this._update(otherNotifications, otherNotifications[0].anchorElement);
+      this._update(otherNotifications);
     if (ourNotifications.length > 0)
-      other._update(ourNotifications, ourNotifications[0].anchorElement);
+      other._update(ourNotifications);
   },
 
   _fireCallback: function PopupNotifications_fireCallback(n, event, ...args) {
     try {
       if (n.options.eventCallback)
         return n.options.eventCallback.call(n, event, ...args);
     } catch (error) {
       Cu.reportError(error);