--- a/browser/base/content/browser-pageActions.js
+++ b/browser/base/content/browser-pageActions.js
@@ -182,40 +182,44 @@ var BrowserPageActions = {
* suitable anchor will be used.
* @param panelNode (DOM node, optional)
* The panel to use. This method takes a hands-off approach with
* regard to your panel in terms of attributes, styling, etc.
*/
togglePanelForAction(action, panelNode = null) {
let aaPanelNode = this.activatedActionPanelNode;
if (panelNode) {
+ // Note that this particular code path will not prevent the panel from
+ // opening later if PanelMultiView.showPopup was called but the panel has
+ // not been opened yet.
if (panelNode.state != "closed") {
- panelNode.hidePopup();
+ PanelMultiView.hidePopup(panelNode);
return;
}
if (aaPanelNode) {
- aaPanelNode.hidePopup();
+ PanelMultiView.hidePopup(aaPanelNode);
}
} else if (aaPanelNode) {
- aaPanelNode.hidePopup();
+ PanelMultiView.hidePopup(aaPanelNode);
return;
} else {
panelNode = this._makeActivatedActionPanelForAction(action);
}
// Hide the main panel before showing the action's panel.
- this.panelNode.hidePopup();
+ PanelMultiView.hidePopup(this.panelNode);
let anchorNode = this.panelAnchorNodeForAction(action);
anchorNode.setAttribute("open", "true");
panelNode.addEventListener("popuphiding", () => {
anchorNode.removeAttribute("open");
}, { once: true });
- panelNode.openPopup(anchorNode, "bottomcenter topright");
+ PanelMultiView.openPopup(panelNode, anchorNode, "bottomcenter topright")
+ .catch(Cu.reportError);
},
_makeActivatedActionPanelForAction(action) {
let panelNode = document.createElement("panel");
panelNode.id = this._activatedActionPanelID;
panelNode.classList.add("cui-widget-panel");
panelNode.setAttribute("actionID", action.id);
panelNode.setAttribute("role", "group");
@@ -549,17 +553,17 @@ var BrowserPageActions = {
buttonNode.closest("panel") == this.panelNode) {
let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false);
let panelViewNode = document.getElementById(panelViewNodeID);
action.subview.onShowing(panelViewNode);
this.multiViewNode.showSubView(panelViewNode, buttonNode);
return;
}
// Otherwise, hide the main popup in case it was open:
- this.panelNode.hidePopup();
+ PanelMultiView.hidePopup(this.panelNode);
// Toggle the activated action's panel if necessary
if (action.subview || action.wantsIframe) {
this.togglePanelForAction(action);
return;
}
// Otherwise, run the action.
@@ -689,22 +693,22 @@ var BrowserPageActions = {
event.keyCode != KeyEvent.DOM_VK_RETURN)) {
return;
}
// If the activated-action panel is open and anchored to the main button,
// close it.
let panelNode = this.activatedActionPanelNode;
if (panelNode && panelNode.anchorNode.id == this.mainButtonNode.id) {
- panelNode.hidePopup();
+ PanelMultiView.hidePopup(panelNode);
return;
}
if (this.panelNode.state == "open") {
- this.panelNode.hidePopup();
+ PanelMultiView.hidePopup(this.panelNode);
} else if (this.panelNode.state == "closed") {
this.showPanel(event);
}
},
/**
* Show the page action panel
*
@@ -718,20 +722,20 @@ var BrowserPageActions = {
action.onShowingInPanel(buttonNode);
}
this.panelNode.hidden = false;
this.panelNode.addEventListener("popuphiding", () => {
this.mainButtonNode.removeAttribute("open");
}, {once: true});
this.mainButtonNode.setAttribute("open", "true");
- this.panelNode.openPopup(this.mainButtonNode, {
+ PanelMultiView.openPopup(this.panelNode, this.mainButtonNode, {
position: "bottomcenter topright",
triggerEvent: event,
- });
+ }).catch(Cu.reportError);
},
/**
* Call this on the context menu's popupshowing event.
*
* @param event (DOM event, required)
* The popupshowing event.
* @param popup (DOM node, required)
@@ -876,20 +880,20 @@ var BrowserPageActionFeedback = {
return this.feedbackLabel = document.getElementById("pageActionFeedbackMessage");
},
show(action, event, textContentOverride) {
this.feedbackLabel.textContent = this.panelNode.getAttribute((textContentOverride || action.id) + "Feedback");
this.panelNode.hidden = false;
let anchor = BrowserPageActions.panelAnchorNodeForAction(action, event);
- this.panelNode.openPopup(anchor, {
+ PanelMultiView.openPopup(this.panelNode, anchor, {
position: "bottomcenter topright",
triggerEvent: event,
- });
+ }).catch(Cu.reportError);
this.panelNode.addEventListener("popupshown", () => {
this.feedbackAnimationBox.setAttribute("animate", "true");
// The timeout value used here allows the panel to stay open for
// 1 second after the text transition (duration=120ms) has finished.
setTimeout(() => {
this.panelNode.hidePopup(true);
@@ -907,47 +911,47 @@ var BrowserPageActionFeedback = {
// bookmark
BrowserPageActions.bookmark = {
onShowingInPanel(buttonNode) {
// Update the button label via the bookmark observer.
BookmarkingUI.updateBookmarkPageMenuItem();
},
onCommand(event, buttonNode) {
- BrowserPageActions.panelNode.hidePopup();
+ PanelMultiView.hidePopup(BrowserPageActions.panelNode);
BookmarkingUI.onStarCommand(event);
},
};
// copy URL
BrowserPageActions.copyURL = {
onPlacedInPanel(buttonNode) {
let action = PageActions.actionForID("copyURL");
BrowserPageActions.takeActionTitleFromPanel(action);
},
onCommand(event, buttonNode) {
- BrowserPageActions.panelNode.hidePopup();
+ PanelMultiView.hidePopup(BrowserPageActions.panelNode);
Cc["@mozilla.org/widget/clipboardhelper;1"]
.getService(Ci.nsIClipboardHelper)
.copyString(gURLBar.makeURIReadable(gBrowser.selectedBrowser.currentURI).displaySpec);
let action = PageActions.actionForID("copyURL");
BrowserPageActionFeedback.show(action, event);
},
};
// email link
BrowserPageActions.emailLink = {
onPlacedInPanel(buttonNode) {
let action = PageActions.actionForID("emailLink");
BrowserPageActions.takeActionTitleFromPanel(action);
},
onCommand(event, buttonNode) {
- BrowserPageActions.panelNode.hidePopup();
+ PanelMultiView.hidePopup(BrowserPageActions.panelNode);
MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser);
},
};
// send to device
BrowserPageActions.sendToDevice = {
onPlacedInPanel(buttonNode) {
let action = PageActions.actionForID("sendToDevice");
@@ -987,17 +991,17 @@ BrowserPageActions.sendToDevice = {
item.classList.add("pageAction-sendToDevice-device", "subviewbutton");
if (clientId) {
item.classList.add("subviewbutton-iconic");
item.setAttribute("tooltiptext", gSync.formatLastSyncDate(lastModified));
}
item.addEventListener("command", event => {
if (panelNode) {
- panelNode.hidePopup();
+ PanelMultiView.hidePopup(panelNode);
}
// There are items in the subview that don't represent devices: "Sign
// in", "Learn about Sync", etc. Device items will be .sendtab-target.
if (event.target.classList.contains("sendtab-target")) {
let action = PageActions.actionForID("sendToDevice");
let textOverride = gSync.offline && "sendToDeviceOffline";
BrowserPageActionFeedback.show(action, event, textOverride);
}
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -36,16 +36,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
FormValidationHandler: "resource:///modules/FormValidationHandler.jsm",
LanguagePrompt: "resource://gre/modules/LanguagePrompt.jsm",
LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
Log: "resource://gre/modules/Log.jsm",
LoginManagerParent: "resource://gre/modules/LoginManagerParent.jsm",
NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
PageActions: "resource:///modules/PageActions.jsm",
PageThumbs: "resource://gre/modules/PageThumbs.jsm",
+ PanelMultiView: "resource:///modules/PanelMultiView.jsm",
PanelView: "resource:///modules/PanelMultiView.jsm",
PluralForm: "resource://gre/modules/PluralForm.jsm",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
ProcessHangMonitor: "resource:///modules/ProcessHangMonitor.jsm",
PromiseUtils: "resource://gre/modules/PromiseUtils.jsm",
ReaderMode: "resource://gre/modules/ReaderMode.jsm",
ReaderParent: "resource:///modules/ReaderParent.jsm",
RecentWindow: "resource:///modules/RecentWindow.jsm",
@@ -7243,17 +7244,17 @@ var gIdentityHandler = {
/**
* Handler for mouseclicks on the "More Information" button in the
* "identity-popup" panel.
*/
handleMoreInfoClick(event) {
displaySecurityInfo();
event.stopPropagation();
- this._identityPopup.hidePopup();
+ PanelMultiView.hidePopup(this._identityPopup);
},
showSecuritySubView() {
this._identityPopupMultiView.showSubView("identity-popup-securityView",
this._popupExpander);
// Elements of hidden views have -moz-user-focus:ignore but setting that
// per CSS selector doesn't blur a focused element in those hidden views.
@@ -7265,36 +7266,36 @@ var gIdentityHandler = {
const kMIXED_CONTENT_UNBLOCK_EVENT = 2;
let histogram =
Services.telemetry.getHistogramById(
"MIXED_CONTENT_UNBLOCK_COUNTER");
histogram.add(kMIXED_CONTENT_UNBLOCK_EVENT);
// Reload the page with the content unblocked
BrowserReloadWithFlags(
Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT);
- this._identityPopup.hidePopup();
+ PanelMultiView.hidePopup(this._identityPopup);
},
enableMixedContentProtection() {
gBrowser.selectedBrowser.messageManager.sendAsyncMessage(
"MixedContent:ReenableProtection", {});
BrowserReload();
- this._identityPopup.hidePopup();
+ PanelMultiView.hidePopup(this._identityPopup);
},
removeCertException() {
if (!this._uriHasHost) {
Cu.reportError("Trying to revoke a cert exception on a URI without a host?");
return;
}
let host = this._uri.host;
let port = this._uri.port > 0 ? this._uri.port : 443;
this._overrideService.clearValidityOverride(host, port);
BrowserReloadSkipCache();
- this._identityPopup.hidePopup();
+ PanelMultiView.hidePopup(this._identityPopup);
},
/**
* Helper to parse out the important parts of _sslStatus (of the SSL cert in
* particular) for use in constructing identity UI strings
*/
getIdentityData() {
var result = {};
@@ -7350,17 +7351,17 @@ var gIdentityHandler = {
this._sslStatus.QueryInterface(Ci.nsISSLStatus);
}
// Then, update the user interface with the available data.
this.refreshIdentityBlock();
// Handle a location change while the Control Center is focused
// by closing the popup (bug 1207542)
if (shouldHidePopup) {
- this._identityPopup.hidePopup();
+ PanelMultiView.hidePopup(this._identityPopup);
}
// NOTE: We do NOT update the identity popup (the control center) when
// we receive a new security state on the existing page (i.e. from a
// subframe). If the user opened the popup and looks at the provided
// information we don't want to suddenly change the panel contents.
// Finally, if there are warnings to issue, issue them
@@ -7814,17 +7815,18 @@ var gIdentityHandler = {
// Update the popup strings
this.refreshIdentityPopup();
// Add the "open" attribute to the identity box for styling
this._identityBox.setAttribute("open", "true");
// Now open the popup, anchored off the primary chrome element
- this._identityPopup.openPopup(this._identityIcon, "bottomcenter topleft");
+ PanelMultiView.openPopup(this._identityPopup, this._identityIcon,
+ "bottomcenter topleft").catch(Cu.reportError);
},
onPopupShown(event) {
if (event.target == this._identityPopup) {
if (this._popupTriggeredByKeyboard) {
// Move focus to the next available element in the identity popup.
// This is required by role=alertdialog and fixes an issue where
// an already open panel would steal focus from the identity popup.
@@ -7847,17 +7849,17 @@ var gIdentityHandler = {
let position = elem.compareDocumentPosition(this._identityPopup);
if (!(position & (Node.DOCUMENT_POSITION_CONTAINS |
Node.DOCUMENT_POSITION_CONTAINED_BY)) &&
!this._identityPopup.hasAttribute("noautohide")) {
// Hide the panel when focusing an element that is
// neither an ancestor nor descendant unless the panel has
// @noautohide (e.g. for a tour).
- this._identityPopup.hidePopup();
+ PanelMultiView.hidePopup(this._identityPopup);
}
},
observe(subject, topic, data) {
if (topic == "perm-changed") {
this.refreshIdentityBlock();
}
},
--- a/browser/base/content/test/general/browser_addCertException.js
+++ b/browser/base/content/test/general/browser_addCertException.js
@@ -9,17 +9,20 @@
// using the button contained therein to load the certificate exception
// dialog, using that to add an exception, and finally successfully visiting
// the site, including showing the right identity box and control center icons.
add_task(async function() {
await BrowserTestUtils.openNewForegroundTab(gBrowser);
await loadBadCertPage("https://expired.example.com");
let { gIdentityHandler } = gBrowser.ownerGlobal;
+ let promisePanelOpen = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popupshown");
gIdentityHandler._identityBox.click();
+ await promisePanelOpen;
+
let promiseViewShown = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "ViewShown");
document.getElementById("identity-popup-security-expander").click();
await promiseViewShown;
is_element_visible(document.getElementById("connection-icon"), "Should see connection icon");
let connectionIconImage = gBrowser.ownerGlobal
.getComputedStyle(document.getElementById("connection-icon"))
.getPropertyValue("list-style-image");
--- a/browser/base/content/test/performance/browser_appmenu_reflows.js
+++ b/browser/base/content/test/performance/browser_appmenu_reflows.js
@@ -9,17 +9,17 @@
*
* See https://developer.mozilla.org/en-US/Firefox/Performance_best_practices_for_Firefox_fe_engineers
* for tips on how to do that.
*/
const EXPECTED_APPMENU_OPEN_REFLOWS = [
{
stack: [
"openPopup@chrome://global/content/bindings/popup.xml",
- "show/</<@chrome://browser/content/customizableui/panelUI.js",
+ "openPopup/this._openPopupPromise<@resource:///modules/PanelMultiView.jsm",
],
},
{
stack: [
"get_alignmentPosition@chrome://global/content/bindings/popup.xml",
"adjustArrowPosition@chrome://global/content/bindings/popup.xml",
"onxblpopuppositioned@chrome://global/content/bindings/popup.xml",
--- a/browser/base/content/test/siteIdentity/head.js
+++ b/browser/base/content/test/siteIdentity/head.js
@@ -199,17 +199,19 @@ async function assertMixedContentBlockin
}
if (passiveLoaded && activeBlocked) {
is(connectionIconImage, "url(\"chrome://browser/skin/connection-mixed-passive-loaded.svg\")",
"Using active blocked and passive loaded icon");
}
}
// Make sure the identity popup has the correct mixedcontent states
+ let promisePanelOpen = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popupshown");
gIdentityHandler._identityBox.click();
+ await promisePanelOpen;
let popupAttr = doc.getElementById("identity-popup").getAttribute("mixedcontent");
let bodyAttr = doc.getElementById("identity-popup-securityView-body").getAttribute("mixedcontent");
is(popupAttr.includes("active-loaded"), activeLoaded,
"identity-popup has expected attr for activeLoaded");
is(bodyAttr.includes("active-loaded"), activeLoaded,
"securityView-body has expected attr for activeLoaded");
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -11,16 +11,17 @@ const {classes: Cc, interfaces: Ci, util
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
SearchWidgetTracker: "resource:///modules/SearchWidgetTracker.jsm",
CustomizableWidgets: "resource:///modules/CustomizableWidgets.jsm",
DeferredTask: "resource://gre/modules/DeferredTask.jsm",
+ PanelMultiView: "resource:///modules/PanelMultiView.jsm",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
ShortcutUtils: "resource://gre/modules/ShortcutUtils.jsm",
LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
});
XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() {
const kUrl = "chrome://browser/locale/customizableui/customizableWidgets.properties";
return Services.strings.createBundle(kUrl);
@@ -1773,17 +1774,17 @@ var CustomizableUIInternal = {
return true;
}
return inInput || !inItem;
},
hidePanelForNode(aNode) {
let panel = this._getPanelForNode(aNode);
if (panel) {
- panel.hidePopup();
+ PanelMultiView.hidePopup(panel);
}
},
maybeAutoHidePanel(aEvent) {
if (aEvent.type == "keypress") {
if (aEvent.keyCode != aEvent.DOM_VK_RETURN) {
return;
}
@@ -4259,29 +4260,29 @@ OverflowableToolbar.prototype = {
break;
case "mousedown":
if (aEvent.button != 0) {
break;
}
if (aEvent.target == this._chevron) {
this._onClickChevron(aEvent);
} else {
- this._panel.hidePopup();
+ PanelMultiView.hidePopup(this._panel);
}
break;
case "customizationstarting":
this._disable();
break;
case "dragover":
if (this._enabled) {
this._showWithTimeout();
}
break;
case "dragend":
- this._panel.hidePopup();
+ PanelMultiView.hidePopup(this._panel);
break;
case "popuphiding":
this._onPanelHiding(aEvent);
break;
case "resize":
this._onResize(aEvent);
}
},
@@ -4297,31 +4298,33 @@ OverflowableToolbar.prototype = {
let mainViewId = multiview.getAttribute("mainViewId");
let mainView = doc.getElementById(mainViewId);
let contextMenu = doc.getElementById(mainView.getAttribute("context"));
gELS.addSystemEventListener(contextMenu, "command", this, true);
let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon");
// Ensure we update the gEditUIVisible flag when opening the popup, in
// case the edit controls are in it.
this._panel.addEventListener("popupshowing", () => doc.defaultView.updateEditUIVisibility(), {once: true});
- this._panel.openPopup(anchor || this._chevron, { triggerEvent: aEvent });
+ PanelMultiView.openPopup(this._panel, anchor || this._chevron, {
+ triggerEvent: aEvent,
+ }).catch(Cu.reportError);
this._chevron.open = true;
this._panel.addEventListener("popupshown", () => {
this._panel.addEventListener("dragover", this);
this._panel.addEventListener("dragend", this);
resolve();
}, {once: true});
});
},
_onClickChevron(aEvent) {
if (this._chevron.open) {
- this._panel.hidePopup();
this._chevron.open = false;
+ PanelMultiView.hidePopup(this._panel);
} else if (this._panel.state != "hiding" && !this._chevron.disabled) {
this.show(aEvent);
}
},
_onPanelHiding(aEvent) {
if (aEvent.target != this._panel) {
// Ignore context menus, <select> popups, etc.
@@ -4603,16 +4606,16 @@ OverflowableToolbar.prototype = {
_showWithTimeout() {
this.show().then(() => {
let window = this._toolbar.ownerGlobal;
if (this._hideTimeoutId) {
window.clearTimeout(this._hideTimeoutId);
}
this._hideTimeoutId = window.setTimeout(() => {
if (!this._panel.firstChild.matches(":hover")) {
- this._panel.hidePopup();
+ PanelMultiView.hidePopup(this._panel);
}
}, OVERFLOW_PANEL_HIDE_DELAY_MS);
});
},
};
CustomizableUIInternal.initialize();
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -7,19 +7,23 @@
* panel is opened may slide out to display a subview, which in turn may lead to
* other subviews in a cascade menu pattern.
*
* The <panel> element should contain a <panelmultiview> element. Views are
* declared using <panelview> elements that are usually children of the main
* <panelmultiview> element, although they don't need to be, as views can also
* be imported into the panel from other panels or popup sets.
*
- * The main view can be declared using the mainViewId attribute, and specific
- * subviews can slide in using the showSubView method. Backwards navigation can
- * be done using the goBack method or through a button in the subview headers.
+ * The panel should be opened asynchronously using the openPopup static method
+ * on the PanelMultiView object. This will display the view specified using the
+ * mainViewId attribute on the contained <panelmultiview> element.
+ *
+ * Specific subviews can slide in using the showSubView method, and backwards
+ * navigation can be done using the goBack method or through a button in the
+ * subview headers.
*
* This diagram shows how <panelview> nodes move during navigation:
*
* In this <panelmultiview> In other panels Action
* ┌───┬───┬───┐ ┌───┬───┐
* │(A)│ B │ C │ │ D │ E │ Open panel
* └───┴───┴───┘ └───┴───┘
* ┌───┬───┬───┐ ┌───┬───┐
@@ -44,16 +48,17 @@
this.EXPORTED_SYMBOLS = [
"PanelMultiView",
"PanelView",
];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
ChromeUtils.defineModuleGetter(this, "BrowserUtils",
"resource://gre/modules/BrowserUtils.jsm");
ChromeUtils.defineModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
const TRANSITION_PHASES = Object.freeze({
@@ -131,16 +136,51 @@ this.AssociatedToNode = class {
return event.defaultPrevented;
}
};
/**
* This is associated to <panelmultiview> elements by the panelUI.xml binding.
*/
this.PanelMultiView = class extends this.AssociatedToNode {
+ /**
+ * Tries to open the specified <panel> and displays the main view specified
+ * with the "mainViewId" attribute on the <panelmultiview> node it contains.
+ *
+ * If the panel does not contain a <panelmultiview>, it is opened directly.
+ * This allows consumers like page actions to accept different panel types.
+ *
+ * @see The non-static openPopup method for details.
+ */
+ static async openPopup(panelNode, ...args) {
+ let panelMultiViewNode = panelNode.querySelector("panelmultiview");
+ if (panelMultiViewNode) {
+ return this.forNode(panelMultiViewNode).openPopup(...args);
+ }
+ panelNode.openPopup(...args);
+ return true;
+ }
+
+ /**
+ * Closes the specified <panel> which contains a <panelmultiview> node.
+ *
+ * If the panel does not contain a <panelmultiview>, it is closed directly.
+ * This allows consumers like page actions to accept different panel types.
+ *
+ * @see The non-static hidePopup method for details.
+ */
+ static hidePopup(panelNode) {
+ let panelMultiViewNode = panelNode.querySelector("panelmultiview");
+ if (panelMultiViewNode) {
+ this.forNode(panelMultiViewNode).hidePopup();
+ } else {
+ panelNode.hidePopup();
+ }
+ }
+
get _panel() {
return this.node.parentNode;
}
get _mainViewId() {
return this.node.getAttribute("mainViewId");
}
get _mainView() {
@@ -191,17 +231,24 @@ this.PanelMultiView = class extends this
/**
* @return {Promise} showSubView() returns a promise, which is kept here for
* random access.
*/
get currentShowPromise() {
return this._currentShowPromise || Promise.resolve();
}
+ constructor(node) {
+ super(node);
+ this._openPopupPromise = Promise.resolve(false);
+ this._openPopupCancelCallback = () => {};
+ }
+
connect() {
+ this.connected = true;
this.knownViews = new Set(Array.from(
this.node.getElementsByTagName("panelview"),
node => PanelView.forNode(node)));
this.openViews = [];
this.__transitioning = false;
this.showingSubView = false;
const {document, window} = this;
@@ -261,21 +308,162 @@ this.PanelMultiView = class extends this
this._moveOutKids(this._viewStack);
this._panel.removeEventListener("mousemove", this);
this._panel.removeEventListener("popupshowing", this);
this._panel.removeEventListener("popuppositioned", this);
this._panel.removeEventListener("popupshown", this);
this._panel.removeEventListener("popuphidden", this);
this.window.removeEventListener("keydown", this);
- this.node = this._viewContainer = this._viewStack = this.__dwu =
+ this.node = this._openPopupPromise = this._openPopupCancelCallback =
+ this._viewContainer = this._viewStack = this.__dwu =
this._panelViewCache = this._transitionDetails = null;
}
/**
+ * Tries to open the panel associated with this PanelMultiView, and displays
+ * the main view specified with the "mainViewId" attribute.
+ *
+ * The hidePopup method can be called while the operation is in progress to
+ * prevent the panel from being displayed. View events may also cancel the
+ * operation, so there is no guarantee that the panel will become visible.
+ *
+ * The "popuphidden" event will be fired either when the operation is canceled
+ * or when the popup is closed later. This event can be used for example to
+ * reset the "open" state of the anchor or tear down temporary panels.
+ *
+ * If this method is called again before the panel is shown, the result
+ * depends on the operation currently in progress. If the operation was not
+ * canceled, the panel is opened using the arguments from the previous call,
+ * and this call is ignored. If the operation was canceled, it will be
+ * retried again using the arguments from this call.
+ *
+ * It's not necessary for the <panelmultiview> binding to be connected when
+ * this method is called, but the containing panel must have its display
+ * turned on, for example it shouldn't have the "hidden" attribute.
+ *
+ * @param args
+ * Arguments to be forwarded to the openPopup method of the panel.
+ *
+ * @resolves With true as soon as the request to display the panel has been
+ * sent, or with false if the operation was canceled. The state of
+ * the panel at this point is not guaranteed. It may be still
+ * showing, completely shown, or completely hidden.
+ * @rejects If an exception is thrown at any point in the process before the
+ * request to display the panel is sent.
+ */
+ async openPopup(...args) {
+ // Set up the function that allows hidePopup or a second call to showPopup
+ // to cancel the specific panel opening operation that we're starting below.
+ // This function must be synchronous, meaning we can't use Promise.race,
+ // because hidePopup wants to dispatch the "popuphidden" event synchronously
+ // even if the panel has not been opened yet.
+ let canCancel = true;
+ let cancelCallback = this._openPopupCancelCallback = () => {
+ // If the cancel callback is called and the panel hasn't been prepared
+ // yet, cancel showing it. Setting canCancel to false will prevent the
+ // popup from opening. If the panel has opened by the time the cancel
+ // callback is called, canCancel will be false already, and we will not
+ // fire the "popuphidden" event.
+ if (canCancel && this.node) {
+ canCancel = false;
+ this.dispatchCustomEvent("popuphidden");
+ }
+ };
+
+ // Create a promise that is resolved with the result of the last call to
+ // this method, where errors indicate that the panel was not opened.
+ let openPopupPromise = this._openPopupPromise.catch(() => {
+ return false;
+ });
+
+ // Make the preparation done before showing the panel non-reentrant. The
+ // promise created here will be resolved only after the panel preparation is
+ // completed, even if a cancellation request is received in the meantime.
+ return this._openPopupPromise = openPopupPromise.then(async wasShown => {
+ // The panel may have been destroyed in the meantime.
+ if (!this.node) {
+ return false;
+ }
+ // If the panel has been already opened there is nothing more to do. We
+ // check the actual state of the panel rather than setting some state in
+ // our handler of the "popuphidden" event because this has a lower chance
+ // of locking indefinitely if events aren't raised in the expected order.
+ if (wasShown && ["open", "showing"].includes(this._panel.state)) {
+ return true;
+ }
+ try {
+ // Most of the panel elements in the browser window have their display
+ // turned off for performance reasons, typically by setting the "hidden"
+ // attribute. If the caller has just turned on the display, the XBL
+ // binding for the <panelmultiview> element may still be disconnected.
+ // In this case, give the layout code a chance to run.
+ if (!this.connected) {
+ await BrowserUtils.promiseLayoutFlushed(this.document, "layout",
+ () => {});
+ // The XBL binding must be connected at this point. If this is not the
+ // case, the calling code should be updated to unhide the panel.
+ if (!this.connected) {
+ throw new Error("The binding for the panelmultiview element isn't" +
+ " connected. The containing panel may still have" +
+ " its display turned off by the hidden attribute.");
+ }
+ }
+ // (The rest of the asynchronous preparation goes here.)
+ } catch (ex) {
+ cancelCallback();
+ throw ex;
+ }
+ // If a cancellation request was received there is nothing more to do.
+ if (!canCancel || !this.node) {
+ return false;
+ }
+ // We have to set canCancel to false before opening the popup because the
+ // hidePopup method of PanelMultiView can be re-entered by event handlers.
+ // If the openPopup call fails, however, we still have to dispatch the
+ // "popuphidden" event even if canCancel was set to false.
+ try {
+ canCancel = false;
+ this._panel.openPopup(...args);
+ return true;
+ } catch (ex) {
+ this.dispatchCustomEvent("popuphidden");
+ throw ex;
+ }
+ });
+ }
+
+ /**
+ * Closes the panel associated with this PanelMultiView.
+ *
+ * If the openPopup method was called but the panel has not been displayed
+ * yet, the operation is canceled and the panel will not be displayed, but the
+ * "popuphidden" event is fired synchronously anyways.
+ *
+ * This means that by the time this method returns all the operations handled
+ * by the "popuphidden" event are completed, for example resetting the "open"
+ * state of the anchor, and the panel is already invisible.
+ */
+ hidePopup() {
+ if (!this.node) {
+ return;
+ }
+
+ // If we have already reached the _panel.openPopup call in the openPopup
+ // method, we can call hidePopup. Otherwise, we have to cancel the latest
+ // request to open the panel, which will have no effect if the request has
+ // been canceled already.
+ if (["open", "showing"].includes(this._panel.state)) {
+ this._panel.hidePopup();
+ } else {
+ this._openPopupCancelCallback();
+ }
+ }
+
+ /**
* Remove any child subviews into the panelViewCache, to ensure
* they remain usable even if this panelmultiview instance is removed
* from the DOM.
* @param viewNodeContainer the container from which to remove subviews
*/
_moveOutKids(viewNodeContainer) {
if (!this._panelViewCache)
return;
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -1,16 +1,18 @@
/* 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/. */
ChromeUtils.defineModuleGetter(this, "AppMenuNotifications",
"resource://gre/modules/AppMenuNotifications.jsm");
ChromeUtils.defineModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
+ChromeUtils.defineModuleGetter(this, "PanelMultiView",
+ "resource:///modules/PanelMultiView.jsm");
ChromeUtils.defineModuleGetter(this, "ScrollbarSampler",
"resource:///modules/ScrollbarSampler.jsm");
/**
* Maintains the state and dispatches events for the main menu panel.
*/
const PanelUI = {
@@ -210,32 +212,34 @@ const PanelUI = {
anchor = aEvent.target;
}
this.panel.addEventListener("popupshown", function() {
resolve();
}, {once: true});
anchor = this._getPanelAnchor(anchor);
- this.panel.openPopup(anchor, { triggerEvent: domEvent });
+ PanelMultiView.openPopup(this.panel, anchor, {
+ triggerEvent: domEvent,
+ }).catch(Cu.reportError);
}, (reason) => {
console.error("Error showing the PanelUI menu", reason);
});
});
},
/**
* If the menu panel is being shown, hide it.
*/
hide() {
if (document.documentElement.hasAttribute("customizing")) {
return;
}
- this.panel.hidePopup();
+ PanelMultiView.hidePopup(this.panel);
},
observe(subject, topic, status) {
switch (topic) {
case "fullscreen-nav-toolbox":
if (this._notifications) {
this._updateNotifications(false);
}
@@ -462,20 +466,20 @@ const PanelUI = {
}
let anchor = this._getPanelAnchor(aAnchor);
if (aAnchor != anchor && aAnchor.id) {
anchor.setAttribute("consumeanchor", aAnchor.id);
}
- tempPanel.openPopup(anchor, {
+ PanelMultiView.openPopup(tempPanel, anchor, {
position: "bottomcenter topright",
triggerEvent: domEvent,
- });
+ }).catch(Cu.reportError);
}
},
/**
* Sets up the event listener for when the Library view is shown.
*
* @param {panelview} viewNode The library view.
*/
@@ -615,19 +619,17 @@ const PanelUI = {
},
updateOverflowStatus() {
let hasKids = this.overflowFixedList.hasChildNodes();
if (hasKids && !this.navbar.hasAttribute("nonemptyoverflow")) {
this.navbar.setAttribute("nonemptyoverflow", "true");
this.overflowPanel.setAttribute("hasfixeditems", "true");
} else if (!hasKids && this.navbar.hasAttribute("nonemptyoverflow")) {
- if (this.overflowPanel.state != "closed") {
- this.overflowPanel.hidePopup();
- }
+ PanelMultiView.hidePopup(this.overflowPanel);
this.overflowPanel.removeAttribute("hasfixeditems");
this.navbar.removeAttribute("nonemptyoverflow");
}
},
onWidgetAfterDOMChange(aNode, aNextNode, aContainer, aWasRemoval) {
if (aContainer == this.overflowFixedList) {
this.updateOverflowStatus();
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -245,17 +245,17 @@ var DownloadsPanel = {
hidePanel() {
DownloadsCommon.log("Closing the downloads panel.");
if (!this.isPanelShowing) {
DownloadsCommon.log("Downloads panel is not showing - nothing to do.");
return;
}
- this.panel.hidePopup();
+ PanelMultiView.hidePopup(this.panel);
// Ensure that we allow the panel to be reopened. Note that, if the popup
// was open, then the onPopupHidden event handler has already updated the
// current state, otherwise we must update the state ourselves.
this._state = this.kStateHidden;
DownloadsCommon.log("Downloads panel is now closed.");
},
@@ -565,17 +565,18 @@ var DownloadsPanel = {
// still exist, and update the allowed items interactions accordingly. We
// do these checks on a background thread, and don't prevent the panel to
// be displayed while these checks are being performed.
for (let viewItem of DownloadsView._visibleViewItems.values()) {
viewItem.download.refresh().catch(Cu.reportError);
}
DownloadsCommon.log("Opening downloads panel popup.");
- this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, null);
+ PanelMultiView.openPopup(this.panel, anchor, "bottomcenter topright",
+ 0, 0, false, null).catch(Cu.reportError);
});
},
};
XPCOMUtils.defineConstant(this, "DownloadsPanel", DownloadsPanel);
// DownloadsOverlayLoader