toolkit/modules/PopupNotifications.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/modules/PopupNotifications.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,975 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +this.EXPORTED_SYMBOLS = ["PopupNotifications"];
     1.9 +
    1.10 +var Cc = Components.classes, Ci = Components.interfaces, Cu = Components.utils;
    1.11 +
    1.12 +Cu.import("resource://gre/modules/Services.jsm");
    1.13 +
    1.14 +const NOTIFICATION_EVENT_DISMISSED = "dismissed";
    1.15 +const NOTIFICATION_EVENT_REMOVED = "removed";
    1.16 +const NOTIFICATION_EVENT_SHOWING = "showing";
    1.17 +const NOTIFICATION_EVENT_SHOWN = "shown";
    1.18 +const NOTIFICATION_EVENT_SWAPPING = "swapping";
    1.19 +
    1.20 +const ICON_SELECTOR = ".notification-anchor-icon";
    1.21 +const ICON_ATTRIBUTE_SHOWING = "showing";
    1.22 +
    1.23 +const PREF_SECURITY_DELAY = "security.notification_enable_delay";
    1.24 +
    1.25 +let popupNotificationsMap = new WeakMap();
    1.26 +let gNotificationParents = new WeakMap;
    1.27 +
    1.28 +function getAnchorFromBrowser(aBrowser) {
    1.29 +  let anchor = aBrowser.getAttribute("popupnotificationanchor") ||
    1.30 +                aBrowser.popupnotificationanchor;
    1.31 +  if (anchor) {
    1.32 +    if (anchor instanceof Ci.nsIDOMXULElement) {
    1.33 +      return anchor;
    1.34 +    }
    1.35 +    return aBrowser.ownerDocument.getElementById(anchor);
    1.36 +  }
    1.37 +  return null;
    1.38 +}
    1.39 +
    1.40 +/**
    1.41 + * Notification object describes a single popup notification.
    1.42 + *
    1.43 + * @see PopupNotifications.show()
    1.44 + */
    1.45 +function Notification(id, message, anchorID, mainAction, secondaryActions,
    1.46 +                      browser, owner, options) {
    1.47 +  this.id = id;
    1.48 +  this.message = message;
    1.49 +  this.anchorID = anchorID;
    1.50 +  this.mainAction = mainAction;
    1.51 +  this.secondaryActions = secondaryActions || [];
    1.52 +  this.browser = browser;
    1.53 +  this.owner = owner;
    1.54 +  this.options = options || {};
    1.55 +}
    1.56 +
    1.57 +Notification.prototype = {
    1.58 +
    1.59 +  id: null,
    1.60 +  message: null,
    1.61 +  anchorID: null,
    1.62 +  mainAction: null,
    1.63 +  secondaryActions: null,
    1.64 +  browser: null,
    1.65 +  owner: null,
    1.66 +  options: null,
    1.67 +  timeShown: null,
    1.68 +
    1.69 +  /**
    1.70 +   * Removes the notification and updates the popup accordingly if needed.
    1.71 +   */
    1.72 +  remove: function Notification_remove() {
    1.73 +    this.owner.remove(this);
    1.74 +  },
    1.75 +
    1.76 +  get anchorElement() {
    1.77 +    let iconBox = this.owner.iconBox;
    1.78 +
    1.79 +    let anchorElement = getAnchorFromBrowser(this.browser);
    1.80 +
    1.81 +    if (!iconBox)
    1.82 +      return anchorElement;
    1.83 +
    1.84 +    if (!anchorElement && this.anchorID)
    1.85 +      anchorElement = iconBox.querySelector("#"+this.anchorID);
    1.86 +
    1.87 +    // Use a default anchor icon if it's available
    1.88 +    if (!anchorElement)
    1.89 +      anchorElement = iconBox.querySelector("#default-notification-icon") ||
    1.90 +                      iconBox;
    1.91 +
    1.92 +    return anchorElement;
    1.93 +  },
    1.94 +
    1.95 +  reshow: function() {
    1.96 +    this.owner._reshowNotifications(this.anchorElement, this.browser);
    1.97 +  }
    1.98 +};
    1.99 +
   1.100 +/**
   1.101 + * The PopupNotifications object manages popup notifications for a given browser
   1.102 + * window.
   1.103 + * @param tabbrowser
   1.104 + *        window's <xul:tabbrowser/>. Used to observe tab switching events and
   1.105 + *        for determining the active browser element.
   1.106 + * @param panel
   1.107 + *        The <xul:panel/> element to use for notifications. The panel is
   1.108 + *        populated with <popupnotification> children and displayed it as
   1.109 + *        needed.
   1.110 + * @param iconBox
   1.111 + *        Reference to a container element that should be hidden or
   1.112 + *        unhidden when notifications are hidden or shown. It should be the
   1.113 + *        parent of anchor elements whose IDs are passed to show().
   1.114 + *        It is used as a fallback popup anchor if notifications specify
   1.115 + *        invalid or non-existent anchor IDs.
   1.116 + */
   1.117 +this.PopupNotifications = function PopupNotifications(tabbrowser, panel, iconBox) {
   1.118 +  if (!(tabbrowser instanceof Ci.nsIDOMXULElement))
   1.119 +    throw "Invalid tabbrowser";
   1.120 +  if (iconBox && !(iconBox instanceof Ci.nsIDOMXULElement))
   1.121 +    throw "Invalid iconBox";
   1.122 +  if (!(panel instanceof Ci.nsIDOMXULElement))
   1.123 +    throw "Invalid panel";
   1.124 +
   1.125 +  this.window = tabbrowser.ownerDocument.defaultView;
   1.126 +  this.panel = panel;
   1.127 +  this.tabbrowser = tabbrowser;
   1.128 +  this.iconBox = iconBox;
   1.129 +  this.buttonDelay = Services.prefs.getIntPref(PREF_SECURITY_DELAY);
   1.130 +
   1.131 +  this.panel.addEventListener("popuphidden", this, true);
   1.132 +
   1.133 +  this.window.addEventListener("activate", this, true);
   1.134 +  if (this.tabbrowser.tabContainer)
   1.135 +    this.tabbrowser.tabContainer.addEventListener("TabSelect", this, true);
   1.136 +}
   1.137 +
   1.138 +PopupNotifications.prototype = {
   1.139 +
   1.140 +  window: null,
   1.141 +  panel: null,
   1.142 +  tabbrowser: null,
   1.143 +
   1.144 +  _iconBox: null,
   1.145 +  set iconBox(iconBox) {
   1.146 +    // Remove the listeners on the old iconBox, if needed
   1.147 +    if (this._iconBox) {
   1.148 +      this._iconBox.removeEventListener("click", this, false);
   1.149 +      this._iconBox.removeEventListener("keypress", this, false);
   1.150 +    }
   1.151 +    this._iconBox = iconBox;
   1.152 +    if (iconBox) {
   1.153 +      iconBox.addEventListener("click", this, false);
   1.154 +      iconBox.addEventListener("keypress", this, false);
   1.155 +    }
   1.156 +  },
   1.157 +  get iconBox() {
   1.158 +    return this._iconBox;
   1.159 +  },
   1.160 +
   1.161 +  /**
   1.162 +   * Enable or disable the opening/closing transition.
   1.163 +   * @param state
   1.164 +   *        Boolean state
   1.165 +   */
   1.166 +  set transitionsEnabled(state) {
   1.167 +    if (state) {
   1.168 +      this.panel.removeAttribute("animate");
   1.169 +    }
   1.170 +    else {
   1.171 +      this.panel.setAttribute("animate", "false");
   1.172 +    }
   1.173 +  },
   1.174 +
   1.175 +  /**
   1.176 +   * Retrieve a Notification object associated with the browser/ID pair.
   1.177 +   * @param id
   1.178 +   *        The Notification ID to search for.
   1.179 +   * @param browser
   1.180 +   *        The browser whose notifications should be searched. If null, the
   1.181 +   *        currently selected browser's notifications will be searched.
   1.182 +   *
   1.183 +   * @returns the corresponding Notification object, or null if no such
   1.184 +   *          notification exists.
   1.185 +   */
   1.186 +  getNotification: function PopupNotifications_getNotification(id, browser) {
   1.187 +    let n = null;
   1.188 +    let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser);
   1.189 +    notifications.some(function(x) x.id == id && (n = x));
   1.190 +    return n;
   1.191 +  },
   1.192 +
   1.193 +  /**
   1.194 +   * Adds a new popup notification.
   1.195 +   * @param browser
   1.196 +   *        The <xul:browser> element associated with the notification. Must not
   1.197 +   *        be null.
   1.198 +   * @param id
   1.199 +   *        A unique ID that identifies the type of notification (e.g.
   1.200 +   *        "geolocation"). Only one notification with a given ID can be visible
   1.201 +   *        at a time. If a notification already exists with the given ID, it
   1.202 +   *        will be replaced.
   1.203 +   * @param message
   1.204 +   *        The text to be displayed in the notification.
   1.205 +   * @param anchorID
   1.206 +   *        The ID of the element that should be used as this notification
   1.207 +   *        popup's anchor. May be null, in which case the notification will be
   1.208 +   *        anchored to the iconBox.
   1.209 +   * @param mainAction
   1.210 +   *        A JavaScript object literal describing the notification button's
   1.211 +   *        action. If present, it must have the following properties:
   1.212 +   *          - label (string): the button's label.
   1.213 +   *          - accessKey (string): the button's accessKey.
   1.214 +   *          - callback (function): a callback to be invoked when the button is
   1.215 +   *            pressed.
   1.216 +   *          - [optional] dismiss (boolean): If this is true, the notification
   1.217 +   *            will be dismissed instead of removed after running the callback.
   1.218 +   *        If null, the notification will not have a button, and
   1.219 +   *        secondaryActions will be ignored.
   1.220 +   * @param secondaryActions
   1.221 +   *        An optional JavaScript array describing the notification's alternate
   1.222 +   *        actions. The array should contain objects with the same properties
   1.223 +   *        as mainAction. These are used to populate the notification button's
   1.224 +   *        dropdown menu.
   1.225 +   * @param options
   1.226 +   *        An options JavaScript object holding additional properties for the
   1.227 +   *        notification. The following properties are currently supported:
   1.228 +   *        persistence: An integer. The notification will not automatically
   1.229 +   *                     dismiss for this many page loads.
   1.230 +   *        timeout:     A time in milliseconds. The notification will not
   1.231 +   *                     automatically dismiss before this time.
   1.232 +   *        persistWhileVisible:
   1.233 +   *                     A boolean. If true, a visible notification will always
   1.234 +   *                     persist across location changes.
   1.235 +   *        dismissed:   Whether the notification should be added as a dismissed
   1.236 +   *                     notification. Dismissed notifications can be activated
   1.237 +   *                     by clicking on their anchorElement.
   1.238 +   *        eventCallback:
   1.239 +   *                     Callback to be invoked when the notification changes
   1.240 +   *                     state. The callback's first argument is a string
   1.241 +   *                     identifying the state change:
   1.242 +   *                     "dismissed": notification has been dismissed by the
   1.243 +   *                                  user (e.g. by clicking away or switching
   1.244 +   *                                  tabs)
   1.245 +   *                     "removed": notification has been removed (due to
   1.246 +   *                                location change or user action)
   1.247 +   *                     "showing": notification is about to be shown
   1.248 +   *                                (this can be fired multiple times as
   1.249 +   *                                 notifications are dismissed and re-shown)
   1.250 +   *                                If the callback returns true, the notification
   1.251 +   *                                will be dismissed.
   1.252 +   *                     "shown": notification has been shown (this can be fired
   1.253 +   *                              multiple times as notifications are dismissed
   1.254 +   *                              and re-shown)
   1.255 +   *                     "swapping": the docshell of the browser that created
   1.256 +   *                                 the notification is about to be swapped to
   1.257 +   *                                 another browser. A second parameter contains
   1.258 +   *                                 the browser that is receiving the docshell,
   1.259 +   *                                 so that the event callback can transfer stuff
   1.260 +   *                                 specific to this notification.
   1.261 +   *                                 If the callback returns true, the notification
   1.262 +   *                                 will be moved to the new browser.
   1.263 +   *                                 If the callback isn't implemented, returns false,
   1.264 +   *                                 or doesn't return any value, the notification
   1.265 +   *                                 will be removed.
   1.266 +   *        neverShow:   Indicate that no popup should be shown for this
   1.267 +   *                     notification. Useful for just showing the anchor icon.
   1.268 +   *        removeOnDismissal:
   1.269 +   *                     Notifications with this parameter set to true will be
   1.270 +   *                     removed when they would have otherwise been dismissed
   1.271 +   *                     (i.e. any time the popup is closed due to user
   1.272 +   *                     interaction).
   1.273 +   *        hideNotNow:  If true, indicates that the 'Not Now' menuitem should
   1.274 +   *                     not be shown. If 'Not Now' is hidden, it needs to be
   1.275 +   *                     replaced by another 'do nothing' item, so providing at
   1.276 +   *                     least one secondary action is required; and one of the
   1.277 +   *                     actions needs to have the 'dismiss' property set to true.
   1.278 +   *        popupIconURL:
   1.279 +   *                     A string. URL of the image to be displayed in the popup.
   1.280 +   *                     Normally specified in CSS using list-style-image and the
   1.281 +   *                     .popup-notification-icon[popupid=...] selector.
   1.282 +   *        learnMoreURL:
   1.283 +   *                     A string URL. Setting this property will make the
   1.284 +   *                     prompt display a "Learn More" link that, when clicked,
   1.285 +   *                     opens the URL in a new tab.
   1.286 +   * @returns the Notification object corresponding to the added notification.
   1.287 +   */
   1.288 +  show: function PopupNotifications_show(browser, id, message, anchorID,
   1.289 +                                         mainAction, secondaryActions, options) {
   1.290 +    function isInvalidAction(a) {
   1.291 +      return !a || !(typeof(a.callback) == "function") || !a.label || !a.accessKey;
   1.292 +    }
   1.293 +
   1.294 +    if (!browser)
   1.295 +      throw "PopupNotifications_show: invalid browser";
   1.296 +    if (!id)
   1.297 +      throw "PopupNotifications_show: invalid ID";
   1.298 +    if (mainAction && isInvalidAction(mainAction))
   1.299 +      throw "PopupNotifications_show: invalid mainAction";
   1.300 +    if (secondaryActions && secondaryActions.some(isInvalidAction))
   1.301 +      throw "PopupNotifications_show: invalid secondaryActions";
   1.302 +    if (options && options.hideNotNow &&
   1.303 +        (!secondaryActions || !secondaryActions.length ||
   1.304 +         !secondaryActions.concat(mainAction).some(action => action.dismiss)))
   1.305 +      throw "PopupNotifications_show: 'Not Now' item hidden without replacement";
   1.306 +
   1.307 +    let notification = new Notification(id, message, anchorID, mainAction,
   1.308 +                                        secondaryActions, browser, this, options);
   1.309 +
   1.310 +    if (options && options.dismissed)
   1.311 +      notification.dismissed = true;
   1.312 +
   1.313 +    let existingNotification = this.getNotification(id, browser);
   1.314 +    if (existingNotification)
   1.315 +      this._remove(existingNotification);
   1.316 +
   1.317 +    let notifications = this._getNotificationsForBrowser(browser);
   1.318 +    notifications.push(notification);
   1.319 +
   1.320 +    let isActive = this._isActiveBrowser(browser);
   1.321 +    let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
   1.322 +    if (isActive && fm.activeWindow == this.window) {
   1.323 +      // show panel now
   1.324 +      this._update(notifications, notification.anchorElement, true);
   1.325 +    } else {
   1.326 +      // Otherwise, update() will display the notification the next time the
   1.327 +      // relevant tab/window is selected.
   1.328 +
   1.329 +      // If the tab is selected but the window is in the background, let the OS
   1.330 +      // tell the user that there's a notification waiting in that window.
   1.331 +      // At some point we might want to do something about background tabs here
   1.332 +      // too. When the user switches to this window, we'll show the panel if
   1.333 +      // this browser is a tab (thus showing the anchor icon). For
   1.334 +      // non-tabbrowser browsers, we need to make the icon visible now or the
   1.335 +      // user will not be able to open the panel.
   1.336 +      if (!notification.dismissed && isActive) {
   1.337 +        this.window.getAttention();
   1.338 +        if (notification.anchorElement.parentNode != this.iconBox) {
   1.339 +          this._updateAnchorIcon(notifications, notification.anchorElement);
   1.340 +        }
   1.341 +      }
   1.342 +
   1.343 +      // Notify observers that we're not showing the popup (useful for testing)
   1.344 +      this._notify("backgroundShow");
   1.345 +    }
   1.346 +
   1.347 +    return notification;
   1.348 +  },
   1.349 +
   1.350 +  /**
   1.351 +   * Returns true if the notification popup is currently being displayed.
   1.352 +   */
   1.353 +  get isPanelOpen() {
   1.354 +    let panelState = this.panel.state;
   1.355 +
   1.356 +    return panelState == "showing" || panelState == "open";
   1.357 +  },
   1.358 +
   1.359 +  /**
   1.360 +   * Called by the consumer to indicate that a browser's location has changed,
   1.361 +   * so that we can update the active notifications accordingly.
   1.362 +   */
   1.363 +  locationChange: function PopupNotifications_locationChange(aBrowser) {
   1.364 +    if (!aBrowser)
   1.365 +      throw "PopupNotifications_locationChange: invalid browser";
   1.366 +
   1.367 +    let notifications = this._getNotificationsForBrowser(aBrowser);
   1.368 +
   1.369 +    notifications = notifications.filter(function (notification) {
   1.370 +      // The persistWhileVisible option allows an open notification to persist
   1.371 +      // across location changes
   1.372 +      if (notification.options.persistWhileVisible &&
   1.373 +          this.isPanelOpen) {
   1.374 +        if ("persistence" in notification.options &&
   1.375 +          notification.options.persistence)
   1.376 +          notification.options.persistence--;
   1.377 +        return true;
   1.378 +      }
   1.379 +
   1.380 +      // The persistence option allows a notification to persist across multiple
   1.381 +      // page loads
   1.382 +      if ("persistence" in notification.options &&
   1.383 +          notification.options.persistence) {
   1.384 +        notification.options.persistence--;
   1.385 +        return true;
   1.386 +      }
   1.387 +
   1.388 +      // The timeout option allows a notification to persist until a certain time
   1.389 +      if ("timeout" in notification.options &&
   1.390 +          Date.now() <= notification.options.timeout) {
   1.391 +        return true;
   1.392 +      }
   1.393 +
   1.394 +      this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED);
   1.395 +      return false;
   1.396 +    }, this);
   1.397 +
   1.398 +    this._setNotificationsForBrowser(aBrowser, notifications);
   1.399 +
   1.400 +    if (this._isActiveBrowser(aBrowser)) {
   1.401 +      // get the anchor element if the browser has defined one so it will
   1.402 +      // _update will handle both the tabs iconBox and non-tab permission
   1.403 +      // anchors.
   1.404 +      let anchorElement = notifications.length > 0 ? notifications[0].anchorElement : null;
   1.405 +      if (!anchorElement)
   1.406 +        anchorElement = getAnchorFromBrowser(aBrowser);
   1.407 +      this._update(notifications, anchorElement);
   1.408 +    }
   1.409 +  },
   1.410 +
   1.411 +  /**
   1.412 +   * Removes a Notification.
   1.413 +   * @param notification
   1.414 +   *        The Notification object to remove.
   1.415 +   */
   1.416 +  remove: function PopupNotifications_remove(notification) {
   1.417 +    this._remove(notification);
   1.418 +
   1.419 +    if (this._isActiveBrowser(notification.browser)) {
   1.420 +      let notifications = this._getNotificationsForBrowser(notification.browser);
   1.421 +      this._update(notifications, notification.anchorElement);
   1.422 +    }
   1.423 +  },
   1.424 +
   1.425 +  handleEvent: function (aEvent) {
   1.426 +    switch (aEvent.type) {
   1.427 +      case "popuphidden":
   1.428 +        this._onPopupHidden(aEvent);
   1.429 +        break;
   1.430 +      case "activate":
   1.431 +      case "TabSelect":
   1.432 +        let self = this;
   1.433 +        // setTimeout(..., 0) needed, otherwise openPopup from "activate" event
   1.434 +        // handler results in the popup being hidden again for some reason...
   1.435 +        this.window.setTimeout(function () {
   1.436 +          self._update();
   1.437 +        }, 0);
   1.438 +        break;
   1.439 +      case "click":
   1.440 +      case "keypress":
   1.441 +        this._onIconBoxCommand(aEvent);
   1.442 +        break;
   1.443 +    }
   1.444 +  },
   1.445 +
   1.446 +////////////////////////////////////////////////////////////////////////////////
   1.447 +// Utility methods
   1.448 +////////////////////////////////////////////////////////////////////////////////
   1.449 +
   1.450 +  _ignoreDismissal: null,
   1.451 +  _currentAnchorElement: null,
   1.452 +
   1.453 +  /**
   1.454 +   * Gets notifications for the currently selected browser.
   1.455 +   */
   1.456 +  get _currentNotifications() {
   1.457 +    return this.tabbrowser.selectedBrowser ? this._getNotificationsForBrowser(this.tabbrowser.selectedBrowser) : [];
   1.458 +  },
   1.459 +
   1.460 +  _remove: function PopupNotifications_removeHelper(notification) {
   1.461 +    // This notification may already be removed, in which case let's just fail
   1.462 +    // silently.
   1.463 +    let notifications = this._getNotificationsForBrowser(notification.browser);
   1.464 +    if (!notifications)
   1.465 +      return;
   1.466 +
   1.467 +    var index = notifications.indexOf(notification);
   1.468 +    if (index == -1)
   1.469 +      return;
   1.470 +
   1.471 +    if (this._isActiveBrowser(notification.browser))
   1.472 +      notification.anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
   1.473 +
   1.474 +    // remove the notification
   1.475 +    notifications.splice(index, 1);
   1.476 +    this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED);
   1.477 +  },
   1.478 +
   1.479 +  /**
   1.480 +   * Dismisses the notification without removing it.
   1.481 +   */
   1.482 +  _dismiss: function PopupNotifications_dismiss() {
   1.483 +    let browser = this.panel.firstChild &&
   1.484 +                  this.panel.firstChild.notification.browser;
   1.485 +    this.panel.hidePopup();
   1.486 +    if (browser)
   1.487 +      browser.focus();
   1.488 +  },
   1.489 +
   1.490 +  /**
   1.491 +   * Hides the notification popup.
   1.492 +   */
   1.493 +  _hidePanel: function PopupNotifications_hide() {
   1.494 +    this._ignoreDismissal = true;
   1.495 +    this.panel.hidePopup();
   1.496 +    this._ignoreDismissal = false;
   1.497 +  },
   1.498 +
   1.499 +  /**
   1.500 +   * Removes all notifications from the notification popup.
   1.501 +   */
   1.502 +  _clearPanel: function () {
   1.503 +    let popupnotification;
   1.504 +    while ((popupnotification = this.panel.lastChild)) {
   1.505 +      this.panel.removeChild(popupnotification);
   1.506 +
   1.507 +      // If this notification was provided by the chrome document rather than
   1.508 +      // created ad hoc, move it back to where we got it from.
   1.509 +      let originalParent = gNotificationParents.get(popupnotification);
   1.510 +      if (originalParent) {
   1.511 +        popupnotification.notification = null;
   1.512 +
   1.513 +        // Remove nodes dynamically added to the notification's menu button
   1.514 +        // in _refreshPanel. Keep popupnotificationcontent nodes; they are
   1.515 +        // provided by the chrome document.
   1.516 +        let contentNode = popupnotification.lastChild;
   1.517 +        while (contentNode) {
   1.518 +          let previousSibling = contentNode.previousSibling;
   1.519 +          if (contentNode.nodeName != "popupnotificationcontent")
   1.520 +            popupnotification.removeChild(contentNode);
   1.521 +          contentNode = previousSibling;
   1.522 +        }
   1.523 +
   1.524 +        // Re-hide the notification such that it isn't rendered in the chrome
   1.525 +        // document. _refreshPanel will unhide it again when needed.
   1.526 +        popupnotification.hidden = true;
   1.527 +
   1.528 +        originalParent.appendChild(popupnotification);
   1.529 +      }
   1.530 +    }
   1.531 +  },
   1.532 +
   1.533 +  _refreshPanel: function PopupNotifications_refreshPanel(notificationsToShow) {
   1.534 +    this._clearPanel();
   1.535 +
   1.536 +    const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
   1.537 +
   1.538 +    notificationsToShow.forEach(function (n) {
   1.539 +      let doc = this.window.document;
   1.540 +
   1.541 +      // Append "-notification" to the ID to try to avoid ID conflicts with other stuff
   1.542 +      // in the document.
   1.543 +      let popupnotificationID = n.id + "-notification";
   1.544 +
   1.545 +      // If the chrome document provides a popupnotification with this id, use
   1.546 +      // that. Otherwise create it ad-hoc.
   1.547 +      let popupnotification = doc.getElementById(popupnotificationID);
   1.548 +      if (popupnotification)
   1.549 +        gNotificationParents.set(popupnotification, popupnotification.parentNode);
   1.550 +      else
   1.551 +        popupnotification = doc.createElementNS(XUL_NS, "popupnotification");
   1.552 +
   1.553 +      popupnotification.setAttribute("label", n.message);
   1.554 +      popupnotification.setAttribute("id", popupnotificationID);
   1.555 +      popupnotification.setAttribute("popupid", n.id);
   1.556 +      popupnotification.setAttribute("closebuttoncommand", "PopupNotifications._dismiss();");
   1.557 +      if (n.mainAction) {
   1.558 +        popupnotification.setAttribute("buttonlabel", n.mainAction.label);
   1.559 +        popupnotification.setAttribute("buttonaccesskey", n.mainAction.accessKey);
   1.560 +        popupnotification.setAttribute("buttoncommand", "PopupNotifications._onButtonCommand(event);");
   1.561 +        popupnotification.setAttribute("menucommand", "PopupNotifications._onMenuCommand(event);");
   1.562 +        popupnotification.setAttribute("closeitemcommand", "PopupNotifications._dismiss();event.stopPropagation();");
   1.563 +      } else {
   1.564 +        popupnotification.removeAttribute("buttonlabel");
   1.565 +        popupnotification.removeAttribute("buttonaccesskey");
   1.566 +        popupnotification.removeAttribute("buttoncommand");
   1.567 +        popupnotification.removeAttribute("menucommand");
   1.568 +        popupnotification.removeAttribute("closeitemcommand");
   1.569 +      }
   1.570 +
   1.571 +      if (n.options.popupIconURL)
   1.572 +        popupnotification.setAttribute("icon", n.options.popupIconURL);
   1.573 +      if (n.options.learnMoreURL)
   1.574 +        popupnotification.setAttribute("learnmoreurl", n.options.learnMoreURL);
   1.575 +      else
   1.576 +        popupnotification.removeAttribute("learnmoreurl");
   1.577 +
   1.578 +      popupnotification.notification = n;
   1.579 +
   1.580 +      if (n.secondaryActions) {
   1.581 +        n.secondaryActions.forEach(function (a) {
   1.582 +          let item = doc.createElementNS(XUL_NS, "menuitem");
   1.583 +          item.setAttribute("label", a.label);
   1.584 +          item.setAttribute("accesskey", a.accessKey);
   1.585 +          item.notification = n;
   1.586 +          item.action = a;
   1.587 +
   1.588 +          popupnotification.appendChild(item);
   1.589 +        }, this);
   1.590 +
   1.591 +        if (n.options.hideNotNow) {
   1.592 +          popupnotification.setAttribute("hidenotnow", "true");
   1.593 +        }
   1.594 +        else if (n.secondaryActions.length) {
   1.595 +          let closeItemSeparator = doc.createElementNS(XUL_NS, "menuseparator");
   1.596 +          popupnotification.appendChild(closeItemSeparator);
   1.597 +        }
   1.598 +      }
   1.599 +
   1.600 +      this.panel.appendChild(popupnotification);
   1.601 +
   1.602 +      // The popupnotification may be hidden if we got it from the chrome
   1.603 +      // document rather than creating it ad hoc.
   1.604 +      popupnotification.hidden = false;
   1.605 +    }, this);
   1.606 +  },
   1.607 +
   1.608 +  _showPanel: function PopupNotifications_showPanel(notificationsToShow, anchorElement) {
   1.609 +    this.panel.hidden = false;
   1.610 +
   1.611 +    notificationsToShow = notificationsToShow.filter(n => {
   1.612 +      let dismiss = this._fireCallback(n, NOTIFICATION_EVENT_SHOWING);
   1.613 +      if (dismiss)
   1.614 +        n.dismissed = true;
   1.615 +      return !dismiss;
   1.616 +    });
   1.617 +    if (!notificationsToShow.length)
   1.618 +      return;
   1.619 +
   1.620 +    this._refreshPanel(notificationsToShow);
   1.621 +
   1.622 +    if (this.isPanelOpen && this._currentAnchorElement == anchorElement)
   1.623 +      return;
   1.624 +
   1.625 +    // If the panel is already open but we're changing anchors, we need to hide
   1.626 +    // it first.  Otherwise it can appear in the wrong spot.  (_hidePanel is
   1.627 +    // safe to call even if the panel is already hidden.)
   1.628 +    this._hidePanel();
   1.629 +
   1.630 +    // If the anchor element is hidden or null, use the tab as the anchor. We
   1.631 +    // only ever show notifications for the current browser, so we can just use
   1.632 +    // the current tab.
   1.633 +    let selectedTab = this.tabbrowser.selectedTab;
   1.634 +    if (anchorElement) {
   1.635 +      let bo = anchorElement.boxObject;
   1.636 +      if (bo.height == 0 && bo.width == 0)
   1.637 +        anchorElement = selectedTab; // hidden
   1.638 +    } else {
   1.639 +      anchorElement = selectedTab; // null
   1.640 +    }
   1.641 +
   1.642 +    this._currentAnchorElement = anchorElement;
   1.643 +
   1.644 +    // On OS X and Linux we need a different panel arrow color for
   1.645 +    // click-to-play plugins, so copy the popupid and use css.
   1.646 +    this.panel.setAttribute("popupid", this.panel.firstChild.getAttribute("popupid"));
   1.647 +    notificationsToShow.forEach(function (n) {
   1.648 +      // Remember the time the notification was shown for the security delay.
   1.649 +      n.timeShown = this.window.performance.now();
   1.650 +    }, this);
   1.651 +    this.panel.openPopup(anchorElement, "bottomcenter topleft");
   1.652 +    notificationsToShow.forEach(function (n) {
   1.653 +      this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
   1.654 +    }, this);
   1.655 +  },
   1.656 +
   1.657 +  /**
   1.658 +   * Updates the notification state in response to window activation or tab
   1.659 +   * selection changes.
   1.660 +   *
   1.661 +   * @param notifications an array of Notification instances. if null,
   1.662 +   *                      notifications will be retrieved off the current
   1.663 +   *                      browser tab
   1.664 +   * @param anchor is a XUL element that the notifications panel will be
   1.665 +   *                      anchored to
   1.666 +   * @param dismissShowing if true, dismiss any currently visible notifications
   1.667 +   *                       if there are no notifications to show. Otherwise,
   1.668 +   *                       currently displayed notifications will be left alone.
   1.669 +   */
   1.670 +  _update: function PopupNotifications_update(notifications, anchor, dismissShowing = false) {
   1.671 +    let useIconBox = this.iconBox && (!anchor || anchor.parentNode == this.iconBox);
   1.672 +    if (useIconBox) {
   1.673 +      // hide icons of the previous tab.
   1.674 +      this._hideIcons();
   1.675 +    }
   1.676 +
   1.677 +    let anchorElement = anchor, notificationsToShow = [];
   1.678 +    if (!notifications)
   1.679 +      notifications = this._currentNotifications;
   1.680 +    let haveNotifications = notifications.length > 0;
   1.681 +    if (haveNotifications) {
   1.682 +      // Only show the notifications that have the passed-in anchor (or the
   1.683 +      // first notification's anchor, if none was passed in). Other
   1.684 +      // notifications will be shown once these are dismissed.
   1.685 +      anchorElement = anchor || notifications[0].anchorElement;
   1.686 +
   1.687 +      if (useIconBox) {
   1.688 +        this._showIcons(notifications);
   1.689 +        this.iconBox.hidden = false;
   1.690 +      } else if (anchorElement) {
   1.691 +        this._updateAnchorIcon(notifications, anchorElement);
   1.692 +      }
   1.693 +
   1.694 +      // Also filter out notifications that have been dismissed.
   1.695 +      notificationsToShow = notifications.filter(function (n) {
   1.696 +        return !n.dismissed && n.anchorElement == anchorElement &&
   1.697 +               !n.options.neverShow;
   1.698 +      });
   1.699 +    }
   1.700 +
   1.701 +    if (notificationsToShow.length > 0) {
   1.702 +      this._showPanel(notificationsToShow, anchorElement);
   1.703 +    } else {
   1.704 +      // Notify observers that we're not showing the popup (useful for testing)
   1.705 +      this._notify("updateNotShowing");
   1.706 +
   1.707 +      // Close the panel if there are no notifications to show.
   1.708 +      // When called from PopupNotifications.show() we should never close the
   1.709 +      // panel, however. It may just be adding a dismissed notification, in
   1.710 +      // which case we want to continue showing any existing notifications.
   1.711 +      if (!dismissShowing)
   1.712 +        this._dismiss();
   1.713 +
   1.714 +      // Only hide the iconBox if we actually have no notifications (as opposed
   1.715 +      // to not having any showable notifications)
   1.716 +      if (!haveNotifications) {
   1.717 +        if (useIconBox)
   1.718 +          this.iconBox.hidden = true;
   1.719 +        else if (anchorElement)
   1.720 +          anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
   1.721 +      }
   1.722 +    }
   1.723 +  },
   1.724 +
   1.725 +  _updateAnchorIcon: function PopupNotifications_updateAnchorIcon(notifications,
   1.726 +                                                                  anchorElement) {
   1.727 +    anchorElement.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
   1.728 +    // Use the anchorID as a class along with the default icon class as a
   1.729 +    // fallback if anchorID is not defined in CSS. We always use the first
   1.730 +    // notifications icon, so in the case of multiple notifications we'll
   1.731 +    // only use the default icon.
   1.732 +    if (anchorElement.classList.contains("notification-anchor-icon")) {
   1.733 +      // remove previous icon classes
   1.734 +      let className = anchorElement.className.replace(/([-\w]+-notification-icon\s?)/g,"")
   1.735 +      className = "default-notification-icon " + className;
   1.736 +      if (notifications.length == 1) {
   1.737 +        className = notifications[0].anchorID + " " + className;
   1.738 +      }
   1.739 +      anchorElement.className = className;
   1.740 +    }
   1.741 +  },
   1.742 +
   1.743 +  _showIcons: function PopupNotifications_showIcons(aCurrentNotifications) {
   1.744 +    for (let notification of aCurrentNotifications) {
   1.745 +      let anchorElm = notification.anchorElement;
   1.746 +      if (anchorElm) {
   1.747 +        anchorElm.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
   1.748 +      }
   1.749 +    }
   1.750 +  },
   1.751 +
   1.752 +  _hideIcons: function PopupNotifications_hideIcons() {
   1.753 +    let icons = this.iconBox.querySelectorAll(ICON_SELECTOR);
   1.754 +    for (let icon of icons) {
   1.755 +      icon.removeAttribute(ICON_ATTRIBUTE_SHOWING);
   1.756 +    }
   1.757 +  },
   1.758 +
   1.759 +  /**
   1.760 +   * Gets and sets notifications for the browser.
   1.761 +   */
   1.762 +  _getNotificationsForBrowser: function PopupNotifications_getNotifications(browser) {
   1.763 +    let notifications = popupNotificationsMap.get(browser);
   1.764 +    if (!notifications) {
   1.765 +      // Initialize the WeakMap for the browser so callers can reference/manipulate the array.
   1.766 +      notifications = [];
   1.767 +      popupNotificationsMap.set(browser, notifications);
   1.768 +    }
   1.769 +    return notifications;
   1.770 +  },
   1.771 +  _setNotificationsForBrowser: function PopupNotifications_setNotifications(browser, notifications) {
   1.772 +    popupNotificationsMap.set(browser, notifications);
   1.773 +    return notifications;
   1.774 +  },
   1.775 +
   1.776 +  _isActiveBrowser: function (browser) {
   1.777 +    // Note: This helper only exists, because in e10s builds,
   1.778 +    // we can't access the docShell of a browser from chrome.
   1.779 +    return browser.docShell
   1.780 +      ? browser.docShell.isActive
   1.781 +      : (this.window.gBrowser.selectedBrowser == browser);
   1.782 +  },
   1.783 +
   1.784 +  _onIconBoxCommand: function PopupNotifications_onIconBoxCommand(event) {
   1.785 +    // Left click, space or enter only
   1.786 +    let type = event.type;
   1.787 +    if (type == "click" && event.button != 0)
   1.788 +      return;
   1.789 +
   1.790 +    if (type == "keypress" &&
   1.791 +        !(event.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE ||
   1.792 +          event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN))
   1.793 +      return;
   1.794 +
   1.795 +    if (this._currentNotifications.length == 0)
   1.796 +      return;
   1.797 +
   1.798 +    // Get the anchor that is the immediate child of the icon box
   1.799 +    let anchor = event.target;
   1.800 +    while (anchor && anchor.parentNode != this.iconBox)
   1.801 +      anchor = anchor.parentNode;
   1.802 +
   1.803 +    this._reshowNotifications(anchor);
   1.804 +  },
   1.805 +
   1.806 +  _reshowNotifications: function PopupNotifications_reshowNotifications(anchor, browser) {
   1.807 +    // Mark notifications anchored to this anchor as un-dismissed
   1.808 +    let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser);
   1.809 +    notifications.forEach(function (n) {
   1.810 +      if (n.anchorElement == anchor)
   1.811 +        n.dismissed = false;
   1.812 +    });
   1.813 +
   1.814 +    // ...and then show them.
   1.815 +    this._update(notifications, anchor);
   1.816 +  },
   1.817 +
   1.818 +  _swapBrowserNotifications: function PopupNotifications_swapBrowserNoficications(ourBrowser, otherBrowser) {
   1.819 +    // When swaping browser docshells (e.g. dragging tab to new window) we need
   1.820 +    // to update our notification map.
   1.821 +
   1.822 +    let ourNotifications = this._getNotificationsForBrowser(ourBrowser);
   1.823 +    let other = otherBrowser.ownerDocument.defaultView.PopupNotifications;
   1.824 +    if (!other) {
   1.825 +      if (ourNotifications.length > 0)
   1.826 +        Cu.reportError("unable to swap notifications: otherBrowser doesn't support notifications");
   1.827 +      return;
   1.828 +    }
   1.829 +    let otherNotifications = other._getNotificationsForBrowser(otherBrowser);
   1.830 +    if (ourNotifications.length < 1 && otherNotifications.length < 1) {
   1.831 +      // No notification to swap.
   1.832 +      return;
   1.833 +    }
   1.834 +
   1.835 +    otherNotifications = otherNotifications.filter(n => {
   1.836 +      if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, ourBrowser)) {
   1.837 +        n.browser = ourBrowser;
   1.838 +        n.owner = this;
   1.839 +        return true;
   1.840 +      }
   1.841 +      other._fireCallback(n, NOTIFICATION_EVENT_REMOVED);
   1.842 +      return false;
   1.843 +    });
   1.844 +
   1.845 +    ourNotifications = ourNotifications.filter(n => {
   1.846 +      if (this._fireCallback(n, NOTIFICATION_EVENT_SWAPPING, otherBrowser)) {
   1.847 +        n.browser = otherBrowser;
   1.848 +        n.owner = other;
   1.849 +        return true;
   1.850 +      }
   1.851 +      this._fireCallback(n, NOTIFICATION_EVENT_REMOVED);
   1.852 +      return false;
   1.853 +    });
   1.854 +
   1.855 +    this._setNotificationsForBrowser(otherBrowser, ourNotifications);
   1.856 +    other._setNotificationsForBrowser(ourBrowser, otherNotifications);
   1.857 +
   1.858 +    if (otherNotifications.length > 0)
   1.859 +      this._update(otherNotifications, otherNotifications[0].anchorElement);
   1.860 +    if (ourNotifications.length > 0)
   1.861 +      other._update(ourNotifications, ourNotifications[0].anchorElement);
   1.862 +  },
   1.863 +
   1.864 +  _fireCallback: function PopupNotifications_fireCallback(n, event, ...args) {
   1.865 +    try {
   1.866 +      if (n.options.eventCallback)
   1.867 +        return n.options.eventCallback.call(n, event, ...args);
   1.868 +    } catch (error) {
   1.869 +      Cu.reportError(error);
   1.870 +    }
   1.871 +    return undefined;
   1.872 +  },
   1.873 +
   1.874 +  _onPopupHidden: function PopupNotifications_onPopupHidden(event) {
   1.875 +    if (event.target != this.panel || this._ignoreDismissal)
   1.876 +      return;
   1.877 +
   1.878 +    let browser = this.panel.firstChild &&
   1.879 +                  this.panel.firstChild.notification.browser;
   1.880 +    if (!browser)
   1.881 +      return;
   1.882 +
   1.883 +    let notifications = this._getNotificationsForBrowser(browser);
   1.884 +    // Mark notifications as dismissed and call dismissal callbacks
   1.885 +    Array.forEach(this.panel.childNodes, function (nEl) {
   1.886 +      let notificationObj = nEl.notification;
   1.887 +      // Never call a dismissal handler on a notification that's been removed.
   1.888 +      if (notifications.indexOf(notificationObj) == -1)
   1.889 +        return;
   1.890 +
   1.891 +      // Do not mark the notification as dismissed or fire NOTIFICATION_EVENT_DISMISSED
   1.892 +      // if the notification is removed.
   1.893 +      if (notificationObj.options.removeOnDismissal)
   1.894 +        this._remove(notificationObj);
   1.895 +      else {
   1.896 +        notificationObj.dismissed = true;
   1.897 +        this._fireCallback(notificationObj, NOTIFICATION_EVENT_DISMISSED);
   1.898 +      }
   1.899 +    }, this);
   1.900 +
   1.901 +    this._clearPanel();
   1.902 +
   1.903 +    this._update();
   1.904 +  },
   1.905 +
   1.906 +  _onButtonCommand: function PopupNotifications_onButtonCommand(event) {
   1.907 +    // Need to find the associated notification object, which is a bit tricky
   1.908 +    // since it isn't associated with the button directly - this is kind of
   1.909 +    // gross and very dependent on the structure of the popupnotification
   1.910 +    // binding's content.
   1.911 +    let target = event.originalTarget;
   1.912 +    let notificationEl;
   1.913 +    let parent = target;
   1.914 +    while (parent && (parent = target.ownerDocument.getBindingParent(parent)))
   1.915 +      notificationEl = parent;
   1.916 +
   1.917 +    if (!notificationEl)
   1.918 +      throw "PopupNotifications_onButtonCommand: couldn't find notification element";
   1.919 +
   1.920 +    if (!notificationEl.notification)
   1.921 +      throw "PopupNotifications_onButtonCommand: couldn't find notification";
   1.922 +
   1.923 +    let notification = notificationEl.notification;
   1.924 +    let timeSinceShown = this.window.performance.now() - notification.timeShown;
   1.925 +
   1.926 +    // Only report the first time mainAction is triggered and remember that this occurred.
   1.927 +    if (!notification.timeMainActionFirstTriggered) {
   1.928 +      notification.timeMainActionFirstTriggered = timeSinceShown;
   1.929 +      Services.telemetry.getHistogramById("POPUP_NOTIFICATION_MAINACTION_TRIGGERED_MS").
   1.930 +                         add(timeSinceShown);
   1.931 +    }
   1.932 +
   1.933 +    if (timeSinceShown < this.buttonDelay) {
   1.934 +      Services.console.logStringMessage("PopupNotifications_onButtonCommand: " +
   1.935 +                                        "Button click happened before the security delay: " +
   1.936 +                                        timeSinceShown + "ms");
   1.937 +      return;
   1.938 +    }
   1.939 +    try {
   1.940 +      notification.mainAction.callback.call();
   1.941 +    } catch(error) {
   1.942 +      Cu.reportError(error);
   1.943 +    }
   1.944 +
   1.945 +    if (notification.mainAction.dismiss) {
   1.946 +      this._dismiss();
   1.947 +      return;
   1.948 +    }
   1.949 +
   1.950 +    this._remove(notification);
   1.951 +    this._update();
   1.952 +  },
   1.953 +
   1.954 +  _onMenuCommand: function PopupNotifications_onMenuCommand(event) {
   1.955 +    let target = event.originalTarget;
   1.956 +    if (!target.action || !target.notification)
   1.957 +      throw "menucommand target has no associated action/notification";
   1.958 +
   1.959 +    event.stopPropagation();
   1.960 +    try {
   1.961 +      target.action.callback.call();
   1.962 +    } catch(error) {
   1.963 +      Cu.reportError(error);
   1.964 +    }
   1.965 +
   1.966 +    if (target.action.dismiss) {
   1.967 +      this._dismiss();
   1.968 +      return;
   1.969 +    }
   1.970 +
   1.971 +    this._remove(target.notification);
   1.972 +    this._update();
   1.973 +  },
   1.974 +
   1.975 +  _notify: function PopupNotifications_notify(topic) {
   1.976 +    Services.obs.notifyObservers(null, "PopupNotifications-" + topic, "");
   1.977 +  },
   1.978 +};

mercurial