Bug 958071 - PopupNotification consumers should have a way to replace the "Not Now" item, r=dolske.
authorFlorian Quèze <florian@queze.net>
Thu, 23 Jan 2014 11:48:33 +0100
changeset 164917 22737775ef328cbf76553a1f6c71df97517c17e4
parent 164916 b92275eaf6a749d1d74b8a2b09bcc36935923adf
child 164918 815d5970e94759b3978142cba323fa6913f8e869
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersdolske
bugs958071
milestone29.0a1
Bug 958071 - PopupNotification consumers should have a way to replace the "Not Now" item, r=dolske.
browser/base/content/test/general/browser_popupNotification.js
toolkit/content/widgets/notification.xml
toolkit/modules/PopupNotifications.jsm
--- a/browser/base/content/test/general/browser_popupNotification.js
+++ b/browser/base/content/test/general/browser_popupNotification.js
@@ -977,17 +977,17 @@ var tests = [
       // checkPopup checks for the matching label. Note that this assumes that
       // this.notifyObj.mainAction is the same as notification.mainAction,
       // which could be a problem if we ever decided to deep-copy.
       checkPopup(popup, this.notifyObj);
       triggerMainCommand(popup);
     },
     onHidden: function() { }
   },
-  { // Test #31 - Moving a tab to a new window should remove non-swappable
+  { // Test #34 - Moving a tab to a new window should remove non-swappable
     // notifications.
     run: function() {
       gBrowser.selectedTab = gBrowser.addTab("about:blank");
       let notifyObj = new basicNotification();
       showNotification(notifyObj);
       let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
       whenDelayedStartupFinished(win, function() {
         let [tab] = win.gBrowser.tabs;
@@ -997,17 +997,17 @@ var tests = [
            "no notification displayed in new window");
         ok(notifyObj.swappingCallbackTriggered, "the swapping callback was triggered");
         ok(notifyObj.removedCallbackTriggered, "the removed callback was triggered");
         win.close();
         goNext();
       });
     }
   },
-  { // Test #32 - Moving a tab to a new window should preserve swappable notifications.
+  { // Test #35 - Moving a tab to a new window should preserve swappable notifications.
     run: function() {
       gBrowser.selectedTab = gBrowser.addTab("about:blank");
       let notifyObj = new basicNotification();
       let originalCallback = notifyObj.options.eventCallback;
         notifyObj.options.eventCallback = function (eventName) {
           originalCallback(eventName);
           return eventName == "swapping";
         };
@@ -1019,16 +1019,60 @@ var tests = [
         let anchor = win.document.getElementById("default-notification-icon");
         win.PopupNotifications._reshowNotifications(anchor);
         checkPopup(win.PopupNotifications.panel, notifyObj);
         ok(notifyObj.swappingCallbackTriggered, "the swapping callback was triggered");
         win.close();
         goNext();
       });
     }
+  },
+  { // Test #36 - the hideNotNow option
+    run: function () {
+      this.notifyObj = new basicNotification();
+      this.notifyObj.options.hideNotNow = true;
+      this.notifyObj.mainAction.dismiss = true;
+      showNotification(this.notifyObj);
+    },
+    onShown: function (popup) {
+      // checkPopup verifies that the Not Now item is hidden, and that no separator is added.
+      checkPopup(popup, this.notifyObj);
+      triggerMainCommand(popup);
+    },
+    onHidden: function (popup) { }
+  },
+  { // Test #37 - the main action callback can keep the notification.
+    run: function () {
+      this.notifyObj = new basicNotification();
+      this.notifyObj.mainAction.dismiss = true;
+      showNotification(this.notifyObj);
+    },
+    onShown: function (popup) {
+      checkPopup(popup, this.notifyObj);
+      triggerMainCommand(popup);
+    },
+    onHidden: function (popup) {
+      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered");
+      ok(!this.notifyObj.removedCallbackTriggered, "removed callback wasn't triggered");
+    }
+  },
+  { // Test #38 - a secondary action callback can keep the notification.
+    run: function () {
+      this.notifyObj = new basicNotification();
+      this.notifyObj.secondaryActions[0].dismiss = true;
+      showNotification(this.notifyObj);
+    },
+    onShown: function (popup) {
+      checkPopup(popup, this.notifyObj);
+      triggerSecondaryCommand(popup, 0);
+    },
+    onHidden: function (popup) {
+      ok(this.notifyObj.dismissalCallbackTriggered, "dismissal callback was triggered");
+      ok(!this.notifyObj.removedCallbackTriggered, "removed callback wasn't triggered");
+    }
   }
 ];
 
 function showNotification(notifyObj) {
   return PopupNotifications.show(notifyObj.browser,
                                  notifyObj.id,
                                  notifyObj.message,
                                  notifyObj.anchorID,
@@ -1058,17 +1102,22 @@ function checkPopup(popup, notificationO
   if (notificationObj.mainAction) {
     is(notification.getAttribute("buttonlabel"), notificationObj.mainAction.label, "main action label matches");
     is(notification.getAttribute("buttonaccesskey"), notificationObj.mainAction.accessKey, "main action accesskey matches");
   }
   let actualSecondaryActions = Array.filter(notification.childNodes,
                                             function (child) child.nodeName == "menuitem");
   let secondaryActions = notificationObj.secondaryActions || [];
   let actualSecondaryActionsCount = actualSecondaryActions.length;
-  if (secondaryActions.length) {
+  if (notificationObj.options.hideNotNow) {
+    is(notification.getAttribute("hidenotnow"), "true", "Not Now item hidden");
+    if (secondaryActions.length)
+      is(notification.lastChild.tagName, "menuitem", "no menuseparator");
+  }
+  else if (secondaryActions.length) {
     is(notification.lastChild.tagName, "menuseparator", "menuseparator exists");
   }
   is(actualSecondaryActionsCount, secondaryActions.length, actualSecondaryActions.length + " secondary actions");
   secondaryActions.forEach(function (a, i) {
     is(actualSecondaryActions[i].getAttribute("label"), a.label, "label for secondary action " + i + " matches");
     is(actualSecondaryActions[i].getAttribute("accesskey"), a.accessKey, "accessKey for secondary action " + i + " matches");
   });
 }
--- a/toolkit/content/widgets/notification.xml
+++ b/toolkit/content/widgets/notification.xml
@@ -463,17 +463,17 @@
                       class="popup-notification-menubutton"
                       type="menu-button"
                       xbl:inherits="oncommand=buttoncommand,label=buttonlabel,accesskey=buttonaccesskey">
             <xul:menupopup anonid="menupopup"
                            xbl:inherits="oncommand=menucommand">
               <children/>
               <xul:menuitem class="menuitem-iconic popup-notification-closeitem close-icon"
                             label="&closeNotificationItem.label;"
-                            xbl:inherits="oncommand=closeitemcommand"/>
+                            xbl:inherits="oncommand=closeitemcommand,hidden=hidenotnow"/>
             </xul:menupopup>
           </xul:button>
         </xul:hbox>
       </xul:vbox>
       <xul:vbox pack="start">
         <xul:toolbarbutton anonid="closebutton"
                            class="messageCloseButton close-icon popup-notification-closebutton tabbable"
                            xbl:inherits="oncommand=closebuttoncommand"
--- a/toolkit/modules/PopupNotifications.jsm
+++ b/toolkit/modules/PopupNotifications.jsm
@@ -191,16 +191,18 @@ PopupNotifications.prototype = {
    *        anchored to the iconBox.
    * @param mainAction
    *        A JavaScript object literal describing the notification button's
    *        action. If present, it must have the following properties:
    *          - label (string): the button's label.
    *          - accessKey (string): the button's accessKey.
    *          - callback (function): a callback to be invoked when the button is
    *            pressed.
+   *          - [optional] dismiss (boolean): If this is true, the notification
+   *            will be dismissed instead of removed after running the callback.
    *        If null, the notification will not have a button, and
    *        secondaryActions will be ignored.
    * @param secondaryActions
    *        An optional JavaScript array describing the notification's alternate
    *        actions. The array should contain objects with the same properties
    *        as mainAction. These are used to populate the notification button's
    *        dropdown menu.
    * @param options
@@ -244,16 +246,21 @@ PopupNotifications.prototype = {
    *                                 will be removed.
    *        neverShow:   Indicate that no popup should be shown for this
    *                     notification. Useful for just showing the anchor icon.
    *        removeOnDismissal:
    *                     Notifications with this parameter set to true will be
    *                     removed when they would have otherwise been dismissed
    *                     (i.e. any time the popup is closed due to user
    *                     interaction).
+   *        hideNotNow:  If true, indicates that the 'Not Now' menuitem should
+   *                     not be shown. If 'Not Now' is hidden, it needs to be
+   *                     replaced by another 'do nothing' item, so providing at
+   *                     least one secondary action is required; and one of the
+   *                     actions needs to have the 'dismiss' property set to true.
    *        popupIconURL:
    *                     A string. URL of the image to be displayed in the popup.
    *                     Normally specified in CSS using list-style-image and the
    *                     .popup-notification-icon[popupid=...] selector.
    * @returns the Notification object corresponding to the added notification.
    */
   show: function PopupNotifications_show(browser, id, message, anchorID,
                                          mainAction, secondaryActions, options) {
@@ -264,16 +271,20 @@ PopupNotifications.prototype = {
     if (!browser)
       throw "PopupNotifications_show: invalid browser";
     if (!id)
       throw "PopupNotifications_show: invalid ID";
     if (mainAction && isInvalidAction(mainAction))
       throw "PopupNotifications_show: invalid mainAction";
     if (secondaryActions && secondaryActions.some(isInvalidAction))
       throw "PopupNotifications_show: invalid secondaryActions";
+    if (options && options.hideNotNow &&
+        (!secondaryActions || !secondaryActions.length ||
+         !secondaryActions.concat(mainAction).some(action => action.dismiss)))
+      throw "PopupNotifications_show: 'Not Now' item hidden without replacement";
 
     let notification = new Notification(id, message, anchorID, mainAction,
                                         secondaryActions, browser, this, options);
 
     if (options && options.dismissed)
       notification.dismissed = true;
 
     let existingNotification = this.getNotification(id, browser);
@@ -544,17 +555,20 @@ PopupNotifications.prototype = {
           item.setAttribute("label", a.label);
           item.setAttribute("accesskey", a.accessKey);
           item.notification = n;
           item.action = a;
 
           popupnotification.appendChild(item);
         }, this);
 
-        if (n.secondaryActions.length) {
+        if (n.options.hideNotNow) {
+          popupnotification.setAttribute("hidenotnow", "true");
+        }
+        else if (n.secondaryActions.length) {
           let closeItemSeparator = doc.createElementNS(XUL_NS, "menuseparator");
           popupnotification.appendChild(closeItemSeparator);
         }
       }
 
       this.panel.appendChild(popupnotification);
 
       // The popupnotification may be hidden if we got it from the chrome
@@ -889,32 +903,42 @@ PopupNotifications.prototype = {
       return;
     }
     try {
       notification.mainAction.callback.call();
     } catch(error) {
       Cu.reportError(error);
     }
 
+    if (notification.mainAction.dismiss) {
+      this._dismiss();
+      return;
+    }
+
     this._remove(notification);
     this._update();
   },
 
   _onMenuCommand: function PopupNotifications_onMenuCommand(event) {
     let target = event.originalTarget;
     if (!target.action || !target.notification)
       throw "menucommand target has no associated action/notification";
 
     event.stopPropagation();
     try {
       target.action.callback.call();
     } catch(error) {
       Cu.reportError(error);
     }
 
+    if (target.action.dismiss) {
+      this._dismiss();
+      return;
+    }
+
     this._remove(target.notification);
     this._update();
   },
 
   _notify: function PopupNotifications_notify(topic) {
     Services.obs.notifyObservers(null, "PopupNotifications-" + topic, "");
   },
 };