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.

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

mercurial