Bug 1291642 - Part 1 - Add an optional checkbox to PopupNotifications. r=paolo
authorJohann Hofmann <jhofmann@mozilla.com>
Tue, 06 Sep 2016 18:36:23 +0200
changeset 312961 c37f66cfffcaff8c09f61fe6417605d1a24bf8ab
parent 312960 49136978cec73e21b953e9d6152914836b50d296
child 312962 3a1eefbee0df4c5f7b0d43be16e36a228f05f1cd
push id30665
push usercbook@mozilla.com
push dateWed, 07 Sep 2016 15:20:43 +0000
treeherdermozilla-central@95acb9299faf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspaolo
bugs1291642
milestone51.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1291642 - Part 1 - Add an optional checkbox to PopupNotifications. r=paolo MozReview-Commit-ID: 9wzV6kNt5pV
browser/modules/webrtcUI.jsm
toolkit/content/widgets/notification.xml
toolkit/modules/PopupNotifications.jsm
toolkit/themes/osx/global/notification.css
toolkit/themes/windows/global/notification.css
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -342,18 +342,18 @@ function prompt(aBrowser, aRequest) {
 
   if (aRequest.secure && !sharingScreen && !sharingAudio) {
     // Don't show the 'Always' action if the connection isn't secure, or for
     // screen/audio sharing (because we can't guess which window the user wants
     // to share without prompting).
     secondaryActions.unshift({
       label: stringBundle.getString("getUserMedia.always.label"),
       accessKey: stringBundle.getString("getUserMedia.always.accesskey"),
-      callback: function () {
-        mainAction.callback(true);
+      callback: function (aState) {
+        mainAction.callback(aState, true);
       }
     });
   }
 
   let options = {
     eventCallback: function(aTopic, aNewBrowser) {
       if (aTopic == "swapping")
         return true;
@@ -514,17 +514,17 @@ function prompt(aBrowser, aRequest) {
       if (sharingScreen)
         listScreenShareDevices(windowMenupopup, videoDevices);
       else
         listDevices(camMenupopup, videoDevices);
 
       if (!sharingAudio)
         listDevices(micMenupopup, audioDevices);
 
-      this.mainAction.callback = function(aRemember) {
+      this.mainAction.callback = function(aState, aRemember) {
         let allowedDevices = [];
         let perms = Services.perms;
         if (videoDevices.length) {
           let listId = "webRTC-select" + (sharingScreen ? "Window" : "Camera") + "-menulist";
           let videoDeviceIndex = chromeDoc.getElementById(listId).value;
           let allowCamera = videoDeviceIndex != "-1";
           if (allowCamera) {
             allowedDevices.push(videoDeviceIndex);
--- a/toolkit/content/widgets/notification.xml
+++ b/toolkit/content/widgets/notification.xml
@@ -488,39 +488,45 @@
           <xul:toolbarbutton anonid="closebutton"
                              class="messageCloseButton close-icon popup-notification-closebutton tabbable"
                              xbl:inherits="oncommand=closebuttoncommand"
                              tooltiptext="&closeNotification.tooltip;"/>
         </xul:hbox>
         <children includes="popupnotificationcontent"/>
         <xul:label class="text-link popup-notification-learnmore-link"
                xbl:inherits="onclick=learnmoreclick,href=learnmoreurl">&learnMore;</xul:label>
+        <xul:checkbox anonid="checkbox"
+                      xbl:inherits="hidden=checkboxhidden,checked=checkboxchecked,label=checkboxlabel,oncommand=checkboxcommand" />
+        <xul:description class="popup-notification-warning" xbl:inherits="hidden=warninghidden,xbl:text=warninglabel"/>
         <xul:spacer flex="1"/>
         <xul:hbox class="popup-notification-button-container"
                   pack="end" align="center">
           <children includes="button"/>
           <xul:button anonid="button"
                       class="popup-notification-menubutton"
                       type="menu-button"
-                      xbl:inherits="oncommand=buttoncommand,onpopupshown=buttonpopupshown,label=buttonlabel,accesskey=buttonaccesskey">
+                      xbl:inherits="oncommand=buttoncommand,onpopupshown=buttonpopupshown,label=buttonlabel,accesskey=buttonaccesskey,disabled=mainactiondisabled">
             <xul:menupopup anonid="menupopup"
                            xbl:inherits="oncommand=menucommand">
               <children/>
               <xul:menuitem class="menuitem-iconic popup-notification-closeitem"
                             label="&closeNotificationItem.label;"
                             xbl:inherits="oncommand=closeitemcommand,hidden=hidenotnow"/>
             </xul:menupopup>
           </xul:button>
         </xul:hbox>
       </xul:vbox>
     </content>
     <resources>
       <stylesheet src="chrome://global/skin/notification.css"/>
     </resources>
     <implementation>
+      <field name="checkbox" readonly="true">
+        document.getAnonymousElementByAttribute(this, "anonid", "checkbox");
+      </field>
       <field name="closebutton" readonly="true">
         document.getAnonymousElementByAttribute(this, "anonid", "closebutton");
       </field>
       <field name="button" readonly="true">
         document.getAnonymousElementByAttribute(this, "anonid", "button");
       </field>
       <field name="menupopup" readonly="true">
         document.getAnonymousElementByAttribute(this, "anonid", "menupopup");
--- a/toolkit/modules/PopupNotifications.jsm
+++ b/toolkit/modules/PopupNotifications.jsm
@@ -50,16 +50,28 @@ function getAnchorFromBrowser(aBrowser, 
     if (anchor instanceof Ci.nsIDOMXULElement) {
       return anchor;
     }
     return aBrowser.ownerDocument.getElementById(anchor);
   }
   return null;
 }
 
+function getNotificationFromElement(aElement) {
+  // Need to find the associated notification object, which is a bit tricky
+  // since it isn't associated with the element directly - this is kind of
+  // gross and very dependent on the structure of the popupnotification
+  // binding's content.
+  let notificationEl;
+  let parent = aElement;
+  while (parent && (parent = aElement.ownerDocument.getBindingParent(parent)))
+    notificationEl = parent;
+  return notificationEl;
+}
+
 /**
  * Notification object describes a single popup notification.
  *
  * @see PopupNotifications.show()
  */
 function Notification(id, message, anchorID, mainAction, secondaryActions,
                       browser, owner, options) {
   this.id = id;
@@ -67,16 +79,18 @@ function Notification(id, message, ancho
   this.anchorID = anchorID;
   this.mainAction = mainAction;
   this.secondaryActions = secondaryActions || [];
   this.browser = browser;
   this.owner = owner;
   this.options = options || {};
 
   this._dismissed = false;
+  // Will become a boolean when manually toggled by the user.
+  this._checkboxChecked = null;
   this.wasDismissed = false;
   this.recordedTelemetryStats = new Set();
   this.isPrivate = PrivateBrowsingUtils.isWindowPrivate(
                                         this.browser.ownerDocument.defaultView);
   this.timeCreated = this.owner.window.performance.now();
 }
 
 Notification.prototype = {
@@ -269,17 +283,18 @@ PopupNotifications.prototype = {
    *        popup's anchor. May be null, in which case the notification will be
    *        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.
+   *            pressed, is passed an object that contains the following fields:
+   *              - checkboxChecked: (boolean) If the optional checkbox is checked.
    *          - [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
@@ -327,16 +342,36 @@ 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).
+   *        checkbox:    An object that allows you to add a checkbox and
+   *                     control its behavior with these fields:
+   *                       label:
+   *                         (required) Label to be shown next to the checkbox.
+   *                       checked:
+   *                         (optional) Whether the checkbox should be checked
+   *                         by default. Defaults to false.
+   *                       checkedState:
+   *                         (optional) An object that allows you to customize
+   *                         the notification state when the checkbox is checked.
+   *                           disableMainAction:
+   *                             (optional) Whether the mainAction is disabled.
+   *                             Defaults to false.
+   *                           warningLabel:
+   *                             (optional) A (warning) text that is shown below the
+   *                             checkbox. Pass null to hide.
+   *                       uncheckedState:
+   *                         (optional) An object that allows you to customize
+   *                         the notification state when the checkbox is not checked.
+   *                         Has the same attributes as checkedState.
    *        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
@@ -700,24 +735,69 @@ PopupNotifications.prototype = {
           popupnotification.setAttribute("hidenotnow", "true");
         }
         else if (n.secondaryActions.length) {
           let closeItemSeparator = doc.createElementNS(XUL_NS, "menuseparator");
           popupnotification.appendChild(closeItemSeparator);
         }
       }
 
+      let checkbox = n.options.checkbox;
+      if (checkbox && checkbox.label) {
+        let checked = n._checkboxChecked != null ? n._checkboxChecked : !!checkbox.checked;
+
+        popupnotification.setAttribute("checkboxhidden", "false");
+        popupnotification.setAttribute("checkboxchecked", checked);
+        popupnotification.setAttribute("checkboxlabel", checkbox.label);
+
+        popupnotification.setAttribute("checkboxcommand", "PopupNotifications._onCheckboxCommand(event);");
+
+        if (checked) {
+          this._setNotificationUIState(popupnotification, checkbox.checkedState);
+        } else {
+          this._setNotificationUIState(popupnotification, checkbox.uncheckedState);
+        }
+      } else {
+        popupnotification.setAttribute("checkboxhidden", "true");
+      }
+
       this.panel.appendChild(popupnotification);
 
       // The popupnotification may be hidden if we got it from the chrome
       // document rather than creating it ad hoc.
       popupnotification.hidden = false;
     }, this);
   },
 
+  _setNotificationUIState(notification, state={}) {
+    notification.setAttribute("mainactiondisabled", state.disableMainAction || "false");
+
+    if (state.warningLabel) {
+      notification.setAttribute("warninglabel", state.warningLabel);
+      notification.setAttribute("warninghidden", "false");
+    } else {
+      notification.setAttribute("warninghidden", "true");
+    }
+  },
+
+  _onCheckboxCommand(event) {
+    let notificationEl = getNotificationFromElement(event.originalTarget);
+    let checked = notificationEl.checkbox.checked;
+    let notification = notificationEl.notification;
+
+    // Save checkbox state to be able to persist it when re-opening the doorhanger.
+    notification._checkboxChecked = checked;
+
+    if (checked) {
+      this._setNotificationUIState(notificationEl, notification.options.checkbox.checkedState);
+    } else {
+      this._setNotificationUIState(notificationEl, notification.options.checkbox.uncheckedState);
+    }
+  },
+
   _showPanel: function PopupNotifications_showPanel(notificationsToShow, anchorElement) {
     this.panel.hidden = false;
 
     notificationsToShow = notificationsToShow.filter(n => {
       let dismiss = this._fireCallback(n, NOTIFICATION_EVENT_SHOWING);
       if (dismiss)
         n.dismissed = true;
       return !dismiss;
@@ -1097,25 +1177,17 @@ PopupNotifications.prototype = {
       } else {
         notificationObj.dismissed = true;
         this._fireCallback(notificationObj, NOTIFICATION_EVENT_DISMISSED);
       }
     }, this);
   },
 
   _onButtonEvent(event, type) {
-    // Need to find the associated notification object, which is a bit tricky
-    // since it isn't associated with the button directly - this is kind of
-    // gross and very dependent on the structure of the popupnotification
-    // binding's content.
-    let target = event.originalTarget;
-    let notificationEl;
-    let parent = target;
-    while (parent && (parent = target.ownerDocument.getBindingParent(parent)))
-      notificationEl = parent;
+    let notificationEl = getNotificationFromElement(event.originalTarget);
 
     if (!notificationEl)
       throw "PopupNotifications._onButtonEvent: couldn't find notification element";
 
     if (!notificationEl.notification)
       throw "PopupNotifications._onButtonEvent: couldn't find notification";
 
     let notification = notificationEl.notification;
@@ -1145,17 +1217,19 @@ PopupNotifications.prototype = {
                                         "Button click happened before the security delay: " +
                                         timeSinceShown + "ms");
       return;
     }
 
     notification._recordTelemetryStat(TELEMETRY_STAT_ACTION_1);
 
     try {
-      notification.mainAction.callback.call();
+      notification.mainAction.callback.call(undefined, {
+        checkboxChecked: notificationEl.checkbox.checked
+      });
     } catch (error) {
       Cu.reportError(error);
     }
 
     if (notification.mainAction.dismiss) {
       this._dismiss();
       return;
     }
@@ -1164,22 +1238,25 @@ PopupNotifications.prototype = {
     this._update();
   },
 
   _onMenuCommand: function PopupNotifications_onMenuCommand(event) {
     let target = event.originalTarget;
     if (!target.action || !target.notification)
       throw "menucommand target has no associated action/notification";
 
+    let notificationEl = target.parentElement;
     event.stopPropagation();
 
     target.notification._recordTelemetryStat(target.action.telemetryStatId);
 
     try {
-      target.action.callback.call();
+      target.action.callback.call(undefined, {
+        checkboxChecked: notificationEl.checkbox.checked
+      });
     } catch (error) {
       Cu.reportError(error);
     }
 
     if (target.action.dismiss) {
       this._dismiss();
       return;
     }
--- a/toolkit/themes/osx/global/notification.css
+++ b/toolkit/themes/osx/global/notification.css
@@ -195,8 +195,12 @@ notification[type="info"]:not([value="tr
 
 .popup-notification-closeitem > .menu-iconic-left {
   display: none;
 }
 
 .popup-notification-menubutton > .button-menubutton-button[disabled] {
   opacity: 0.5;
 }
+
+.popup-notification-warning {
+  color: #aa1b08;
+}
--- a/toolkit/themes/windows/global/notification.css
+++ b/toolkit/themes/windows/global/notification.css
@@ -202,8 +202,12 @@ XXX: apply styles to all themes until bu
 .popup-notification-closebutton {
   margin-inline-end: -14px;
   margin-top: -10px;
 }
 
 .popup-notification-menubutton > .button-menubutton-button[disabled] {
   opacity: 0.5;
 }
+
+@media (-moz-windows-default-theme) {
+  color: #aa1b08;
+}