--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -5,16 +5,18 @@
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScrollbarSampler",
"resource:///modules/ScrollbarSampler.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
"resource://gre/modules/ShortcutUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AppMenuNotifications",
+ "resource://gre/modules/AppMenuNotifications.jsm");
XPCOMUtils.defineLazyPreferenceGetter(this, "gPhotonStructure",
"browser.photon.structure.enabled", false);
/**
* Maintains the state and dispatches events for the main menu panel.
*/
@@ -41,38 +43,39 @@ const PanelUI = {
overflowFixedList: gPhotonStructure ? "widget-overflow-fixed-list" : "",
overflowPanel: gPhotonStructure ? "widget-overflow" : "",
navbar: "nav-bar",
};
},
_initialized: false,
+ _notifications: null,
+
init() {
this._initElements();
- this.notifications = [];
this.menuButton.addEventListener("mousedown", this);
this.menuButton.addEventListener("keypress", this);
this._overlayScrollListenerBoundFn = this._overlayScrollListener.bind(this);
Services.obs.addObserver(this, "fullscreen-nav-toolbox");
- Services.obs.addObserver(this, "panelUI-notification-main-action");
- Services.obs.addObserver(this, "panelUI-notification-dismissed");
+ Services.obs.addObserver(this, "appMenu-notifications");
window.addEventListener("fullscreen", this);
window.addEventListener("activate", this);
window.matchMedia("(-moz-overlay-scrollbars)").addListener(this._overlayScrollListenerBoundFn);
CustomizableUI.addListener(this);
for (let event of this.kEvents) {
this.notificationPanel.addEventListener(event, this);
}
this._initPhotonPanel();
+ Services.obs.notifyObservers(null, "appMenu-notifications-request", "refresh");
this._initialized = true;
},
reinit() {
this._removeEventListeners();
// If the Photon pref changes, we need to re-init our element references.
this._initElements();
@@ -137,18 +140,17 @@ const PanelUI = {
uninit() {
this._removeEventListeners();
for (let event of this.kEvents) {
this.notificationPanel.removeEventListener(event, this);
}
Services.obs.removeObserver(this, "fullscreen-nav-toolbox");
- Services.obs.removeObserver(this, "panelUI-notification-main-action");
- Services.obs.removeObserver(this, "panelUI-notification-dismissed");
+ Services.obs.removeObserver(this, "appMenu-notifications");
window.removeEventListener("fullscreen", this);
window.removeEventListener("activate", this);
this.menuButton.removeEventListener("mousedown", this);
this.menuButton.removeEventListener("keypress", this);
window.matchMedia("(-moz-overlay-scrollbars)").removeListener(this._overlayScrollListenerBoundFn);
CustomizableUI.removeListener(this);
this._overlayScrollListenerBoundFn = null;
@@ -227,104 +229,41 @@ const PanelUI = {
anchor = this._getPanelAnchor(anchor);
this.panel.openPopup(anchor);
}, (reason) => {
console.error("Error showing the PanelUI menu", reason);
});
});
},
- showNotification(id, mainAction, secondaryActions = [], options = {}) {
- let notification = new PanelUINotification(id, mainAction, secondaryActions, options);
- let existingIndex = this.notifications.findIndex(n => n.id == id);
- if (existingIndex != -1) {
- this.notifications.splice(existingIndex, 1);
- }
-
- // We don't want to clobber doorhanger notifications just to show a badge,
- // so don't dismiss any of them and the badge will show once the doorhanger
- // gets resolved.
- if (!options.badgeOnly && !options.dismissed) {
- this.notifications.forEach(n => { n.dismissed = true; });
- }
-
- // Since notifications are generally somewhat pressing, the ideal case is that
- // we never have two notifications at once. However, in the event that we do,
- // it's more likely that the older notification has been sitting around for a
- // bit, and so we don't want to hide the new notification behind it. Thus,
- // we want our notifications to behave like a stack instead of a queue.
- this.notifications.unshift(notification);
- this._updateNotifications();
- return notification;
- },
-
- showBadgeOnlyNotification(id) {
- return this.showNotification(id, null, null, { badgeOnly: true });
- },
-
- removeNotification(id) {
- let notifications;
- if (typeof id == "string") {
- notifications = this.notifications.filter(n => n.id == id);
- } else {
- // If it's not a string, assume RegExp
- notifications = this.notifications.filter(n => id.test(n.id));
- }
- // _updateNotifications can be expensive if it forces attachment of XBL
- // bindings that haven't been used yet, so return early if we haven't found
- // any notification to remove, as callers may expect this removeNotification
- // method to be a no-op for non-existent notifications.
- if (!notifications.length) {
- return;
- }
-
- notifications.forEach(n => {
- this._removeNotification(n);
- });
- this._updateNotifications();
- },
-
- dismissNotification(id) {
- let notifications;
- if (typeof id == "string") {
- notifications = this.notifications.filter(n => n.id == id);
- } else {
- // If it's not a string, assume RegExp
- notifications = this.notifications.filter(n => id.test(n.id));
- }
-
- notifications.forEach(n => n.dismissed = true);
- this._updateNotifications();
- },
-
/**
* If the menu panel is being shown, hide it.
*/
hide() {
if (document.documentElement.hasAttribute("customizing")) {
return;
}
this.panel.hidePopup();
},
observe(subject, topic, status) {
switch (topic) {
case "fullscreen-nav-toolbox":
- this._updateNotifications();
- break;
- case "panelUI-notification-main-action":
- if (subject != window) {
- this.removeNotification(status);
+ if (this._notifications) {
+ this._updateNotifications(false);
}
break;
- case "panelUI-notification-dismissed":
- if (subject != window) {
- this.dismissNotification(status);
+ case "appMenu-notifications":
+ // Don't initialize twice.
+ if (status == "init" && this._notifications) {
+ break;
}
+ this._notifications = AppMenuNotifications.notifications;
+ this._updateNotifications(true);
break;
}
},
handleEvent(aEvent) {
// Ignore context menus and menu button menus showing and hiding:
if (aEvent.type.startsWith("popup") &&
aEvent.target != this.panel) {
@@ -371,26 +310,16 @@ const PanelUI = {
},
get isNotificationPanelOpen() {
let panelState = this.notificationPanel.state;
return panelState == "showing" || panelState == "open";
},
- get activeNotification() {
- if (this.notifications.length > 0) {
- const doorhanger =
- this.notifications.find(n => !n.dismissed && !n.options.badgeOnly);
- return doorhanger || this.notifications[0];
- }
-
- return null;
- },
-
/**
* Registering the menu panel is done lazily for performance reasons. This
* method is exposed so that CustomizationMode can force panel-readyness in the
* event that customization mode is started before the panel has been opened
* by the user.
*
* @param aCustomizing (optional) set to true if this was called while entering
* customization mode. If that's the case, we trust that customization
@@ -744,54 +673,57 @@ const PanelUI = {
},
_hidePopup() {
if (this.isNotificationPanelOpen) {
this.notificationPanel.hidePopup();
}
},
- _updateNotifications() {
- if (!this.notifications.length) {
- this._clearAllNotifications();
- this._hidePopup();
+ _updateNotifications(notificationsChanged) {
+ let notifications = this._notifications;
+ if (!notifications || !notifications.length) {
+ if (notificationsChanged) {
+ this._clearAllNotifications();
+ this._hidePopup();
+ }
return;
}
if (window.fullScreen && FullScreen.navToolboxHidden) {
this._hidePopup();
return;
}
let doorhangers =
- this.notifications.filter(n => !n.dismissed && !n.options.badgeOnly);
+ notifications.filter(n => !n.dismissed && !n.options.badgeOnly);
if (this.panel.state == "showing" || this.panel.state == "open") {
// If the menu is already showing, then we need to dismiss all notifications
// since we don't want their doorhangers competing for attention
doorhangers.forEach(n => { n.dismissed = true; })
this._hidePopup();
this._clearBadge();
- if (!this.notifications[0].options.badgeOnly) {
- this._showBannerItem(this.notifications[0]);
+ if (!notifications[0].options.badgeOnly) {
+ this._showBannerItem(notifications[0]);
}
} else if (doorhangers.length > 0) {
// Only show the doorhanger if the window is focused and not fullscreen
if (window.fullScreen || Services.focus.activeWindow !== window) {
this._hidePopup();
this._showBadge(doorhangers[0]);
this._showBannerItem(doorhangers[0]);
} else {
this._clearBadge();
this._showNotificationPanel(doorhangers[0]);
}
} else {
this._hidePopup();
- this._showBadge(this.notifications[0]);
- this._showBannerItem(this.notifications[0]);
+ this._showBadge(notifications[0]);
+ this._showBannerItem(notifications[0]);
}
},
_showNotificationPanel(notification) {
this._refreshNotificationPanel(notification);
if (this.isNotificationPanelOpen) {
return;
@@ -859,89 +791,41 @@ const PanelUI = {
_clearBannerItem() {
if (this._panelBannerItem) {
this._panelBannerItem.notification = null;
this._panelBannerItem.hidden = true;
}
},
- _removeNotification(notification) {
- // This notification may already be removed, in which case let's just fail
- // silently.
- let notifications = this.notifications;
- if (!notifications)
- return;
-
- var index = notifications.indexOf(notification);
- if (index == -1)
- return;
-
- // Remove the notification
- notifications.splice(index, 1);
- },
-
_onNotificationButtonEvent(event, type) {
let notificationEl = getNotificationFromElement(event.originalTarget);
if (!notificationEl)
throw "PanelUI._onNotificationButtonEvent: couldn't find notification element";
if (!notificationEl.notification)
throw "PanelUI._onNotificationButtonEvent: couldn't find notification";
let notification = notificationEl.notification;
- let action = notification.mainAction;
-
if (type == "secondarybuttoncommand") {
- action = notification.secondaryActions[0];
+ AppMenuNotifications.callSecondaryAction(window, notification);
+ } else {
+ AppMenuNotifications.callMainAction(window, notification, true);
}
-
- let dismiss = true;
- if (action) {
- try {
- if (action === notification.mainAction) {
- action.callback(true);
- this._notify(notification.id, "main-action");
- } else {
- action.callback();
- }
- } catch (error) {
- Cu.reportError(error);
- }
-
- dismiss = action.dismiss;
- }
-
- if (dismiss) {
- notification.dismissed = true;
- this._notify(notification.id, "dismissed");
- } else {
- this._removeNotification(notification);
- }
- this._updateNotifications();
},
_onBannerItemSelected(event) {
let target = event.originalTarget;
if (!target.notification)
throw "menucommand target has no associated action/notification";
event.stopPropagation();
-
- try {
- target.notification.mainAction.callback(false);
- this._notify(target.notification.id, "main-action");
- } catch (error) {
- Cu.reportError(error);
- }
-
- this._removeNotification(target.notification);
- this._updateNotifications();
+ AppMenuNotifications.callMainAction(window, target.notification, false);
},
_getPopupId(notification) { return "appMenu-" + notification.id + "-notification"; },
_getBadgeStatus(notification) { return notification.id; },
_getPanelAnchor(candidate) {
let iconAnchor =
@@ -960,40 +844,28 @@ const PanelUI = {
let keyId = button.getAttribute("key");
let key = document.getElementById(keyId);
if (!key) {
continue;
}
button.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(key));
}
},
-
- _notify(status, topic) {
- Services.obs.notifyObservers(window, "panelUI-notification-" + topic, status);
- }
};
XPCOMUtils.defineConstant(this, "PanelUI", PanelUI);
/**
* Gets the currently selected locale for display.
* @return the selected locale
*/
function getLocale() {
return Services.locale.getAppLocaleAsLangTag();
}
-function PanelUINotification(id, mainAction, secondaryActions = [], options = {}) {
- this.id = id;
- this.mainAction = mainAction;
- this.secondaryActions = secondaryActions;
- this.options = options;
- this.dismissed = this.options.dismissed || false;
-}
-
function getNotificationFromElement(aElement) {
// Need to find the associated notification object, which is a bit tricky
// since it isn't associated with the element directly - this is kind of
// gross and very dependent on the structure of the popupnotification
// binding's content.
let notificationEl;
let parent = aElement;
while (parent && (parent = aElement.ownerDocument.getBindingParent(parent))) {
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -146,14 +146,16 @@ skip-if = os == "mac"
[browser_1096763_seen_widgets_post_reset.js]
[browser_1161838_inserted_new_default_buttons.js]
[browser_bootstrapped_custom_toolbar.js]
[browser_customizemode_contextmenu_menubuttonstate.js]
[browser_exit_background_customize_mode.js]
[browser_overflow_use_subviews.js]
[browser_panel_toggle.js]
[browser_panelUINotifications.js]
+[browser_panelUINotifications_fullscreen.js]
+[browser_panelUINotifications_multiWindow.js]
[browser_switch_to_customize_mode.js]
[browser_synced_tabs_menu.js]
[browser_check_tooltips_in_navbar.js]
[browser_editcontrols_update.js]
subsuite = clipboard
[browser_remote_tabs_button.js]
--- a/browser/components/customizableui/test/browser_panelUINotifications.js
+++ b/browser/components/customizableui/test/browser_panelUINotifications.js
@@ -1,10 +1,12 @@
"use strict";
+Cu.import("resource://gre/modules/AppMenuNotifications.jsm");
+
/**
* Tests that when we click on the main call-to-action of the doorhanger, the provided
* action is called, and the doorhanger removed.
*/
add_task(async function testMainActionCalled() {
let options = {
gBrowser: window.gBrowser,
url: "about:blank"
@@ -13,17 +15,17 @@ add_task(async function testMainActionCa
await BrowserTestUtils.withNewTab(options, function(browser) {
let doc = browser.ownerDocument;
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
let mainActionCalled = false;
let mainAction = {
callback: () => { mainActionCalled = true; }
};
- PanelUI.showNotification("update-manual", mainAction);
+ AppMenuNotifications.showNotification("update-manual", mainAction);
isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
let doorhanger = notifications[0];
is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
@@ -31,118 +33,16 @@ add_task(async function testMainActionCa
ok(mainActionCalled, "Main action callback was called");
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
});
});
/**
- * Tests that when we try to show a notification in a background window, it
- * does not display until the window comes back into the foreground. However,
- * it should display a badge.
- */
-add_task(async function testDoesNotShowDoorhangerForBackgroundWindow() {
- let options = {
- gBrowser: window.gBrowser,
- url: "about:blank"
- };
-
- await BrowserTestUtils.withNewTab(options, async function(browser) {
- let doc = browser.ownerDocument;
-
- let win = await BrowserTestUtils.openNewBrowserWindow();
- let mainActionCalled = false;
- let mainAction = {
- callback: () => { mainActionCalled = true; }
- };
- PanelUI.showNotification("update-manual", mainAction);
- is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
- is(PanelUI.menuButton.hasAttribute("badge-status"), true, "The background window has a badge.");
-
- await BrowserTestUtils.closeWindow(win);
- await SimpleTest.promiseFocus(window);
- isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
- let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
- is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
- let doorhanger = notifications[0];
- is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
-
- let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
- button.click();
-
- ok(mainActionCalled, "Main action callback was called");
- is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
- is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
- });
-});
-
-/**
- * Tests that when we try to show a notification in a background window and in
- * a foreground window, if the foreground window's main action is called, the
- * background window's doorhanger will be removed.
- */
-add_task(async function testBackgroundWindowNotificationsAreRemovedByForeground() {
- let options = {
- gBrowser: window.gBrowser,
- url: "about:blank"
- };
-
- await BrowserTestUtils.withNewTab(options, async function(browser) {
- let win = await BrowserTestUtils.openNewBrowserWindow();
- PanelUI.showNotification("update-manual", {callback() {}});
- win.PanelUI.showNotification("update-manual", {callback() {}});
- let doc = win.gBrowser.ownerDocument;
- let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
- let doorhanger = notifications[0];
- let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
- button.click();
-
- await BrowserTestUtils.closeWindow(win);
- await SimpleTest.promiseFocus(window);
-
- is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
- is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
- });
-});
-
-/**
- * Tests that when we try to show a notification in a background window and in
- * a foreground window, if the foreground window's doorhanger is dismissed,
- * the background window's doorhanger will also be dismissed once the window
- * regains focus.
- */
-add_task(async function testBackgroundWindowNotificationsAreDismissedByForeground() {
- let options = {
- gBrowser: window.gBrowser,
- url: "about:blank"
- };
-
- await BrowserTestUtils.withNewTab(options, async function(browser) {
- let win = await BrowserTestUtils.openNewBrowserWindow();
- PanelUI.showNotification("update-manual", {callback() {}});
- win.PanelUI.showNotification("update-manual", {callback() {}});
- let doc = win.gBrowser.ownerDocument;
- let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
- let doorhanger = notifications[0];
- let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
- button.click();
-
- await BrowserTestUtils.closeWindow(win);
- await SimpleTest.promiseFocus(window);
-
- is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
- is(PanelUI.menuButton.hasAttribute("badge-status"), true,
- "The dismissed notification should still have a badge status");
-
- PanelUI.removeNotification(/.*/);
- });
-});
-
-/**
* This tests that when we click the secondary action for a notification,
* it will display the badge for that notification on the PanelUI menu button.
* Once we click on this button, we should see an item in the menu which will
* call our main action.
*/
add_task(async function testSecondaryActionWorkflow() {
let options = {
gBrowser: window.gBrowser,
@@ -153,17 +53,17 @@ add_task(async function testSecondaryAct
let doc = browser.ownerDocument;
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
let mainActionCalled = false;
let mainAction = {
callback: () => { mainActionCalled = true; },
};
- PanelUI.showNotification("update-manual", mainAction);
+ AppMenuNotifications.showNotification("update-manual", mainAction);
isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
let doorhanger = notifications[0];
is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
@@ -181,39 +81,39 @@ add_task(async function testSecondaryAct
await PanelUI.hide();
is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is shown on PanelUI button.");
await PanelUI.show();
menuItem.click();
ok(mainActionCalled, "Main action callback was called");
- PanelUI.removeNotification(/.*/);
+ AppMenuNotifications.removeNotification(/.*/);
});
});
/**
* We want to ensure a few things with this:
* - Adding a doorhanger will make a badge disappear
* - once the notification for the doorhanger is resolved (removed, not just dismissed),
* then we display any other badges that are remaining.
*/
add_task(async function testInteractionWithBadges() {
await BrowserTestUtils.withNewTab("about:blank", async function(browser) {
let doc = browser.ownerDocument;
- PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+ AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
let mainActionCalled = false;
let mainAction = {
callback: () => { mainActionCalled = true; },
};
- PanelUI.showNotification("update-manual", mainAction);
+ AppMenuNotifications.showNotification("update-manual", mainAction);
isnot(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is hidden on PanelUI button.");
isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
let doorhanger = notifications[0];
is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
@@ -229,96 +129,96 @@ add_task(async function testInteractionW
let menuItem = PanelUI.mainView.querySelector(".panel-banner-item");
is(menuItem.label, menuItem.getAttribute("label-update-manual"), "Showing correct label");
is(menuItem.hidden, false, "update-manual menu item is showing.");
menuItem.click();
ok(mainActionCalled, "Main action callback was called");
is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
- PanelUI.removeNotification(/.*/);
+ AppMenuNotifications.removeNotification(/.*/);
is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
});
});
/**
* This tests that adding a badge will not dismiss any existing doorhangers.
*/
add_task(async function testAddingBadgeWhileDoorhangerIsShowing() {
await BrowserTestUtils.withNewTab("about:blank", function(browser) {
let doc = browser.ownerDocument;
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
let mainActionCalled = false;
let mainAction = {
callback: () => { mainActionCalled = true; }
};
- PanelUI.showNotification("update-manual", mainAction);
- PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+ AppMenuNotifications.showNotification("update-manual", mainAction);
+ AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
isnot(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is hidden on PanelUI button.");
isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
let doorhanger = notifications[0];
is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
mainActionButton.click();
ok(mainActionCalled, "Main action callback was called");
is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
- PanelUI.removeNotification(/.*/);
+ AppMenuNotifications.removeNotification(/.*/);
is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
});
});
/**
* Tests that badges operate like a stack.
*/
add_task(async function testMultipleBadges() {
await BrowserTestUtils.withNewTab("about:blank", async function(browser) {
let doc = browser.ownerDocument;
let menuButton = doc.getElementById("PanelUI-menu-button");
is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
is(menuButton.hasAttribute("badge"), false, "Should not have the badge attribute set");
- PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+ AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
- PanelUI.showBadgeOnlyNotification("update-succeeded");
+ AppMenuNotifications.showBadgeOnlyNotification("update-succeeded");
is(menuButton.getAttribute("badge-status"), "update-succeeded", "Should have update-succeeded badge status (update > fxa)");
- PanelUI.showBadgeOnlyNotification("update-failed");
+ AppMenuNotifications.showBadgeOnlyNotification("update-failed");
is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
- PanelUI.showBadgeOnlyNotification("download-severe");
+ AppMenuNotifications.showBadgeOnlyNotification("download-severe");
is(menuButton.getAttribute("badge-status"), "download-severe", "Should have download-severe badge status");
- PanelUI.showBadgeOnlyNotification("download-warning");
+ AppMenuNotifications.showBadgeOnlyNotification("download-warning");
is(menuButton.getAttribute("badge-status"), "download-warning", "Should have download-warning badge status");
- PanelUI.removeNotification(/^download-/);
+ AppMenuNotifications.removeNotification(/^download-/);
is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
- PanelUI.removeNotification(/^update-/);
+ AppMenuNotifications.removeNotification(/^update-/);
is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
- PanelUI.removeNotification(/^fxa-/);
+ AppMenuNotifications.removeNotification(/^fxa-/);
is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
await PanelUI.show();
is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status (Hamburger menu opened)");
PanelUI.hide();
- PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
- PanelUI.showBadgeOnlyNotification("update-succeeded");
- PanelUI.removeNotification(/.*/);
+ AppMenuNotifications.showBadgeOnlyNotification("fxa-needs-authentication");
+ AppMenuNotifications.showBadgeOnlyNotification("update-succeeded");
+ AppMenuNotifications.removeNotification(/.*/);
is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
});
});
/**
* Tests that non-badges also operate like a stack.
*/
add_task(async function testMultipleNonBadges() {
@@ -331,28 +231,28 @@ add_task(async function testMultipleNonB
called: false,
callback: () => { updateManualAction.called = true; },
};
let updateRestartAction = {
called: false,
callback: () => { updateRestartAction.called = true; },
};
- PanelUI.showNotification("update-manual", updateManualAction);
+ AppMenuNotifications.showNotification("update-manual", updateManualAction);
let notifications;
let doorhanger;
isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing.");
notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
doorhanger = notifications[0];
is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
- PanelUI.showNotification("update-restart", updateRestartAction);
+ AppMenuNotifications.showNotification("update-restart", updateRestartAction);
isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing.");
notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
doorhanger = notifications[0];
is(doorhanger.id, "appMenu-update-restart-notification", "PanelUI is displaying the update-restart notification.");
let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
@@ -377,47 +277,8 @@ add_task(async function testMultipleNonB
isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "update-manual badge is hidden on PanelUI button.");
is(menuItem.label, menuItem.getAttribute("label-update-manual"), "Showing correct label");
is(menuItem.hidden, false, "update-manual menu item is showing.");
menuItem.click();
ok(updateManualAction.called, "update-manual main action callback was called");
});
});
-
-add_task(async function testFullscreen() {
- let doc = document;
-
- is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
- let mainActionCalled = false;
- let mainAction = {
- callback: () => { mainActionCalled = true; }
- };
- PanelUI.showNotification("update-manual", mainAction);
-
- isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
- let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
- is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
- let doorhanger = notifications[0];
- is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
-
- let popuphiddenPromise = BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popuphidden");
- EventUtils.synthesizeKey("VK_F11", {});
- await popuphiddenPromise;
- await new Promise(executeSoon);
- is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
-
- window.FullScreen.showNavToolbox();
- is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
-
- let popupshownPromise = BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popupshown");
- EventUtils.synthesizeKey("VK_F11", {});
- await popupshownPromise;
- await new Promise(executeSoon);
- isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
- isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is not displaying on PanelUI button.");
-
- let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
- mainActionButton.click();
- ok(mainActionCalled, "Main action callback was called");
- is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
- is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
-});
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_panelUINotifications_fullscreen.js
@@ -0,0 +1,42 @@
+"use strict";
+
+Cu.import("resource://gre/modules/AppMenuNotifications.jsm");
+
+add_task(async function testFullscreen() {
+ let doc = document;
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+ let mainActionCalled = false;
+ let mainAction = {
+ callback: () => { mainActionCalled = true; }
+ };
+ AppMenuNotifications.showNotification("update-manual", mainAction);
+
+ isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+ let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
+ is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+ let doorhanger = notifications[0];
+ is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+ let popuphiddenPromise = BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popuphidden");
+ EventUtils.synthesizeKey("VK_F11", {});
+ await popuphiddenPromise;
+ await new Promise(executeSoon);
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+ FullScreen.showNavToolbox();
+ is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
+
+ let popupshownPromise = BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popupshown");
+ EventUtils.synthesizeKey("VK_F11", {});
+ await popupshownPromise;
+ await new Promise(executeSoon);
+ isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+ isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is not displaying on PanelUI button.");
+
+ let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+ mainActionButton.click();
+ ok(mainActionCalled, "Main action callback was called");
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+ is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_panelUINotifications_multiWindow.js
@@ -0,0 +1,130 @@
+"use strict";
+
+Cu.import("resource://gre/modules/AppMenuNotifications.jsm");
+
+/**
+ * Tests that when we try to show a notification in a background window, it
+ * does not display until the window comes back into the foreground. However,
+ * it should display a badge.
+ */
+add_task(async function testDoesNotShowDoorhangerForBackgroundWindow() {
+ let options = {
+ gBrowser: window.gBrowser,
+ url: "about:blank"
+ };
+
+ await BrowserTestUtils.withNewTab(options, async function(browser) {
+ let doc = browser.ownerDocument;
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let mainActionCalled = false;
+ let mainAction = {
+ callback: () => { mainActionCalled = true; }
+ };
+ AppMenuNotifications.showNotification("update-manual", mainAction);
+ is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
+ is(PanelUI.menuButton.hasAttribute("badge-status"), true, "The background window has a badge.");
+
+ await BrowserTestUtils.closeWindow(win);
+ await SimpleTest.promiseFocus(window);
+ isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+ let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
+ is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+ let doorhanger = notifications[0];
+ is(doorhanger.id, "appMenu-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+ let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+ button.click();
+
+ ok(mainActionCalled, "Main action callback was called");
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+ is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+ });
+});
+
+/**
+ * Tests that when we try to show a notification in a background window and in
+ * a foreground window, if the foreground window's main action is called, the
+ * background window's doorhanger will be removed.
+ */
+add_task(async function testBackgroundWindowNotificationsAreRemovedByForeground() {
+ let options = {
+ gBrowser: window.gBrowser,
+ url: "about:blank"
+ };
+
+ await BrowserTestUtils.withNewTab(options, async function(browser) {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ AppMenuNotifications.showNotification("update-manual", {callback() {}});
+ let doc = win.gBrowser.ownerDocument;
+ let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
+ is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+ let doorhanger = notifications[0];
+ let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+ button.click();
+
+ await BrowserTestUtils.closeWindow(win);
+ await SimpleTest.promiseFocus(window);
+
+ is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+ is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+ });
+});
+
+/**
+ * Tests that when we try to show a notification in a background window and in
+ * a foreground window, if the foreground window's doorhanger is dismissed,
+ * the background window's doorhanger will also be dismissed once the window
+ * regains focus.
+ */
+add_task(async function testBackgroundWindowNotificationsAreDismissedByForeground() {
+ let options = {
+ gBrowser: window.gBrowser,
+ url: "about:blank"
+ };
+
+ await BrowserTestUtils.withNewTab(options, async function(browser) {
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ AppMenuNotifications.showNotification("update-manual", {callback() {}});
+ let doc = win.gBrowser.ownerDocument;
+ let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
+ is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+ let doorhanger = notifications[0];
+ let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+ button.click();
+
+ await BrowserTestUtils.closeWindow(win);
+ await SimpleTest.promiseFocus(window);
+
+ is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
+ is(PanelUI.menuButton.hasAttribute("badge-status"), true,
+ "The dismissed notification should still have a badge status");
+
+ AppMenuNotifications.removeNotification(/.*/);
+ });
+});
+
+/**
+ * Tests that when we open a new window while a notification is showing, the
+ * notification also shows on the new window.
+ */
+add_task(async function testOpenWindowAfterShowingNotification() {
+ AppMenuNotifications.showNotification("update-manual", {callback() {}});
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let doc = win.gBrowser.ownerDocument;
+ let notifications = [...win.PanelUI.notificationPanel.children].filter(n => !n.hidden);
+ is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+ let doorhanger = notifications[0];
+ let button = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+ button.click();
+
+ await BrowserTestUtils.closeWindow(win);
+ await SimpleTest.promiseFocus(window);
+
+ is(PanelUI.notificationPanel.state, "closed", "The background window's doorhanger is closed.");
+ is(PanelUI.menuButton.hasAttribute("badge-status"), true,
+ "The dismissed notification should still have a badge status");
+
+ AppMenuNotifications.removeNotification(/.*/);
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/AppMenuNotifications.jsm
@@ -0,0 +1,179 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["AppMenuNotifications"];
+
+const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+function AppMenuNotification(id, mainAction, secondaryAction, options = {}) {
+ this.id = id;
+ this.mainAction = mainAction;
+ this.secondaryAction = secondaryAction;
+ this.options = options;
+ this.dismissed = this.options.dismissed || false;
+}
+
+var AppMenuNotifications = {
+ _notifications: [],
+ _hasInitialized: false,
+
+ get notifications() {
+ return Array.from(this._notifications);
+ },
+
+ _lazyInit() {
+ if (!this._hasInitialized) {
+ Services.obs.addObserver(this, "xpcom-shutdown");
+ Services.obs.addObserver(this, "appMenu-notifications-request");
+ }
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ Services.obs.removeObserver(this, "appMenu-notifications-request");
+ },
+
+ observe(subject, topic, status) {
+ switch (topic) {
+ case "xpcom-shutdown":
+ this.uninit();
+ break;
+ case "appMenu-notifications-request":
+ if (this._notifications.length != 0) {
+ Services.obs.notifyObservers(null, "appMenu-notifications", "init");
+ }
+ break;
+ }
+ },
+
+ get activeNotification() {
+ if (this._notifications.length > 0) {
+ const doorhanger =
+ this._notifications.find(n => !n.dismissed && !n.options.badgeOnly);
+ return doorhanger || this._notifications[0];
+ }
+
+ return null;
+ },
+
+ showNotification(id, mainAction, secondaryAction, options = {}) {
+ let notification = new AppMenuNotification(id, mainAction, secondaryAction, options);
+ let existingIndex = this._notifications.findIndex(n => n.id == id);
+ if (existingIndex != -1) {
+ this._notifications.splice(existingIndex, 1);
+ }
+
+ // We don't want to clobber doorhanger notifications just to show a badge,
+ // so don't dismiss any of them and the badge will show once the doorhanger
+ // gets resolved.
+ if (!options.badgeOnly && !options.dismissed) {
+ this._notifications.forEach(n => { n.dismissed = true; });
+ }
+
+ // Since notifications are generally somewhat pressing, the ideal case is that
+ // we never have two notifications at once. However, in the event that we do,
+ // it's more likely that the older notification has been sitting around for a
+ // bit, and so we don't want to hide the new notification behind it. Thus,
+ // we want our notifications to behave like a stack instead of a queue.
+ this._notifications.unshift(notification);
+
+ this._lazyInit();
+ this._updateNotifications();
+ return notification;
+ },
+
+ showBadgeOnlyNotification(id) {
+ return this.showNotification(id, null, null, { badgeOnly: true });
+ },
+
+ removeNotification(id) {
+ let notifications;
+ if (typeof id == "string") {
+ notifications = this._notifications.filter(n => n.id == id);
+ } else {
+ // If it's not a string, assume RegExp
+ notifications = this._notifications.filter(n => id.test(n.id));
+ }
+ // _updateNotifications can be expensive if it forces attachment of XBL
+ // bindings that haven't been used yet, so return early if we haven't found
+ // any notification to remove, as callers may expect this removeNotification
+ // method to be a no-op for non-existent notifications.
+ if (!notifications.length) {
+ return;
+ }
+
+ notifications.forEach(n => {
+ this._removeNotification(n);
+ });
+ this._updateNotifications();
+ },
+
+ dismissNotification(id) {
+ let notifications;
+ if (typeof id == "string") {
+ notifications = this._notifications.filter(n => n.id == id);
+ } else {
+ // If it's not a string, assume RegExp
+ notifications = this._notifications.filter(n => id.test(n.id));
+ }
+
+ notifications.forEach(n => {
+ n.dismissed = true;
+ });
+ this._updateNotifications();
+ },
+
+ callMainAction(win, notification, fromDoorhanger) {
+ let action = notification.mainAction;
+ this._callAction(win, notification, action, fromDoorhanger);
+ },
+
+ callSecondaryAction(win, notification) {
+ let action = notification.secondaryAction;
+ this._callAction(win, notification, action, true);
+ },
+
+ _callAction(win, notification, action, fromDoorhanger) {
+ let dismiss = true;
+ if (action) {
+ try {
+ action.callback(win, fromDoorhanger);
+ } catch (error) {
+ Cu.reportError(error);
+ }
+
+ dismiss = action.dismiss;
+ }
+
+ if (dismiss) {
+ notification.dismissed = true;
+ } else {
+ this._removeNotification(notification);
+ }
+
+ this._updateNotifications();
+ },
+
+ _removeNotification(notification) {
+ // This notification may already be removed, in which case let's just ignore.
+ let notifications = this._notifications;
+ if (!notifications)
+ return;
+
+ var index = notifications.indexOf(notification);
+ if (index == -1)
+ return;
+
+ // Remove the notification
+ notifications.splice(index, 1);
+ },
+
+ _updateNotifications() {
+ Services.obs.notifyObservers(null, "appMenu-notifications", "update");
+ },
+};
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -168,16 +168,17 @@ EXTRA_JS_MODULES += [
'addons/MatchPattern.jsm',
'addons/WebNavigation.jsm',
'addons/WebNavigationContent.js',
'addons/WebNavigationFrames.jsm',
'addons/WebRequest.jsm',
'addons/WebRequestCommon.jsm',
'addons/WebRequestContent.js',
'addons/WebRequestUpload.jsm',
+ 'AppMenuNotifications.jsm',
'AsyncPrefs.jsm',
'Battery.jsm',
'BinarySearch.jsm',
'BrowserUtils.jsm',
'CanonicalJSON.jsm',
'CertUtils.jsm',
'CharsetMenu.jsm',
'ClientID.jsm',