summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/customizableui
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/customizableui')
-rw-r--r--comm/mail/components/customizableui/CustomizableUI.sys.mjs360
-rw-r--r--comm/mail/components/customizableui/PanelMultiView.sys.mjs1699
-rw-r--r--comm/mail/components/customizableui/content/customizeMode.inc.xhtml128
-rw-r--r--comm/mail/components/customizableui/content/jar.mn6
-rw-r--r--comm/mail/components/customizableui/content/moz.build7
-rw-r--r--comm/mail/components/customizableui/content/panelUI.inc.xhtml606
-rw-r--r--comm/mail/components/customizableui/content/panelUI.js882
-rw-r--r--comm/mail/components/customizableui/moz.build14
8 files changed, 3702 insertions, 0 deletions
diff --git a/comm/mail/components/customizableui/CustomizableUI.sys.mjs b/comm/mail/components/customizableui/CustomizableUI.sys.mjs
new file mode 100644
index 0000000000..2628bd6109
--- /dev/null
+++ b/comm/mail/components/customizableui/CustomizableUI.sys.mjs
@@ -0,0 +1,360 @@
+/* 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/. */
+
+// This file is a copy of a file with the same name in Firefox. Only the
+// pieces we're using, and a few pieces the devtools rely on such as the
+// constants, remain.
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
+});
+
+/**
+ * gPanelsForWindow is a list of known panels in a window which we may need to close
+ * should command events fire which target them.
+ */
+var gPanelsForWindow = new WeakMap();
+
+var CustomizableUIInternal = {
+ addPanelCloseListeners(aPanel) {
+ Services.els.addSystemEventListener(aPanel, "click", this, false);
+ Services.els.addSystemEventListener(aPanel, "keypress", this, false);
+ let win = aPanel.ownerGlobal;
+ if (!gPanelsForWindow.has(win)) {
+ gPanelsForWindow.set(win, new Set());
+ }
+ gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel));
+ },
+
+ removePanelCloseListeners(aPanel) {
+ Services.els.removeSystemEventListener(aPanel, "click", this, false);
+ Services.els.removeSystemEventListener(aPanel, "keypress", this, false);
+ let win = aPanel.ownerGlobal;
+ let panels = gPanelsForWindow.get(win);
+ if (panels) {
+ panels.delete(this._getPanelForNode(aPanel));
+ }
+ },
+
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "click":
+ case "keypress":
+ this.maybeAutoHidePanel(aEvent);
+ break;
+ }
+ },
+
+ _getPanelForNode(aNode) {
+ return aNode.closest("panel");
+ },
+
+ /*
+ * If people put things in the panel which need more than single-click interaction,
+ * we don't want to close it. Right now we check for text inputs and menu buttons.
+ * We also check for being outside of any toolbaritem/toolbarbutton, ie on a blank
+ * part of the menu.
+ */
+ _isOnInteractiveElement(aEvent) {
+ function getMenuPopupForDescendant(aNode) {
+ let lastPopup = null;
+ while (
+ aNode &&
+ aNode.parentNode &&
+ aNode.parentNode.localName.startsWith("menu")
+ ) {
+ lastPopup = aNode.localName == "menupopup" ? aNode : lastPopup;
+ aNode = aNode.parentNode;
+ }
+ return lastPopup;
+ }
+
+ let target = aEvent.target;
+ let panel = this._getPanelForNode(aEvent.currentTarget);
+ // This can happen in e.g. customize mode. If there's no panel,
+ // there's clearly nothing for us to close; pretend we're interactive.
+ if (!panel) {
+ return true;
+ }
+ // We keep track of:
+ // whether we're in an input container (text field)
+ let inInput = false;
+ // whether we're in a popup/context menu
+ let inMenu = false;
+ // whether we're in a toolbarbutton/toolbaritem
+ let inItem = false;
+ // whether the current menuitem has a valid closemenu attribute
+ let menuitemCloseMenu = "auto";
+
+ // While keeping track of that, we go from the original target back up,
+ // to the panel if we have to. We bail as soon as we find an input,
+ // a toolbarbutton/item, or the panel:
+ while (target) {
+ // Skip out of iframes etc:
+ if (target.nodeType == target.DOCUMENT_NODE) {
+ if (!target.defaultView) {
+ // Err, we're done.
+ break;
+ }
+ // Find containing browser or iframe element in the parent doc.
+ target = target.defaultView.docShell.chromeEventHandler;
+ if (!target) {
+ break;
+ }
+ }
+ let tagName = target.localName;
+ inInput = tagName == "input";
+ inItem = tagName == "toolbaritem" || tagName == "toolbarbutton";
+ let isMenuItem = tagName == "menuitem";
+ inMenu = inMenu || isMenuItem;
+
+ if (isMenuItem && target.hasAttribute("closemenu")) {
+ let closemenuVal = target.getAttribute("closemenu");
+ menuitemCloseMenu =
+ closemenuVal == "single" || closemenuVal == "none"
+ ? closemenuVal
+ : "auto";
+ }
+
+ // Keep the menu open and break out of the loop if the click happened on
+ // the ShadowRoot or a disabled menu item.
+ if (
+ target.nodeType == target.DOCUMENT_FRAGMENT_NODE ||
+ target.getAttribute("disabled") == "true"
+ ) {
+ return true;
+ }
+
+ // This isn't in the loop condition because we want to break before
+ // changing |target| if any of these conditions are true
+ if (inInput || inItem || target == panel) {
+ break;
+ }
+ // We need specific code for popups: the item on which they were invoked
+ // isn't necessarily in their parentNode chain:
+ if (isMenuItem) {
+ let topmostMenuPopup = getMenuPopupForDescendant(target);
+ target =
+ (topmostMenuPopup && topmostMenuPopup.triggerNode) ||
+ target.parentNode;
+ } else {
+ target = target.parentNode;
+ }
+ }
+
+ // If the user clicked a menu item...
+ if (inMenu) {
+ // We care if we're in an input also,
+ // or if the user specified closemenu!="auto":
+ if (inInput || menuitemCloseMenu != "auto") {
+ return true;
+ }
+ // Otherwise, we're probably fine to close the panel
+ return false;
+ }
+ // If we're not in a menu, and we *are* in a type="menu" toolbarbutton,
+ // we'll now interact with the menu
+ if (inItem && target.getAttribute("type") == "menu") {
+ return true;
+ }
+ return inInput || !inItem;
+ },
+
+ hidePanelForNode(aNode) {
+ let panel = this._getPanelForNode(aNode);
+ if (panel) {
+ lazy.PanelMultiView.hidePopup(panel);
+ }
+ },
+
+ maybeAutoHidePanel(aEvent) {
+ let eventType = aEvent.type;
+ if (eventType == "keypress" && aEvent.keyCode != aEvent.DOM_VK_RETURN) {
+ return;
+ }
+
+ if (eventType == "click" && aEvent.button != 0) {
+ return;
+ }
+
+ // We don't check preventDefault - it makes sense that this was prevented,
+ // but we probably still want to close the panel. If consumers don't want
+ // this to happen, they should specify the closemenu attribute.
+ if (eventType != "command" && this._isOnInteractiveElement(aEvent)) {
+ return;
+ }
+
+ // We can't use event.target because we might have passed an anonymous
+ // content boundary as well, and so target points to the outer element in
+ // that case. Unfortunately, this means we get anonymous child nodes instead
+ // of the real ones, so looking for the 'stoooop, don't close me' attributes
+ // is more involved.
+ let target = aEvent.originalTarget;
+ while (target.parentNode && target.localName != "panel") {
+ if (
+ target.getAttribute("closemenu") == "none" ||
+ target.getAttribute("widget-type") == "view" ||
+ target.getAttribute("widget-type") == "button-and-view"
+ ) {
+ return;
+ }
+ target = target.parentNode;
+ }
+
+ // If we get here, we can actually hide the popup:
+ this.hidePanelForNode(aEvent.target);
+ },
+};
+Object.freeze(CustomizableUIInternal);
+
+export var CustomizableUI = {
+ /**
+ * Constant reference to the ID of the navigation toolbar.
+ */
+ AREA_NAVBAR: "nav-bar",
+ /**
+ * Constant reference to the ID of the menubar's toolbar.
+ */
+ AREA_MENUBAR: "toolbar-menubar",
+ /**
+ * Constant reference to the ID of the tabstrip toolbar.
+ */
+ AREA_TABSTRIP: "TabsToolbar",
+ /**
+ * Constant reference to the ID of the bookmarks toolbar.
+ */
+ AREA_BOOKMARKS: "PersonalToolbar",
+ /**
+ * Constant reference to the ID of the non-dymanic (fixed) list in the overflow panel.
+ */
+ AREA_FIXED_OVERFLOW_PANEL: "widget-overflow-fixed-list",
+
+ /**
+ * Constant indicating the area is a menu panel.
+ */
+ TYPE_MENU_PANEL: "menu-panel",
+ /**
+ * Constant indicating the area is a toolbar.
+ */
+ TYPE_TOOLBAR: "toolbar",
+
+ /**
+ * Constant indicating a XUL-type provider.
+ */
+ PROVIDER_XUL: "xul",
+ /**
+ * Constant indicating an API-type provider.
+ */
+ PROVIDER_API: "api",
+ /**
+ * Constant indicating dynamic (special) widgets: spring, spacer, and separator.
+ */
+ PROVIDER_SPECIAL: "special",
+
+ /**
+ * Constant indicating the widget is built-in
+ */
+ SOURCE_BUILTIN: "builtin",
+ /**
+ * Constant indicating the widget is externally provided
+ * (e.g. by add-ons or other items not part of the builtin widget set).
+ */
+ SOURCE_EXTERNAL: "external",
+
+ /**
+ * Constant indicating the reason the event was fired was a window closing
+ */
+ REASON_WINDOW_CLOSED: "window-closed",
+ /**
+ * Constant indicating the reason the event was fired was an area being
+ * unregistered separately from window closing mechanics.
+ */
+ REASON_AREA_UNREGISTERED: "area-unregistered",
+
+ /**
+ * Add a widget to an area.
+ * If the area to which you try to add is not known to CustomizableUI,
+ * this will throw.
+ * If the area to which you try to add is the same as the area in which
+ * the widget is currently placed, this will do the same as
+ * moveWidgetWithinArea.
+ * If the widget cannot be removed from its original location, this will
+ * no-op.
+ *
+ * This will fire an onWidgetAdded notification,
+ * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification
+ * for each window CustomizableUI knows about.
+ *
+ * @param aWidgetId the ID of the widget to add
+ * @param aArea the ID of the area to add the widget to
+ * @param aPosition the position at which to add the widget. If you do not
+ * pass a position, the widget will be added to the end
+ * of the area.
+ */
+ addWidgetToArea(aWidgetId, aArea, aPosition) {},
+ /**
+ * Remove a widget from its area. If the widget cannot be removed from its
+ * area, or is not in any area, this will no-op. Otherwise, this will fire an
+ * onWidgetRemoved notification, and an onWidgetBeforeDOMChange and
+ * onWidgetAfterDOMChange notification for each window CustomizableUI knows
+ * about.
+ *
+ * @param aWidgetId the ID of the widget to remove
+ */
+ removeWidgetFromArea(aWidgetId) {},
+ /**
+ * Get the placement of a widget. This is by far the best way to obtain
+ * information about what the state of your widget is. The internals of
+ * this call are cheap (no DOM necessary) and you will know where the user
+ * has put your widget.
+ *
+ * @param aWidgetId the ID of the widget whose placement you want to know
+ * @returns
+ * {
+ * area: "somearea", // The ID of the area where the widget is placed
+ * position: 42 // the index in the placements array corresponding to
+ * // your widget.
+ * }
+ *
+ * OR
+ *
+ * null // if the widget is not placed anywhere (ie in the palette)
+ */
+ getPlacementOfWidget(aWidgetId, aOnlyRegistered = true, aDeadAreas = false) {
+ return null;
+ },
+ /**
+ * Add listeners to a panel that will close it. For use from the menu panel
+ * and overflowable toolbar implementations, unlikely to be useful for
+ * consumers.
+ *
+ * @param aPanel the panel to which listeners should be attached.
+ */
+ addPanelCloseListeners(aPanel) {
+ CustomizableUIInternal.addPanelCloseListeners(aPanel);
+ },
+ /**
+ * Remove close listeners that have been added to a panel with
+ * addPanelCloseListeners. For use from the menu panel and overflowable
+ * toolbar implementations, unlikely to be useful for consumers.
+ *
+ * @param aPanel the panel from which listeners should be removed.
+ */
+ removePanelCloseListeners(aPanel) {
+ CustomizableUIInternal.removePanelCloseListeners(aPanel);
+ },
+ /**
+ * Notify toolbox(es) of a particular event. If you don't pass aWindow,
+ * all toolboxes will be notified. For use from Customize Mode only,
+ * do not use otherwise.
+ *
+ * @param aEvent the name of the event to send.
+ * @param aDetails optional, the details of the event.
+ * @param aWindow optional, the window in which to send the event.
+ */
+ dispatchToolboxEvent(aEvent, aDetails = {}, aWindow = null) {},
+};
+Object.freeze(CustomizableUI);
diff --git a/comm/mail/components/customizableui/PanelMultiView.sys.mjs b/comm/mail/components/customizableui/PanelMultiView.sys.mjs
new file mode 100644
index 0000000000..c68f88c586
--- /dev/null
+++ b/comm/mail/components/customizableui/PanelMultiView.sys.mjs
@@ -0,0 +1,1699 @@
+/* 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/. */
+
+/**
+ * Allows a popup panel to host multiple subviews. The main view shown when the
+ * 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 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.
+ *
+ * The process of displaying the main view or a new subview requires multiple
+ * steps to be completed, hence at any given time the <panelview> element may
+ * be in different states:
+ *
+ * -- Open or closed
+ *
+ * All the <panelview> elements start "closed", meaning that they are not
+ * associated to a <panelmultiview> element and can be located anywhere in
+ * the document. When the openPopup or showSubView methods are called, the
+ * relevant view becomes "open" and the <panelview> element may be moved to
+ * ensure it is a descendant of the <panelmultiview> element.
+ *
+ * The "ViewShowing" event is fired at this point, when the view is not
+ * visible yet. The event is allowed to cancel the operation, in which case
+ * the view is closed immediately.
+ *
+ * Closing the view does not move the node back to its original position.
+ *
+ * -- Visible or invisible
+ *
+ * This indicates whether the view is visible in the document from a layout
+ * perspective, regardless of whether it is currently scrolled into view. In
+ * fact, all subviews are already visible before they start sliding in.
+ *
+ * Before scrolling into view, a view may become visible but be placed in a
+ * special off-screen area of the document where layout and measurements can
+ * take place asynchronously.
+ *
+ * When navigating forward, an open view may become invisible but stay open
+ * after sliding out of view. The last known size of these views is still
+ * taken into account for determining the overall panel size.
+ *
+ * When navigating backwards, an open subview will first become invisible and
+ * then will be closed.
+ *
+ * -- Active or inactive
+ *
+ * This indicates whether the view is fully scrolled into the visible area
+ * and ready to receive mouse and keyboard events. An active view is always
+ * visible, but a visible view may be inactive. For example, during a scroll
+ * transition, both views will be inactive.
+ *
+ * When a view becomes active, the ViewShown event is fired synchronously,
+ * and the showSubView and goBack methods can be called for navigation.
+ *
+ * For the main view of the panel, the ViewShown event is dispatched during
+ * the "popupshown" event, which means that other "popupshown" handlers may
+ * be called before the view is active. Thus, code that needs to perform
+ * further navigation automatically should either use the ViewShown event or
+ * wait for an event loop tick, like BrowserTestUtils.waitForEvent does.
+ *
+ * -- Navigating with the keyboard
+ *
+ * An open view may keep state related to keyboard navigation, even if it is
+ * invisible. When a view is closed, keyboard navigation state is cleared.
+ *
+ * This diagram shows how <panelview> nodes move during navigation:
+ *
+ * In this <panelmultiview> In other panels Action
+ * ┌───┬───┬───┐ ┌───┬───┐
+ * │(A)│ B │ C │ │ D │ E │ Open panel
+ * └───┴───┴───┘ └───┴───┘
+ * ┌───┬───┬───┐ ┌───┬───┐
+ * │{A}│(C)│ B │ │ D │ E │ Show subview C
+ * └───┴───┴───┘ └───┴───┘
+ * ┌───┬───┬───┬───┐ ┌───┐
+ * │{A}│{C}│(D)│ B │ │ E │ Show subview D
+ * └───┴───┴───┴───┘ └───┘
+ * │ ┌───┬───┬───┬───┐ ┌───┐
+ * │ │{A}│(C)│ D │ B │ │ E │ Go back
+ * │ └───┴───┴───┴───┘ └───┘
+ * │ │ │
+ * │ │ └── Currently visible view
+ * │ │ │
+ * └───┴───┴── Open views
+ */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyGetter(lazy, "gBundle", function () {
+ return Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+});
+
+/**
+ * Safety timeout after which asynchronous events will be canceled if any of the
+ * registered blockers does not return.
+ */
+const BLOCKERS_TIMEOUT_MS = 10000;
+
+const TRANSITION_PHASES = Object.freeze({
+ START: 1,
+ PREPARE: 2,
+ TRANSITION: 3,
+});
+
+let gNodeToObjectMap = new WeakMap();
+let gWindowsWithUnloadHandler = new WeakSet();
+
+/**
+ * Allows associating an object to a node lazily using a weak map.
+ *
+ * Classes deriving from this one may be easily converted to Custom Elements,
+ * although they would lose the ability of being associated lazily.
+ */
+var AssociatedToNode = class {
+ constructor(node) {
+ /**
+ * Node associated to this object.
+ */
+ this.node = node;
+
+ /**
+ * This promise is resolved when the current set of blockers set by event
+ * handlers have all been processed.
+ */
+ this._blockersPromise = Promise.resolve();
+ }
+
+ /**
+ * Retrieves the instance associated with the given node, constructing a new
+ * one if necessary. When the last reference to the node is released, the
+ * object instance will be garbage collected as well.
+ */
+ static forNode(node) {
+ let associatedToNode = gNodeToObjectMap.get(node);
+ if (!associatedToNode) {
+ associatedToNode = new this(node);
+ gNodeToObjectMap.set(node, associatedToNode);
+ }
+ return associatedToNode;
+ }
+
+ get document() {
+ return this.node.ownerDocument;
+ }
+
+ get window() {
+ return this.node.ownerGlobal;
+ }
+
+ _getBoundsWithoutFlushing(element) {
+ return this.window.windowUtils.getBoundsWithoutFlushing(element);
+ }
+
+ /**
+ * Dispatches a custom event on this element.
+ *
+ * @param {string} eventName Name of the event to dispatch.
+ * @param {object} [detail] Event detail object. Optional.
+ * @param {boolean} cancelable If the event can be canceled.
+ * @returns {boolean} `true` if the event was canceled by an event handler, `false`
+ * otherwise.
+ */
+ dispatchCustomEvent(eventName, detail, cancelable = false) {
+ let event = new this.window.CustomEvent(eventName, {
+ detail,
+ bubbles: true,
+ cancelable,
+ });
+ this.node.dispatchEvent(event);
+ return event.defaultPrevented;
+ }
+
+ /**
+ * Dispatches a custom event on this element and waits for any blocking
+ * promises registered using the "addBlocker" function on the details object.
+ * If this function is called again, the event is only dispatched after all
+ * the previously registered blockers have returned.
+ *
+ * The event can be canceled either by resolving any blocking promise to the
+ * boolean value "false" or by calling preventDefault on the event. Rejections
+ * and exceptions will be reported and will cancel the event.
+ *
+ * Blocking should be used sporadically because it slows down the interface.
+ * Also, non-reentrancy is not strictly guaranteed because a safety timeout of
+ * BLOCKERS_TIMEOUT_MS is implemented, after which the event will be canceled.
+ * This helps to prevent deadlocks if any of the event handlers does not
+ * resolve a blocker promise.
+ *
+ * @note Since there is no use case for dispatching different asynchronous
+ * events in parallel for the same element, this function will also wait
+ * for previous blockers when the event name is different.
+ *
+ * @param eventName
+ * Name of the custom event to dispatch.
+ *
+ * @resolves True if the event was canceled by a handler, false otherwise.
+ */
+ async dispatchAsyncEvent(eventName) {
+ // Wait for all the previous blockers before dispatching the event.
+ let blockersPromise = this._blockersPromise.catch(() => {});
+ return (this._blockersPromise = blockersPromise.then(async () => {
+ let blockers = new Set();
+ let cancel = this.dispatchCustomEvent(
+ eventName,
+ {
+ addBlocker(promise) {
+ // Any exception in the blocker will cancel the operation.
+ blockers.add(
+ promise.catch(ex => {
+ console.error(ex);
+ return true;
+ })
+ );
+ },
+ },
+ true
+ );
+ if (blockers.size) {
+ let timeoutPromise = new Promise((resolve, reject) => {
+ this.window.setTimeout(reject, BLOCKERS_TIMEOUT_MS);
+ });
+ try {
+ let results = await Promise.race([
+ Promise.all(blockers),
+ timeoutPromise,
+ ]);
+ cancel = cancel || results.some(result => result === false);
+ } catch (ex) {
+ console.error(
+ new Error(`One of the blockers for ${eventName} timed out.`)
+ );
+ return true;
+ }
+ }
+ return cancel;
+ }));
+ }
+};
+
+/**
+ * This is associated to <panelmultiview> elements.
+ */
+export class PanelMultiView extends 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();
+ }
+ }
+
+ /**
+ * Removes the specified <panel> from the document, ensuring that any
+ * <panelmultiview> node it contains is destroyed properly.
+ *
+ * If the viewCacheId attribute is present on the <panelmultiview> element,
+ * imported subviews will be moved out again to the element it specifies, so
+ * that the panel element can be removed safely.
+ *
+ * If the panel does not contain a <panelmultiview>, it is removed directly.
+ * This allows consumers like page actions to accept different panel types.
+ */
+ static removePopup(panelNode) {
+ try {
+ let panelMultiViewNode = panelNode.querySelector("panelmultiview");
+ if (panelMultiViewNode) {
+ let panelMultiView = this.forNode(panelMultiViewNode);
+ panelMultiView._moveOutKids();
+ panelMultiView.disconnect();
+ }
+ } finally {
+ // Make sure to remove the panel element even if disconnecting fails.
+ panelNode.remove();
+ }
+ }
+
+ /**
+ * Ensures that when the specified window is closed all the <panelmultiview>
+ * node it contains are destroyed properly.
+ */
+ static ensureUnloadHandlerRegistered(window) {
+ if (gWindowsWithUnloadHandler.has(window)) {
+ return;
+ }
+
+ window.addEventListener(
+ "unload",
+ () => {
+ for (let panelMultiViewNode of window.document.querySelectorAll(
+ "panelmultiview"
+ )) {
+ this.forNode(panelMultiViewNode).disconnect();
+ }
+ },
+ { once: true }
+ );
+
+ gWindowsWithUnloadHandler.add(window);
+ }
+
+ get _panel() {
+ return this.node.parentNode;
+ }
+
+ set _transitioning(val) {
+ if (val) {
+ this.node.setAttribute("transitioning", "true");
+ } else {
+ this.node.removeAttribute("transitioning");
+ }
+ }
+
+ get _screenManager() {
+ if (this.__screenManager) {
+ return this.__screenManager;
+ }
+ return (this.__screenManager = Cc[
+ "@mozilla.org/gfx/screenmanager;1"
+ ].getService(Ci.nsIScreenManager));
+ }
+
+ constructor(node) {
+ super(node);
+ this._openPopupPromise = Promise.resolve(false);
+ this._openPopupCancelCallback = () => {};
+ }
+
+ connect() {
+ this.connected = true;
+
+ PanelMultiView.ensureUnloadHandlerRegistered(this.window);
+
+ let viewContainer = (this._viewContainer =
+ this.document.createXULElement("box"));
+ viewContainer.classList.add("panel-viewcontainer");
+
+ let viewStack = (this._viewStack = this.document.createXULElement("box"));
+ viewStack.classList.add("panel-viewstack");
+ viewContainer.append(viewStack);
+
+ let offscreenViewContainer = this.document.createXULElement("box");
+ offscreenViewContainer.classList.add("panel-viewcontainer", "offscreen");
+
+ let offscreenViewStack = (this._offscreenViewStack =
+ this.document.createXULElement("box"));
+ offscreenViewStack.classList.add("panel-viewstack");
+ offscreenViewContainer.append(offscreenViewStack);
+
+ this.node.prepend(offscreenViewContainer);
+ this.node.prepend(viewContainer);
+
+ this.openViews = [];
+
+ this._panel.addEventListener("popupshowing", this);
+ this._panel.addEventListener("popuppositioned", this);
+ this._panel.addEventListener("popuphidden", this);
+ this._panel.addEventListener("popupshown", this);
+
+ // Proxy these public properties and methods, as used elsewhere by various
+ // parts of the browser, to this instance.
+ ["goBack", "showSubView"].forEach(method => {
+ Object.defineProperty(this.node, method, {
+ enumerable: true,
+ value: (...args) => this[method](...args),
+ });
+ });
+ }
+
+ disconnect() {
+ // Guard against re-entrancy.
+ if (!this.node || !this.connected) {
+ return;
+ }
+
+ 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, true);
+ this.node =
+ this._openPopupPromise =
+ this._openPopupCancelCallback =
+ this._viewContainer =
+ this._viewStack =
+ 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 anchor
+ * The node to anchor the popup to.
+ * @param options
+ * Either options to use or a string position. This is forwarded to
+ * the openPopup method of the panel.
+ * @param args
+ * Additional 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(anchor, options, ...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 {
+ if (!this.connected) {
+ this.connect();
+ }
+ // Allow any of the ViewShowing handlers to prevent showing the main view.
+ if (!(await this._showMainView())) {
+ cancelCallback();
+ }
+ } 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(anchor, options, ...args);
+
+ // On Windows, if another popup is hiding while we call openPopup, the
+ // call won't fail but the popup won't open. In this case, we have to
+ // dispatch an artificial "popuphidden" event to reset our state.
+ if (this._panel.state == "closed" && this.openViews.length) {
+ this.dispatchCustomEvent("popuphidden");
+ return false;
+ }
+
+ if (
+ options &&
+ typeof options == "object" &&
+ options.triggerEvent &&
+ options.triggerEvent.type == "keypress" &&
+ this.openViews.length
+ ) {
+ // This was opened via the keyboard, so focus the first item.
+ this.openViews[0].focusWhenActive = true;
+ }
+
+ 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 || !this.connected) {
+ 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();
+ }
+
+ // We close all the views synchronously, so that they are ready to be opened
+ // in other PanelMultiView instances. The "popuphidden" handler may also
+ // call this function, but the second time openViews will be empty.
+ this.closeAllViews();
+ }
+
+ /**
+ * Move any child subviews into the element defined by "viewCacheId" to make
+ * sure they will not be removed together with the <panelmultiview> element.
+ */
+ _moveOutKids() {
+ let viewCacheId = this.node.getAttribute("viewCacheId");
+ if (!viewCacheId) {
+ return;
+ }
+
+ // Node.children and Node.children is live to DOM changes like the
+ // ones we're about to do, so iterate over a static copy:
+ let subviews = Array.from(this._viewStack.children);
+ let viewCache = this.document.getElementById(viewCacheId);
+ for (let subview of subviews) {
+ viewCache.appendChild(subview);
+ }
+ }
+
+ /**
+ * Slides in the specified view as a subview.
+ *
+ * @param viewIdOrNode
+ * DOM element or string ID of the <panelview> to display.
+ * @param anchor
+ * DOM element that triggered the subview, which will be highlighted
+ * and whose "label" attribute will be used for the title of the
+ * subview when a "title" attribute is not specified.
+ */
+ showSubView(viewIdOrNode, anchor) {
+ this._showSubView(viewIdOrNode, anchor).catch(console.error);
+ }
+ async _showSubView(viewIdOrNode, anchor) {
+ let viewNode =
+ typeof viewIdOrNode == "string"
+ ? this.document.getElementById(viewIdOrNode)
+ : viewIdOrNode;
+ if (!viewNode) {
+ console.error(new Error(`Subview ${viewIdOrNode} doesn't exist.`));
+ return;
+ }
+
+ if (!this.openViews.length) {
+ console.error(new Error(`Cannot show a subview in a closed panel.`));
+ return;
+ }
+
+ let prevPanelView = this.openViews[this.openViews.length - 1];
+ let nextPanelView = PanelView.forNode(viewNode);
+ if (this.openViews.includes(nextPanelView)) {
+ console.error(new Error(`Subview ${viewNode.id} is already open.`));
+ return;
+ }
+
+ // Do not re-enter the process if navigation is already in progress. Since
+ // there is only one active view at any given time, we can do this check
+ // safely, even considering that during the navigation process the actual
+ // view to which prevPanelView refers will change.
+ if (!prevPanelView.active) {
+ return;
+ }
+ // If prevPanelView._doingKeyboardActivation is true, it will be reset to
+ // false synchronously. Therefore, we must capture it before we use any
+ // "await" statements.
+ let doingKeyboardActivation = prevPanelView._doingKeyboardActivation;
+ // Marking the view that is about to scrolled out of the visible area as
+ // inactive will prevent re-entrancy and also disable keyboard navigation.
+ // From this point onwards, "await" statements can be used safely.
+ prevPanelView.active = false;
+
+ // Provide visual feedback while navigation is in progress, starting before
+ // the transition starts and ending when the previous view is invisible.
+ if (anchor) {
+ anchor.setAttribute("open", "true");
+ }
+ try {
+ // If the ViewShowing event cancels the operation we have to re-enable
+ // keyboard navigation, but this must be avoided if the panel was closed.
+ if (!(await this._openView(nextPanelView))) {
+ if (prevPanelView.isOpenIn(this)) {
+ // We don't raise a ViewShown event because nothing actually changed.
+ // Technically we should use a different state flag just because there
+ // is code that could check the "active" property to determine whether
+ // to wait for a ViewShown event later, but this only happens in
+ // regression tests and is less likely to be a technique used in
+ // production code, where use of ViewShown is less common.
+ prevPanelView.active = true;
+ }
+ return;
+ }
+
+ prevPanelView.captureKnownSize();
+
+ // The main view of a panel can be a subview in another one. Make sure to
+ // reset all the properties that may be set on a subview.
+ nextPanelView.mainview = false;
+ // The header may change based on how the subview was opened.
+ nextPanelView.headerText =
+ viewNode.getAttribute("title") ||
+ (anchor && anchor.getAttribute("label"));
+ // The constrained width of subviews may also vary between panels.
+ nextPanelView.minMaxWidth = prevPanelView.knownWidth;
+
+ if (anchor) {
+ viewNode.classList.add("PanelUI-subView");
+ }
+
+ await this._transitionViews(prevPanelView.node, viewNode, false, anchor);
+ } finally {
+ if (anchor) {
+ anchor.removeAttribute("open");
+ }
+ }
+
+ nextPanelView.focusWhenActive = doingKeyboardActivation;
+ this._activateView(nextPanelView);
+ }
+
+ /**
+ * Navigates backwards by sliding out the most recent subview.
+ */
+ goBack() {
+ this._goBack().catch(console.error);
+ }
+ async _goBack() {
+ if (this.openViews.length < 2) {
+ // This may be called by keyboard navigation or external code when only
+ // the main view is open.
+ return;
+ }
+
+ let prevPanelView = this.openViews[this.openViews.length - 1];
+ let nextPanelView = this.openViews[this.openViews.length - 2];
+
+ // Like in the showSubView method, do not re-enter navigation while it is
+ // in progress, and make the view inactive immediately. From this point
+ // onwards, "await" statements can be used safely.
+ if (!prevPanelView.active) {
+ return;
+ }
+
+ prevPanelView.active = false;
+
+ prevPanelView.captureKnownSize();
+
+ await this._transitionViews(prevPanelView.node, nextPanelView.node, true);
+
+ this._closeLatestView();
+
+ this._activateView(nextPanelView);
+ }
+
+ /**
+ * Prepares the main view before showing the panel.
+ */
+ async _showMainView() {
+ let nextPanelView = PanelView.forNode(
+ this.document.getElementById(this.node.getAttribute("mainViewId"))
+ );
+
+ // If the view is already open in another panel, close the panel first.
+ let oldPanelMultiViewNode = nextPanelView.node.panelMultiView;
+ if (oldPanelMultiViewNode) {
+ PanelMultiView.forNode(oldPanelMultiViewNode).hidePopup();
+ // Wait for a layout flush after hiding the popup, otherwise the view may
+ // not be displayed correctly for some time after the new panel is opened.
+ // This is filed as bug 1441015.
+ await this.window.promiseDocumentFlushed(() => {});
+ }
+
+ if (!(await this._openView(nextPanelView))) {
+ return false;
+ }
+
+ // The main view of a panel can be a subview in another one. Make sure to
+ // reset all the properties that may be set on a subview.
+ nextPanelView.mainview = true;
+ nextPanelView.headerText = "";
+ nextPanelView.minMaxWidth = 0;
+
+ // Ensure the view will be visible once the panel is opened.
+ nextPanelView.visible = true;
+
+ return true;
+ }
+
+ /**
+ * Opens the specified PanelView and dispatches the ViewShowing event, which
+ * can be used to populate the subview or cancel the operation.
+ *
+ * This also clears all the attributes and styles that may be left by a
+ * transition that was interrupted.
+ *
+ * @resolves With true if the view was opened, false otherwise.
+ */
+ async _openView(panelView) {
+ if (panelView.node.parentNode != this._viewStack) {
+ this._viewStack.appendChild(panelView.node);
+ }
+
+ panelView.node.panelMultiView = this.node;
+ this.openViews.push(panelView);
+
+ let canceled = await panelView.dispatchAsyncEvent("ViewShowing");
+
+ // The panel can be hidden while we are processing the ViewShowing event.
+ // This results in all the views being closed synchronously, and at this
+ // point the ViewHiding event has already been dispatched for all of them.
+ if (!this.openViews.length) {
+ return false;
+ }
+
+ // Check if the event requested cancellation but the panel is still open.
+ if (canceled) {
+ // Handlers for ViewShowing can't know if a different handler requested
+ // cancellation, so this will dispatch a ViewHiding event to give a chance
+ // to clean up.
+ this._closeLatestView();
+ return false;
+ }
+
+ // Clean up all the attributes and styles related to transitions. We do this
+ // here rather than when the view is closed because we are likely to make
+ // other DOM modifications soon, which isn't the case when closing.
+ let { style } = panelView.node;
+ style.removeProperty("outline");
+ style.removeProperty("width");
+
+ return true;
+ }
+
+ /**
+ * Activates the specified view and raises the ViewShown event, unless the
+ * view was closed in the meantime.
+ */
+ _activateView(panelView) {
+ if (panelView.isOpenIn(this)) {
+ panelView.active = true;
+ if (panelView.focusWhenActive) {
+ panelView.focusFirstNavigableElement(false, true);
+ panelView.focusWhenActive = false;
+ }
+ panelView.dispatchCustomEvent("ViewShown");
+ }
+ }
+
+ /**
+ * Closes the most recent PanelView and raises the ViewHiding event.
+ *
+ * @note The ViewHiding event is not cancelable and should probably be renamed
+ * to ViewHidden or ViewClosed instead, see bug 1438507.
+ */
+ _closeLatestView() {
+ let panelView = this.openViews.pop();
+ panelView.clearNavigation();
+ panelView.dispatchCustomEvent("ViewHiding");
+ panelView.node.panelMultiView = null;
+ // Views become invisible synchronously when they are closed, and they won't
+ // become visible again until they are opened. When this is called at the
+ // end of backwards navigation, the view is already invisible.
+ panelView.visible = false;
+ }
+
+ /**
+ * Closes all the views that are currently open.
+ */
+ closeAllViews() {
+ // Raise ViewHiding events for open views in reverse order.
+ while (this.openViews.length) {
+ this._closeLatestView();
+ }
+ }
+
+ /**
+ * Apply a transition to 'slide' from the currently active view to the next
+ * one.
+ * Sliding the next subview in means that the previous panelview stays where it
+ * is and the active panelview slides in from the left in LTR mode, right in
+ * RTL mode.
+ *
+ * @param {panelview} previousViewNode Node that is currently displayed, but
+ * is about to be transitioned away. This
+ * must be already inactive at this point.
+ * @param {panelview} viewNode - Node that will becode the active view,
+ * after the transition has finished.
+ * @param {boolean} reverse Whether we're navigation back to a
+ * previous view or forward to a next view.
+ */
+ async _transitionViews(previousViewNode, viewNode, reverse) {
+ const { window } = this;
+
+ let nextPanelView = PanelView.forNode(viewNode);
+ let prevPanelView = PanelView.forNode(previousViewNode);
+
+ let details = (this._transitionDetails = {
+ phase: TRANSITION_PHASES.START,
+ });
+
+ // Set the viewContainer dimensions to make sure only the current view is
+ // visible.
+ let olderView = reverse ? nextPanelView : prevPanelView;
+ this._viewContainer.style.minHeight = olderView.knownHeight + "px";
+ this._viewContainer.style.height = prevPanelView.knownHeight + "px";
+ this._viewContainer.style.width = prevPanelView.knownWidth + "px";
+ // Lock the dimensions of the window that hosts the popup panel.
+ let rect = this._getBoundsWithoutFlushing(this._panel);
+ this._panel.style.width = rect.width + "px";
+ this._panel.style.height = rect.height + "px";
+
+ let viewRect;
+ if (reverse) {
+ // Use the cached size when going back to a previous view, but not when
+ // reopening a subview, because its contents may have changed.
+ viewRect = {
+ width: nextPanelView.knownWidth,
+ height: nextPanelView.knownHeight,
+ };
+ nextPanelView.visible = true;
+ } else if (viewNode.customRectGetter) {
+ // We use a customRectGetter for WebExtensions panels, because they need
+ // to query the size from an embedded browser. The presence of this
+ // getter also provides an indication that the view node shouldn't be
+ // moved around, otherwise the state of the browser would get disrupted.
+ let width = prevPanelView.knownWidth;
+ let height = prevPanelView.knownHeight;
+ viewRect = Object.assign({ height, width }, viewNode.customRectGetter());
+ nextPanelView.visible = true;
+ // Until the header is visible, it has 0 height.
+ // Wait for layout before measuring it
+ let header = viewNode.firstElementChild;
+ if (header && header.classList.contains("panel-header")) {
+ viewRect.height += await window.promiseDocumentFlushed(() => {
+ return this._getBoundsWithoutFlushing(header).height;
+ });
+ }
+ } else {
+ this._offscreenViewStack.style.minHeight = olderView.knownHeight + "px";
+ this._offscreenViewStack.appendChild(viewNode);
+ nextPanelView.visible = true;
+
+ viewRect = await window.promiseDocumentFlushed(() => {
+ return this._getBoundsWithoutFlushing(viewNode);
+ });
+ // Bail out if the panel was closed in the meantime.
+ if (!nextPanelView.isOpenIn(this)) {
+ return;
+ }
+
+ // Place back the view after all the other views that are already open in
+ // order for the transition to work as expected.
+ this._viewStack.appendChild(viewNode);
+
+ this._offscreenViewStack.style.removeProperty("min-height");
+ }
+
+ this._transitioning = true;
+ details.phase = TRANSITION_PHASES.PREPARE;
+
+ // The 'magic' part: build up the amount of pixels to move right or left.
+ let moveToLeft =
+ (this.window.RTL_UI && !reverse) || (!this.window.RTL_UI && reverse);
+ let deltaX = prevPanelView.knownWidth;
+ let deepestNode = reverse ? previousViewNode : viewNode;
+
+ // With a transition when navigating backwards - user hits the 'back'
+ // button - we need to make sure that the views are positioned in a way
+ // that a translateX() unveils the previous view from the right direction.
+ if (reverse) {
+ this._viewStack.style.marginInlineStart = "-" + deltaX + "px";
+ }
+
+ // Set the transition style and listen for its end to clean up and make sure
+ // the box sizing becomes dynamic again.
+ // Somehow, putting these properties in PanelUI.css doesn't work for newly
+ // shown nodes in a XUL parent node.
+ this._viewStack.style.transition =
+ "transform var(--animation-easing-function)" +
+ " var(--panelui-subview-transition-duration)";
+ this._viewStack.style.willChange = "transform";
+ // Use an outline instead of a border so that the size is not affected.
+ deepestNode.style.outline = "1px solid var(--panel-separator-color)";
+
+ // Now that all the elements are in place for the start of the transition,
+ // give the layout code a chance to set the initial values.
+ await window.promiseDocumentFlushed(() => {});
+ // Bail out if the panel was closed in the meantime.
+ if (!nextPanelView.isOpenIn(this)) {
+ return;
+ }
+
+ // Now set the viewContainer dimensions to that of the new view, which
+ // kicks of the height animation.
+ this._viewContainer.style.height = viewRect.height + "px";
+ this._viewContainer.style.width = viewRect.width + "px";
+ this._panel.style.removeProperty("width");
+ this._panel.style.removeProperty("height");
+
+ // We're setting the width property to prevent flickering during the
+ // sliding animation with smaller views.
+ viewNode.style.width = viewRect.width + "px";
+
+ // Kick off the transition!
+ details.phase = TRANSITION_PHASES.TRANSITION;
+
+ // If we're going to show the main view, we can remove the
+ // min-height property on the view container.
+ if (viewNode.getAttribute("mainview")) {
+ this._viewContainer.style.removeProperty("min-height");
+ }
+
+ this._viewStack.style.transform =
+ "translateX(" + (moveToLeft ? "" : "-") + deltaX + "px)";
+
+ await new Promise(resolve => {
+ details.resolve = resolve;
+ this._viewContainer.addEventListener(
+ "transitionend",
+ (details.listener = ev => {
+ // It's quite common that `height` on the view container doesn't need
+ // to transition, so we make sure to do all the work on the transform
+ // transition-end, because that is guaranteed to happen.
+ if (ev.target != this._viewStack || ev.propertyName != "transform") {
+ return;
+ }
+ this._viewContainer.removeEventListener(
+ "transitionend",
+ details.listener
+ );
+ delete details.listener;
+ resolve();
+ })
+ );
+ this._viewContainer.addEventListener(
+ "transitioncancel",
+ (details.cancelListener = ev => {
+ if (ev.target != this._viewStack) {
+ return;
+ }
+ this._viewContainer.removeEventListener(
+ "transitioncancel",
+ details.cancelListener
+ );
+ delete details.cancelListener;
+ resolve();
+ })
+ );
+ });
+
+ // Bail out if the panel was closed during the transition.
+ if (!nextPanelView.isOpenIn(this)) {
+ return;
+ }
+ prevPanelView.visible = false;
+
+ // This will complete the operation by removing any transition properties.
+ nextPanelView.node.style.removeProperty("width");
+ deepestNode.style.removeProperty("outline");
+ this._cleanupTransitionPhase();
+
+ nextPanelView.focusSelectedElement();
+ }
+
+ /**
+ * Attempt to clean up the attributes and properties set by `_transitionViews`
+ * above. Which attributes and properties depends on the phase the transition
+ * was left from.
+ */
+ _cleanupTransitionPhase() {
+ if (!this._transitionDetails) {
+ return;
+ }
+
+ let { phase, resolve, listener, cancelListener } = this._transitionDetails;
+ this._transitionDetails = null;
+
+ if (phase >= TRANSITION_PHASES.START) {
+ this._panel.style.removeProperty("width");
+ this._panel.style.removeProperty("height");
+ this._viewContainer.style.removeProperty("height");
+ this._viewContainer.style.removeProperty("width");
+ }
+ if (phase >= TRANSITION_PHASES.PREPARE) {
+ this._transitioning = false;
+ this._viewStack.style.removeProperty("margin-inline-start");
+ this._viewStack.style.removeProperty("transition");
+ }
+ if (phase >= TRANSITION_PHASES.TRANSITION) {
+ this._viewStack.style.removeProperty("transform");
+ if (listener) {
+ this._viewContainer.removeEventListener("transitionend", listener);
+ }
+ if (cancelListener) {
+ this._viewContainer.removeEventListener(
+ "transitioncancel",
+ cancelListener
+ );
+ }
+ if (resolve) {
+ resolve();
+ }
+ }
+ }
+
+ _calculateMaxHeight(aEvent) {
+ // While opening the panel, we have to limit the maximum height of any
+ // view based on the space that will be available. We cannot just use
+ // window.screen.availTop and availHeight because these may return an
+ // incorrect value when the window spans multiple screens.
+ let anchor = this._panel.anchorNode;
+ let anchorRect = anchor.getBoundingClientRect();
+
+ let screen = this._screenManager.screenForRect(
+ anchor.screenX,
+ anchor.screenY,
+ anchorRect.width,
+ anchorRect.height
+ );
+ let availTop = {},
+ availHeight = {};
+ screen.GetAvailRect({}, availTop, {}, availHeight);
+ let cssAvailTop = availTop.value / screen.defaultCSSScaleFactor;
+
+ // The distance from the anchor to the available margin of the screen is
+ // based on whether the panel will open towards the top or the bottom.
+ let maxHeight;
+ if (aEvent.alignmentPosition.startsWith("before_")) {
+ maxHeight = anchor.screenY - cssAvailTop;
+ } else {
+ let anchorScreenBottom = anchor.screenY + anchorRect.height;
+ let cssAvailHeight = availHeight.value / screen.defaultCSSScaleFactor;
+ maxHeight = cssAvailTop + cssAvailHeight - anchorScreenBottom;
+ }
+
+ // To go from the maximum height of the panel to the maximum height of
+ // the view stack, we need to subtract the height of the arrow and the
+ // height of the opposite margin, but we cannot get their actual values
+ // because the panel is not visible yet. However, we know that this is
+ // currently 11px on Mac, 13px on Windows, and 13px on Linux. We also
+ // want an extra margin, both for visual reasons and to prevent glitches
+ // due to small rounding errors. So, we just use a value that makes
+ // sense for all platforms. If the arrow visuals change significantly,
+ // this value will be easy to adjust.
+ const EXTRA_MARGIN_PX = 20;
+ maxHeight -= EXTRA_MARGIN_PX;
+ return maxHeight;
+ }
+
+ handleEvent(aEvent) {
+ // Only process actual popup events from the panel or events we generate
+ // ourselves, but not from menus being shown from within the panel.
+ if (
+ aEvent.type.startsWith("popup") &&
+ aEvent.target != this._panel &&
+ aEvent.target != this.node
+ ) {
+ return;
+ }
+ switch (aEvent.type) {
+ case "keydown":
+ // Since we start listening for the "keydown" event when the popup is
+ // already showing and stop listening when the panel is hidden, we
+ // always have at least one view open.
+ let currentView = this.openViews[this.openViews.length - 1];
+ currentView.keyNavigation(aEvent);
+ break;
+ case "mousemove":
+ this.openViews.forEach(panelView => panelView.clearNavigation());
+ break;
+ case "popupshowing": {
+ this._viewContainer.setAttribute("panelopen", "true");
+ if (!this.node.hasAttribute("disablekeynav")) {
+ // We add the keydown handler on the window so that it handles key
+ // presses when a panel appears but doesn't get focus, as happens
+ // when a button to open a panel is clicked with the mouse.
+ // However, this means the listener is on an ancestor of the panel,
+ // which means that handlers such as ToolbarKeyboardNavigator are
+ // deeper in the tree. Therefore, this must be a capturing listener
+ // so we get the event first.
+ this.window.addEventListener("keydown", this, true);
+ this._panel.addEventListener("mousemove", this);
+ }
+ break;
+ }
+ case "popuppositioned": {
+ if (this._panel.state == "showing") {
+ let maxHeight = this._calculateMaxHeight(aEvent);
+ this._viewStack.style.maxHeight = maxHeight + "px";
+ this._offscreenViewStack.style.maxHeight = maxHeight + "px";
+ }
+ break;
+ }
+ case "popupshown":
+ // The main view is always open and visible when the panel is first
+ // shown, so we can check the height of the description elements it
+ // contains and notify consumers using the ViewShown event. In order to
+ // minimize flicker we need to allow synchronous reflows, and we still
+ // make sure the ViewShown event is dispatched synchronously.
+ let mainPanelView = this.openViews[0];
+ this._activateView(mainPanelView);
+ break;
+ case "popuphidden": {
+ // WebExtensions consumers can hide the popup from viewshowing, or
+ // mid-transition, which disrupts our state:
+ this._transitioning = false;
+ this._viewContainer.removeAttribute("panelopen");
+ this._cleanupTransitionPhase();
+ this.window.removeEventListener("keydown", this, true);
+ this._panel.removeEventListener("mousemove", this);
+ this.closeAllViews();
+
+ // Clear the main view size caches. The dimensions could be different
+ // when the popup is opened again, e.g. through touch mode sizing.
+ this._viewContainer.style.removeProperty("min-height");
+ this._viewStack.style.removeProperty("max-height");
+ this._viewContainer.style.removeProperty("width");
+ this._viewContainer.style.removeProperty("height");
+
+ this.dispatchCustomEvent("PanelMultiViewHidden");
+ break;
+ }
+ }
+ }
+}
+
+/**
+ * This is associated to <panelview> elements.
+ */
+export class PanelView extends AssociatedToNode {
+ constructor(node) {
+ super(node);
+
+ /**
+ * Indicates whether the view is active. When this is false, consumers can
+ * wait for the ViewShown event to know when the view becomes active.
+ */
+ this.active = false;
+
+ /**
+ * Specifies whether the view should be focused when active. When this
+ * is true, the first navigable element in the view will be focused
+ * when the view becomes active. This should be set to true when the view
+ * is activated from the keyboard. It will be set to false once the view
+ * is active.
+ */
+ this.focusWhenActive = false;
+ }
+
+ /**
+ * Indicates whether the view is open in the specified PanelMultiView object.
+ */
+ isOpenIn(panelMultiView) {
+ return this.node.panelMultiView == panelMultiView.node;
+ }
+
+ /**
+ * The "mainview" attribute is set before the panel is opened when this view
+ * is displayed as the main view, and is removed before the <panelview> is
+ * displayed as a subview. The same view element can be displayed as a main
+ * view and as a subview at different times.
+ */
+ set mainview(value) {
+ if (value) {
+ this.node.setAttribute("mainview", true);
+ } else {
+ this.node.removeAttribute("mainview");
+ }
+ }
+
+ /**
+ * Determines whether the view is visible. Setting this to false also resets
+ * the "active" property.
+ */
+ set visible(value) {
+ if (value) {
+ this.node.setAttribute("visible", true);
+ } else {
+ this.node.removeAttribute("visible");
+ this.active = false;
+ this.focusWhenActive = false;
+ }
+ }
+
+ /**
+ * Constrains the width of this view using the "min-width" and "max-width"
+ * styles. Setting this to zero removes the constraints.
+ */
+ set minMaxWidth(value) {
+ let style = this.node.style;
+ if (value) {
+ style.minWidth = style.maxWidth = value + "px";
+ } else {
+ style.removeProperty("min-width");
+ style.removeProperty("max-width");
+ }
+ }
+
+ /**
+ * Adds a header with the given title, or removes it if the title is empty.
+ */
+ set headerText(value) {
+ // If the header already exists, update or remove it as requested.
+ let header = this.node.firstElementChild;
+ if (header && header.classList.contains("panel-header")) {
+ if (value) {
+ header.querySelector(".panel-header > h1 > span").textContent = value;
+ } else {
+ header.remove();
+ }
+ return;
+ }
+
+ // The header doesn't exist, only create it if needed.
+ if (!value) {
+ return;
+ }
+
+ header = this.document.createXULElement("box");
+ header.classList.add("panel-header");
+
+ let backButton = this.document.createXULElement("toolbarbutton");
+ backButton.className =
+ "subviewbutton subviewbutton-iconic subviewbutton-back";
+ backButton.setAttribute("closemenu", "none");
+ backButton.setAttribute("tabindex", "0");
+
+ backButton.setAttribute(
+ "aria-label",
+ lazy.gBundle.GetStringFromName("panel.back")
+ );
+
+ backButton.addEventListener("command", () => {
+ // The panelmultiview element may change if the view is reused.
+ this.node.panelMultiView.goBack();
+ backButton.blur();
+ });
+
+ let h1 = this.document.createElement("h1");
+ let span = this.document.createElement("span");
+ span.textContent = value;
+ h1.appendChild(span);
+
+ header.append(backButton, h1);
+ this.node.prepend(header);
+ }
+
+ /**
+ * Populates the "knownWidth" and "knownHeight" properties with the current
+ * dimensions of the view. These may be zero if the view is invisible.
+ *
+ * These values are relevant during transitions and are retained for backwards
+ * navigation if the view is still open but is invisible.
+ */
+ captureKnownSize() {
+ let rect = this._getBoundsWithoutFlushing(this.node);
+ this.knownWidth = rect.width;
+ this.knownHeight = rect.height;
+ }
+
+ /**
+ * Determine whether an element can only be navigated to with tab/shift+tab,
+ * not the arrow keys.
+ */
+ _isNavigableWithTabOnly(element) {
+ let tag = element.localName;
+ return (
+ tag == "menulist" ||
+ tag == "input" ||
+ tag == "textarea" ||
+ // Allow tab to reach embedded documents in extension panels.
+ tag == "browser"
+ );
+ }
+
+ /**
+ * Make a TreeWalker for keyboard navigation.
+ *
+ * @param {boolean} arrowKey If `true`, elements only navigable with tab are
+ * excluded.
+ */
+ _makeNavigableTreeWalker(arrowKey) {
+ let filter = node => {
+ if (node.disabled) {
+ return NodeFilter.FILTER_REJECT;
+ }
+ let bounds = this._getBoundsWithoutFlushing(node);
+ if (bounds.width == 0 || bounds.height == 0) {
+ return NodeFilter.FILTER_REJECT;
+ }
+ if (
+ node.tagName == "button" ||
+ node.tagName == "toolbarbutton" ||
+ node.classList.contains("text-link") ||
+ (!arrowKey && this._isNavigableWithTabOnly(node))
+ ) {
+ // Set the tabindex attribute to make sure the node is focusable.
+ if (!node.hasAttribute("tabindex")) {
+ node.setAttribute("tabindex", "-1");
+ }
+ return NodeFilter.FILTER_ACCEPT;
+ }
+ return NodeFilter.FILTER_SKIP;
+ };
+ return this.document.createTreeWalker(
+ this.node,
+ NodeFilter.SHOW_ELEMENT,
+ filter
+ );
+ }
+
+ /**
+ * Get a TreeWalker which finds elements navigable with tab/shift+tab.
+ */
+ get _tabNavigableWalker() {
+ if (!this.__tabNavigableWalker) {
+ this.__tabNavigableWalker = this._makeNavigableTreeWalker(false);
+ }
+ return this.__tabNavigableWalker;
+ }
+
+ /**
+ * Get a TreeWalker which finds elements navigable with up/down arrow keys.
+ */
+ get _arrowNavigableWalker() {
+ if (!this.__arrowNavigableWalker) {
+ this.__arrowNavigableWalker = this._makeNavigableTreeWalker(true);
+ }
+ return this.__arrowNavigableWalker;
+ }
+
+ /**
+ * Element that is currently selected with the keyboard, or null if no element
+ * is selected. Since the reference is held weakly, it can become null or
+ * undefined at any time.
+ */
+ get selectedElement() {
+ return this._selectedElement && this._selectedElement.get();
+ }
+ set selectedElement(value) {
+ if (!value) {
+ delete this._selectedElement;
+ } else {
+ this._selectedElement = Cu.getWeakReference(value);
+ }
+ }
+
+ /**
+ * Focuses and moves keyboard selection to the first navigable element.
+ * This is a no-op if there are no navigable elements.
+ *
+ * @param {boolean} homeKey - `true` if this is for the home key.
+ * @param {boolean} skipBack - `true` if the Back button should be skipped.
+ */
+ focusFirstNavigableElement(homeKey = false, skipBack = false) {
+ // The home key is conceptually similar to the up/down arrow keys.
+ let walker = homeKey
+ ? this._arrowNavigableWalker
+ : this._tabNavigableWalker;
+ walker.currentNode = walker.root;
+ this.selectedElement = walker.firstChild();
+ if (
+ skipBack &&
+ walker.currentNode &&
+ walker.currentNode.classList.contains("subviewbutton-back") &&
+ walker.nextNode()
+ ) {
+ this.selectedElement = walker.currentNode;
+ }
+ this.focusSelectedElement(/* byKey */ true);
+ }
+
+ /**
+ * Focuses and moves keyboard selection to the last navigable element.
+ * This is a no-op if there are no navigable elements.
+ *
+ * @param {boolean} endKey - `true` if this is for the end key.
+ */
+ focusLastNavigableElement(endKey = false) {
+ // The end key is conceptually similar to the up/down arrow keys.
+ let walker = endKey ? this._arrowNavigableWalker : this._tabNavigableWalker;
+ walker.currentNode = walker.root;
+ this.selectedElement = walker.lastChild();
+ this.focusSelectedElement(/* byKey */ true);
+ }
+
+ /**
+ * Based on going up or down, select the previous or next focusable element.
+ *
+ * @param {boolean} isDown - whether we're going down (true) or up (false).
+ * @param {boolean} arrowKey - `true` if this is for the up/down arrow keys.
+ *
+ * @returns {DOMNode} the element we selected.
+ */
+ moveSelection(isDown, arrowKey = false) {
+ let walker = arrowKey
+ ? this._arrowNavigableWalker
+ : this._tabNavigableWalker;
+ let oldSel = this.selectedElement;
+ let newSel;
+ if (oldSel) {
+ walker.currentNode = oldSel;
+ newSel = isDown ? walker.nextNode() : walker.previousNode();
+ }
+ // If we couldn't find something, select the first or last item:
+ if (!newSel) {
+ walker.currentNode = walker.root;
+ newSel = isDown ? walker.firstChild() : walker.lastChild();
+ }
+ this.selectedElement = newSel;
+ return newSel;
+ }
+
+ /**
+ * Allow for navigating subview buttons using the arrow keys and the Enter key.
+ * The Up and Down keys can be used to navigate the list up and down and the
+ * Enter, Right or Left - depending on the text direction - key can be used to
+ * simulate a click on the currently selected button.
+ * The Right or Left key - depending on the text direction - can be used to
+ * navigate to the previous view, functioning as a shortcut for the view's
+ * back button.
+ * Thus, in LTR mode:
+ * - The Right key functions the same as the Enter key, simulating a click
+ * - The Left key triggers a navigation back to the previous view.
+ *
+ * Key navigation is only enabled while the view is active, meaning that this
+ * method will return early if it is invoked during a sliding transition.
+ *
+ * @param {KeyEvent} event
+ */
+ /* eslint-disable-next-line complexity */
+ keyNavigation(event) {
+ if (!this.active) {
+ return;
+ }
+
+ let focus = this.document.activeElement;
+ // Make sure the focus is actually inside the panel. (It might not be if
+ // the panel was opened with the mouse.) If it isn't, we don't care
+ // about it for our purposes.
+ // We use Node.compareDocumentPosition because Node.contains doesn't
+ // behave as expected for anonymous content; e.g. the input inside a
+ // textbox.
+ if (
+ focus &&
+ !(
+ this.node.compareDocumentPosition(focus) &
+ Node.DOCUMENT_POSITION_CONTAINED_BY
+ )
+ ) {
+ focus = null;
+ }
+
+ // Extension panels contain embedded documents. We can't manage
+ // keyboard navigation within those.
+ if (focus && focus.tagName == "browser") {
+ return;
+ }
+
+ let stop = () => {
+ event.stopPropagation();
+ event.preventDefault();
+ };
+
+ // If the focused element is only navigable with tab, it wants the arrow
+ // keys, etc. We shouldn't handle any keys except tab and shift+tab.
+ // We make a function for this for performance reasons: we only want to
+ // check this for keys we potentially care about, not *all* keys.
+ let tabOnly = () => {
+ // We use the real focus rather than this.selectedElement because focus
+ // might have been moved without keyboard navigation (e.g. mouse click)
+ // and this.selectedElement is only updated for keyboard navigation.
+ return focus && this._isNavigableWithTabOnly(focus);
+ };
+
+ // If a context menu is open, we must let it handle all keys.
+ // Normally, this just happens, but because we have a capturing window
+ // keydown listener, our listener takes precedence.
+ // Again, we only want to do this check on demand for performance.
+ let isContextMenuOpen = () => {
+ if (!focus) {
+ return false;
+ }
+ let contextNode = focus.closest("[context]");
+ if (!contextNode) {
+ return false;
+ }
+ let context = contextNode.getAttribute("context");
+ let popup = this.document.getElementById(context);
+ return popup && popup.state == "open";
+ };
+
+ let keyCode = event.code;
+ switch (keyCode) {
+ case "ArrowDown":
+ case "ArrowUp":
+ if (tabOnly()) {
+ break;
+ }
+ // Fall-through...
+ case "Tab": {
+ if (isContextMenuOpen()) {
+ break;
+ }
+ stop();
+ let isDown =
+ keyCode == "ArrowDown" || (keyCode == "Tab" && !event.shiftKey);
+ let button = this.moveSelection(isDown, keyCode != "Tab");
+ Services.focus.setFocus(button, Services.focus.FLAG_BYKEY);
+ break;
+ }
+ case "Home":
+ if (tabOnly() || isContextMenuOpen()) {
+ break;
+ }
+ stop();
+ this.focusFirstNavigableElement(true);
+ break;
+ case "End":
+ if (tabOnly() || isContextMenuOpen()) {
+ break;
+ }
+ stop();
+ this.focusLastNavigableElement(true);
+ break;
+ case "ArrowLeft":
+ case "ArrowRight": {
+ if (tabOnly() || isContextMenuOpen()) {
+ break;
+ }
+ stop();
+ if (
+ (!this.window.RTL_UI && keyCode == "ArrowLeft") ||
+ (this.window.RTL_UI && keyCode == "ArrowRight")
+ ) {
+ this.node.panelMultiView.goBack();
+ break;
+ }
+ // If the current button is _not_ one that points to a subview, pressing
+ // the arrow key shouldn't do anything.
+ let button = this.selectedElement;
+ if (!button || !button.classList.contains("subviewbutton-nav")) {
+ break;
+ }
+ }
+ // Fall-through...
+ case "Space":
+ case "NumpadEnter":
+ case "Enter": {
+ if (tabOnly() || isContextMenuOpen()) {
+ break;
+ }
+ let button = this.selectedElement;
+ if (!button) {
+ break;
+ }
+ stop();
+
+ this._doingKeyboardActivation = true;
+ // Unfortunately, 'tabindex' doesn't execute the default action, so
+ // we explicitly do this here.
+ // We are sending a command event, a mousedown event and then a click
+ // event. This is done in order to mimic a "real" mouse click event.
+ // Normally, the command event executes the action, then the click event
+ // closes the menu. However, in some cases (e.g. the Library button),
+ // there is no command event handler and the mousedown event executes the
+ // action instead.
+ button.doCommand();
+ let dispEvent = new event.target.ownerGlobal.MouseEvent("mousedown", {
+ bubbles: true,
+ });
+ button.dispatchEvent(dispEvent);
+ dispEvent = new event.target.ownerGlobal.MouseEvent("click", {
+ bubbles: true,
+ });
+ button.dispatchEvent(dispEvent);
+ this._doingKeyboardActivation = false;
+ break;
+ }
+ }
+ }
+
+ /**
+ * Focus the last selected element in the view, if any.
+ *
+ * @param byKey {Boolean} whether focus was moved by the user pressing a key.
+ * Needed to ensure we show focus styles in the right cases.
+ */
+ focusSelectedElement(byKey = false) {
+ let selected = this.selectedElement;
+ if (selected) {
+ let flag = byKey ? "FLAG_BYKEY" : "FLAG_BYELEMENTFOCUS";
+ Services.focus.setFocus(selected, Services.focus[flag]);
+ }
+ }
+
+ /**
+ * Clear all traces of keyboard navigation happening right now.
+ */
+ clearNavigation() {
+ let selected = this.selectedElement;
+ if (selected) {
+ selected.blur();
+ this.selectedElement = null;
+ }
+ }
+}
diff --git a/comm/mail/components/customizableui/content/customizeMode.inc.xhtml b/comm/mail/components/customizableui/content/customizeMode.inc.xhtml
new file mode 100644
index 0000000000..fc7eb0595b
--- /dev/null
+++ b/comm/mail/components/customizableui/content/customizeMode.inc.xhtml
@@ -0,0 +1,128 @@
+<!-- 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/. -->
+
+<box id="customization-content-container">
+<box flex="1" id="customization-palette-container">
+ <label id="customization-header" data-l10n-id="customize-mode-menu-and-toolbars-header"></label>
+ <vbox id="customization-palette" class="customization-palette" hidden="true"/>
+ <vbox id="customization-pong-arena" hidden="true"/>
+ <spacer id="customization-spacer"/>
+</box>
+<vbox id="customization-panel-container">
+ <vbox id="customization-panelWrapper">
+ <box class="panel-arrowbox">
+ <image class="panel-arrow" side="top"/>
+ </box>
+ <box class="panel-arrowcontent" side="top" flex="1">
+ <vbox id="customization-panelHolder">
+ <description id="customization-panelHeader" data-l10n-id="customize-mode-overflow-list-title"></description>
+ <description id="customization-panelDescription" data-l10n-id="customize-mode-overflow-list-description"></description>
+ </vbox>
+ <box class="panel-inner-arrowcontentfooter" hidden="true"/>
+ </box>
+ </vbox>
+</vbox>
+</box>
+<hbox id="customization-footer">
+<checkbox id="customization-titlebar-visibility-checkbox" class="customizationmode-checkbox"
+# NB: because oncommand fires after click, by the time we've fired, the checkbox binding
+# will already have switched the button's state, so this is correct:
+ oncommand="gCustomizeMode.toggleTitlebar(this.checked)" data-l10n-id="customize-mode-titlebar"/>
+<checkbox id="customization-extra-drag-space-checkbox" class="customizationmode-checkbox"
+ data-l10n-id="customize-mode-extra-drag-space"
+ oncommand="gCustomizeMode.toggleDragSpace(this.checked)"/>
+<button id="customization-toolbar-visibility-button" class="customizationmode-button" type="menu" data-l10n-id="customize-mode-toolbars">
+ <menupopup id="customization-toolbar-menu" onpopupshowing="onViewToolbarsPopupShowing(event)"/>
+</button>
+<button id="customization-lwtheme-button" data-l10n-id="customize-mode-lwthemes" class="customizationmode-button" type="menu">
+ <panel type="arrow" id="customization-lwtheme-menu"
+ orient="vertical"
+ onpopupshowing="gCustomizeMode.onThemesMenuShowing(event);"
+ position="topcenter bottomleft"
+ flip="none"
+ role="menu">
+ <label id="customization-lwtheme-menu-header" data-l10n-id="customize-mode-lwthemes-my-themes"/>
+ <hbox id="customization-lwtheme-menu-footer">
+ <toolbarbutton class="customization-lwtheme-menu-footeritem"
+ data-l10n-id="customize-mode-lwthemes-menu-manage"
+ tabindex="0"
+ oncommand="gCustomizeMode.openAddonsManagerThemes(event);"/>
+ <toolbarbutton class="customization-lwtheme-menu-footeritem"
+ data-l10n-id="customize-mode-lwthemes-menu-get-more"
+ tabindex="0"
+ oncommand="gCustomizeMode.getMoreThemes(event);"/>
+ </hbox>
+ </panel>
+</button>
+<button id="customization-uidensity-button"
+ data-l10n-id="customize-mode-uidensity"
+ class="customizationmode-button"
+ type="menu">
+ <panel type="arrow" id="customization-uidensity-menu"
+ onpopupshowing="gCustomizeMode.onUIDensityMenuShowing();"
+ position="topcenter bottomleft"
+ flip="none"
+ role="menu">
+ <menuitem id="customization-uidensity-menuitem-compact"
+ class="menuitem-iconic customization-uidensity-menuitem"
+ role="menuitemradio"
+ data-l10n-id="customize-mode-uidensity-menu-compact"
+ tabindex="0"
+ onfocus="gCustomizeMode.updateUIDensity(this.mode);"
+ onmouseover="gCustomizeMode.updateUIDensity(this.mode);"
+ onblur="gCustomizeMode.resetUIDensity();"
+ onmouseout="gCustomizeMode.resetUIDensity();"
+ oncommand="gCustomizeMode.setUIDensity(this.mode);"/>
+ <menuitem id="customization-uidensity-menuitem-normal"
+ class="menuitem-iconic customization-uidensity-menuitem"
+ role="menuitemradio"
+ data-l10n-id="customize-mode-uidensity-menu-normal"
+ tabindex="0"
+ onfocus="gCustomizeMode.updateUIDensity(this.mode);"
+ onmouseover="gCustomizeMode.updateUIDensity(this.mode);"
+ onblur="gCustomizeMode.resetUIDensity();"
+ onmouseout="gCustomizeMode.resetUIDensity();"
+ oncommand="gCustomizeMode.setUIDensity(this.mode);"/>
+#ifndef XP_MACOSX
+ <menuitem id="customization-uidensity-menuitem-touch"
+ class="menuitem-iconic customization-uidensity-menuitem"
+ role="menuitemradio"
+ data-l10n-id="customize-mode-uidensity-menu-touch"
+ tabindex="0"
+ onfocus="gCustomizeMode.updateUIDensity(this.mode);"
+ onmouseover="gCustomizeMode.updateUIDensity(this.mode);"
+ onblur="gCustomizeMode.resetUIDensity();"
+ onmouseout="gCustomizeMode.resetUIDensity();"
+ oncommand="gCustomizeMode.setUIDensity(this.mode);">
+ </menuitem>
+ <spacer hidden="true" id="customization-uidensity-touch-spacer"/>
+ <checkbox id="customization-uidensity-autotouchmode-checkbox"
+ hidden="true"
+ data-l10n-id="customize-mode-uidensity-auto-touch-mode-checkbox"
+ oncommand="gCustomizeMode.updateAutoTouchMode(this.checked)"/>
+#endif
+ </panel>
+</button>
+
+<button id="whimsy-button"
+ type="checkbox"
+ class="customizationmode-button"
+ oncommand="gCustomizeMode.togglePong(this.checked);"
+ hidden="true"/>
+
+<spacer id="customization-footer-spacer"/>
+<button id="customization-undo-reset-button"
+ class="customizationmode-button"
+ hidden="true"
+ oncommand="gCustomizeMode.undoReset();"
+ data-l10n-id="customize-mode-undo-cmd"/>
+<button id="customization-reset-button"
+ oncommand="gCustomizeMode.reset();"
+ data-l10n-id="customize-mode-restore-defaults"
+ class="customizationmode-button"/>
+<button id="customization-done-button"
+ oncommand="gCustomizeMode.exit();"
+ data-l10n-id="customize-mode-done"
+ class="customizationmode-button"/>
+</hbox>
diff --git a/comm/mail/components/customizableui/content/jar.mn b/comm/mail/components/customizableui/content/jar.mn
new file mode 100644
index 0000000000..db1978fdb0
--- /dev/null
+++ b/comm/mail/components/customizableui/content/jar.mn
@@ -0,0 +1,6 @@
+# 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/.
+
+messenger.jar:
+ content/messenger/panelUI.js
diff --git a/comm/mail/components/customizableui/content/moz.build b/comm/mail/components/customizableui/content/moz.build
new file mode 100644
index 0000000000..d988c0ff9b
--- /dev/null
+++ b/comm/mail/components/customizableui/content/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/mail/components/customizableui/content/panelUI.inc.xhtml b/comm/mail/components/customizableui/content/panelUI.inc.xhtml
new file mode 100644
index 0000000000..3b965da756
--- /dev/null
+++ b/comm/mail/components/customizableui/content/panelUI.inc.xhtml
@@ -0,0 +1,606 @@
+<!-- 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/. -->
+
+<panel id="appMenu-popup"
+ class="cui-widget-panel panel-no-padding"
+ role="group"
+ type="arrow"
+ hidden="true"
+ flip="slide"
+ position="bottomright topright"
+ noautofocus="true">
+ <panelmultiview id="appMenu-multiView"
+ mainViewId="appMenu-mainView"
+ viewCacheId="appMenu-viewCache">
+
+ <!-- Main Appmenu View -->
+ <panelview id="appMenu-mainView" class="PanelUI-subView">
+ <vbox id="appMenu-mainViewItems"
+ class="panel-subview-body">
+ <vbox id="appMenu-addon-banners"/>
+ <toolbarbutton class="panel-banner-item"
+ oncommand="PanelUI._onBannerItemSelected(event)"
+ hidden="true"/>
+#ifdef NIGHTLY_BUILD
+ <toolbarbutton id="appmenu_signin"
+ data-l10n-id="appmenu-signin-panel"
+ class="subviewbutton subviewbutton-iconic"
+ hidden="true"
+ oncommand="gSync.initFxA();"/>
+ <toolbarbutton id="appmenu_sync"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ hidden="true"
+ align="center"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('appMenu-syncView', this)">
+ <hbox flex="1">
+ <html:img id="appmenu-sync-icon"
+ class="toolbarbutton-icon"
+ alt=""/>
+ <vbox flex="1">
+ <label id="appmenu-sync-sync"
+ crop="end"
+ data-l10n-id="appmenu-sync-sync"/>
+ <label id="appmenu-sync-account"
+ class="appmenu-sync-account-email"
+ crop="end"
+ data-l10n-id="appmenu-sync-account"/>
+ </vbox>
+ </hbox>
+ </toolbarbutton>
+ <toolbarseparator id="syncSeparator" hidden="true"/>
+#endif
+ <toolbarbutton id="appmenu_new"
+ data-l10n-id="appmenu-new-account-panel"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('appMenu-newView', this)"/>
+ <toolbarbutton id="appmenu_create"
+ data-l10n-id="appmenu-create-panel"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('appMenu-createView', this)"/>
+ <toolbarseparator id="appmenu_createPopupMenuSeparator"/>
+ <toolbarbutton id="appmenu_open"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ data-l10n-id="appmenu-open-file-panel"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('appMenu-openView', this)"/>
+ <toolbarseparator/>
+ <toolbarbutton id="appmenu_View"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ data-l10n-id="appmenu-view-panel"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('appMenu-viewView', this)"/>
+ <toolbaritem id="appMenu-uiDensity-controls"
+ class="subviewbutton subviewbutton-iconic toolbaritem-combined-buttons"
+ closemenu="none">
+ <html:img class="toolbarbutton-icon" src="" alt=""/>
+ <label class="toolbarbutton-text" data-l10n-id="appmenu-mail-uidensity-value"/>
+ <toolbarbutton id="appmenu_uiDensityCompact"
+ class="subviewbutton subviewbutton-iconic subviewbutton"
+ data-l10n-id="appmenu-uidensity-compact"
+ type="radio"
+ oncommand="PanelUI.setUIDensity(event);"/>
+ <toolbarbutton id="appmenu_uiDensityNormal"
+ class="subviewbutton subviewbutton-iconic subviewbutton"
+ data-l10n-id="appmenu-uidensity-default"
+ type="radio"
+ oncommand="PanelUI.setUIDensity(event);"/>
+ <toolbarbutton id="appmenu_uiDensityTouch"
+ class="subviewbutton subviewbutton-iconic subviewbutton"
+ data-l10n-id="appmenu-uidensity-relaxed"
+ type="radio"
+ oncommand="PanelUI.setUIDensity(event);"/>
+ </toolbaritem>
+ <toolbaritem id="appMenu-fontSize-controls"
+ class="subviewbutton subviewbutton-iconic toolbaritem-combined-buttons"
+ closemenu="none">
+ <html:img class="toolbarbutton-icon" src="" alt=""/>
+ <label class="toolbarbutton-text" data-l10n-id="appmenu-font-size-value"/>
+ <toolbarbutton id="appMenu-fontSizeReduce-button"
+ class="subviewbutton subviewbutton-iconic"
+ oncommand="UIFontSize.reduceSize();"
+ data-l10n-id="appmenuitem-font-size-reduce"/>
+ <toolbarbutton id="appMenu-fontSizeReset-button"
+ class="subviewbutton"
+ oncommand="UIFontSize.resetSize();"
+ tooltip="fontSizeReset"/>
+ <toolbarbutton id="appMenu-fontSizeEnlarge-button"
+ class="subviewbutton subviewbutton-iconic"
+ oncommand="UIFontSize.increaseSize();"
+ data-l10n-id="appmenuitem-font-size-enlarge"/>
+ </toolbaritem>
+ <toolbarseparator/>
+ <toolbarbutton id="appmenu_preferences"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-settings"
+ oncommand="openOptionsDialog();"/>
+ <toolbarbutton id="appmenu_accountmgr"
+ class="subviewbutton subviewbutton-iconic"
+ label="&accountManagerCmd2.label;"
+ oncommand="MsgAccountManager(null);"/>
+ <toolbarbutton id="appmenu_addons"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-addons-and-themes"
+ oncommand="openAddonsMgr();"/>
+ <toolbarseparator/>
+ <toolbarbutton id="appmenu_toolsMenu"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ data-l10n-id="appmenu-tools-panel"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('appMenu-toolsView', this)"/>
+ <toolbarbutton id="appmenu_help"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ data-l10n-id="menu-help-help-title"
+ closemenu="none"
+ oncommand="buildHelpMenu(); PanelUI.showSubView('appMenu-helpView', this)"/>
+ <toolbarseparator/>
+ <toolbarbutton id="appmenu-quit"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="menu-quit"
+ key="key_quitApplication"
+ command="cmd_quitApplication"/>
+ </vbox>
+ </panelview>
+#ifdef NIGHTLY_BUILD
+ <!-- Sync -->
+ <panelview id="appMenu-syncView"
+ data-l10n-id="appmenu-sync-panel-title"
+ class="PanelUI-subView">
+ <vbox id="appMenu-syncViewItems"
+ class="panel-subview-body">
+ <toolbarbutton id="appmenu_manageSyncAccountMenuItem"
+ class="subviewbutton subviewbutton-iconic"
+ align="center"
+ oncommand="gSync.openFxAManagePage();">
+ <hbox flex="1">
+ <html:img id="appmenu-manage-sync-icon"
+ class="toolbarbutton-icon"
+ alt=""/>
+ <vbox flex="1">
+ <label id="appmenu-sync-menu-manage"
+ crop="end"
+ data-l10n-id="appmenu-sync-manage"/>
+ <label id="appmenu-sync-menu-account"
+ class="appmenu-sync-account-email"
+ crop="end"
+ data-l10n-id="appmenu-sync-account"/>
+ </vbox>
+ </hbox>
+ </toolbarbutton>
+
+ <toolbarbutton id="appmenu-submenu-sync-now"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-sync-now"
+ closemenu="none"
+ oncommand="Weave.Service.sync({});"/>
+ <toolbarbutton id="appmenu-submenu-sync-settings"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-sync-settings"
+ oncommand="openPreferencesTab('sync');"/>
+ <toolbarseparator/>
+ <toolbarbutton id="appmenu-submenu-sync-sign-out"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-sync-sign-out"
+ oncommand="gSync.disconnect({ confirm: true });"/>
+ </vbox>
+ </panelview>
+#endif
+ <!-- New -->
+ <panelview id="appMenu-newView"
+ data-l10n-id="appmenu-new-account-panel-title"
+ class="PanelUI-subView">
+ <vbox id="appMenu-newViewItems"
+ class="panel-subview-body">
+ <toolbarbutton id="appmenu_newCreateEmailAccountMenuItem"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-create-new-mail-account"
+ oncommand="openAccountProvisionerTab();"/>
+ <toolbarbutton id="appmenu_newMailAccountMenuItem"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-new-mail-account"
+ oncommand="openAccountSetupTab();"/>
+#ifdef MAIN_WINDOW
+ <toolbarbutton id="appmenu_calendar-new-calendar-menu-item"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-new-calendar"
+ command="calendar_new_calendar_command"/>
+#endif
+ <toolbarbutton id="appmenu_newAB"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ data-l10n-id="appmenu-newab-panel"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('appMenu-newabView', this)"/>
+ <toolbarbutton id="appmenu_newIMAccountMenuItem"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-new-chat-account"
+ oncommand="openIMAccountWizard();"/>
+ <toolbarbutton id="appmenu_newFeedAccountMenuItem"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-new-feed"
+ oncommand="AddFeedAccount();"/>
+ <toolbarbutton id="appmenu_newNewsgroupAccountMenuItem"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-new-newsgroup"
+ oncommand="openNewsgroupAccountWizard();"/>
+ </vbox>
+ </panelview>
+
+ <!-- New AB -->
+ <panelview id="appMenu-newabView"
+ data-l10n-id="appmenu-newab-panel-title"
+ class="PanelUI-subView">
+ <vbox id="appMenu-newABItems"
+ class="panel-subview-body">
+ <toolbarbutton id="appmenu_newABMenuItem"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-new-addressbook"
+ oncommand="openNewABDialog();"/>
+ <toolbarbutton id="appmenu_newCardDAVMenuItem"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-new-carddav"
+ oncommand="openNewABDialog('CARDDAV');"/>
+ <toolbarbutton id="appmenu_newLdapMenuItem"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-new-ldap"
+ oncommand="openNewABDialog('LDAP');"/>
+ </vbox>
+ </panelview>
+
+ <!-- Create -->
+ <panelview id="appMenu-createView"
+ data-l10n-id="appmenu-create-panel-title"
+ class="PanelUI-subView">
+ <vbox id="appMenu-createViewItems"
+ class="panel-subview-body">
+ <toolbarbutton id="appmenu_newNewMsgCmd"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-create-message"
+ key = "key_newMessage2"
+ command="cmd_newMessage"/>
+#ifdef MAIN_WINDOW
+ <toolbarbutton id="appmenu_calendar-new-event-menu-item"
+ class="subviewbutton subviewbutton-iconic hide-when-calendar-deactivated"
+ data-l10n-id="appmenu-create-event"
+ command="calendar_new_event_command"/>
+ <toolbarbutton id="appmenu_calendar-new-task-menu-item"
+ class="subviewbutton subviewbutton-iconic hide-when-calendar-deactivated"
+ data-l10n-id="appmenu-create-task"
+ command="calendar_new_todo_command"/>
+#endif
+ <toolbarbutton id="appmenu_newCard"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-create-contact"
+ command="cmd_newCard"/>
+ </vbox>
+ </panelview>
+
+ <!-- Open -->
+ <panelview id="appMenu-openView"
+ data-l10n-id="appmenu-open-file-panel-title"
+ class="PanelUI-subView">
+ <vbox id="appMenu-openViewItems"
+ class="panel-subview-body">
+ <toolbarbutton id="appmenu_OpenMessageFileMenuitem"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-open-message"
+ oncommand="MsgOpenFromFile();"/>
+ <toolbarbutton id="appmenu_OpenCalendarFileMenuitem"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-open-calendar"
+ oncommand="openLocalCalendar();"/>
+ </vbox>
+ </panelview>
+
+ <!-- View / Toolbars -->
+ <panelview id="appMenu-toolbarsView"
+ title="&viewToolbarsMenu.label;"
+ class="PanelUI-subView">
+ <vbox class="panel-subview-body">
+#ifdef MAIN_WINDOW
+ <toolbarbutton id="appmenu_quickFilterBar"
+ class="subviewbutton subviewbutton-iconic"
+ type="checkbox"
+ closemenu="none"
+ data-l10n-id="quick-filter-bar-toggle"
+ command="cmd_toggleQuickFilterBar"/>
+ <toolbarbutton id="appmenu_spacesToolbar"
+ class="subviewbutton subviewbutton-iconic"
+ type="checkbox"
+ data-l10n-id="menu-spaces-toolbar-button"
+ closemenu="none"
+ oncommand="gSpacesToolbar.toggleToolbarFromMenu();"/>
+#endif
+ <toolbarbutton id="appmenu_showStatusbar"
+ class="subviewbutton subviewbutton-iconic"
+ type="checkbox"
+ label="&showTaskbarCmd.label;"
+ oncommand="goToggleToolbar('status-bar', 'menu_showTaskbar')"
+ closemenu="none"
+ checked="true"
+ observes="menu_showTaskbar"/>
+ <toolbarseparator id="appmenu_toggleToolbarsSeparator"/>
+ <toolbarbutton id="appmenu_toolbarLayout"
+ class="subviewbutton subviewbutton-iconic"
+ label="&appmenuToolbarLayout.label;"
+ command="cmd_CustomizeMailToolbar"/>
+ </vbox>
+ </panelview>
+
+ <!-- View / Layout -->
+ <panelview id="appMenu-preferencesLayoutView"
+ title="&messagePaneLayoutStyle.label;"
+ class="PanelUI-subView">
+ <vbox class="panel-subview-body">
+ <toolbarbutton id="appmenu_messagePaneClassic"
+ class="subviewbutton subviewbutton-iconic"
+ type="radio"
+ label="&messagePaneClassic.label;"
+ name="viewlayoutgroup"
+ command="cmd_viewClassicMailLayout"/>
+ <toolbarbutton id="appmenu_messagePaneWide"
+ class="subviewbutton subviewbutton-iconic"
+ type="radio"
+ label="&messagePaneWide.label;"
+ name="viewlayoutgroup"
+ command="cmd_viewWideMailLayout"/>
+ <toolbarbutton id="appmenu_messagePaneVertical"
+ class="subviewbutton subviewbutton-iconic"
+ type="radio"
+ label="&messagePaneVertical.label;"
+ name="viewlayoutgroup"
+ command="cmd_viewVerticalMailLayout"/>
+ <toolbarseparator id="appmenu_viewMenuAfterPaneVerticalSeparator"/>
+ <toolbarbutton id="appmenu_showFolderPane"
+ class="subviewbutton subviewbutton-iconic"
+ type="checkbox"
+ closemenu="none"
+ label="&showFolderPaneCmd.label;"
+ command="cmd_toggleFolderPane"/>
+ <toolbarbutton id="appmenu_toggleThreadPaneHeader"
+ class="subviewbutton subviewbutton-iconic"
+ type="checkbox"
+ name="threadheader"
+ closemenu="none"
+ data-l10n-id="appmenuitem-toggle-thread-pane-header"
+ command="cmd_toggleThreadPaneHeader"/>
+ <toolbarbutton id="appmenu_showMessage"
+ class="subviewbutton subviewbutton-iconic"
+ type="checkbox"
+ closemenu="none"
+ label="&showMessageCmd.label;"
+ key="key_toggleMessagePane"
+ command="cmd_toggleMessagePane"/>
+ <toolbarseparator/>
+ <toolbarbutton id="appmenu_calShowTodayPane-2"
+ class="subviewbutton subviewbutton-iconic"
+ label="&todaypane.showTodayPane.label;"
+ type="checkbox"
+ command="calendar_toggle_todaypane_command"/>
+ </vbox>
+ </panelview>
+
+ <!-- View -->
+ <panelview id="appMenu-viewView"
+ class="PanelUI-subView"
+ data-l10n-id="appmenu-view-panel-title">
+ <vbox id="appMenu-viewViewItems"
+ class="panel-subview-body">
+ <toolbarbutton id="appmenu_Toolbars"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ label="&viewToolbarsMenu.label;"
+ accesskey="&viewToolbarsMenu.accesskey;"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('appMenu-toolbarsView', this)"/>
+ <toolbarbutton id="appmenu_MessagePaneLayout"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ label="&messagePaneLayoutStyle.label;"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('appMenu-preferencesLayoutView', this)"/>
+ <toolbarbutton id="appmenu_FolderViews"
+ class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+ label="&folderView.label;"
+ closemenu="none"
+ oncommand="PanelUI.showSubView('appMenu-foldersView', this)"/>
+ </vbox>
+ </panelview>
+
+ <!-- View / Folders -->
+ <panelview id="appMenu-foldersView"
+ title="&folderView.label;"
+ class="PanelUI-subView">
+ <vbox class="panel-subview-body">
+ <toolbarbutton id="appmenu_toggleFolderHeader"
+ class="subviewbutton subviewbutton-iconic"
+ name="paneheader"
+ value="toggle-header"
+ data-l10n-id="menu-view-folders-toggle-header"
+ type="checkbox"
+ closemenu="none"
+ oncommand="PanelUI.folderViewMenuOnCommand(event);"/>
+ <toolbarseparator id="appmenu_folderModesSeparator"/>
+ <toolbarbutton id="appmenu_allFolders"
+ class="subviewbutton subviewbutton-iconic"
+ value="all"
+ data-l10n-id="show-all-folders-label"
+ type="checkbox"
+ name="viewmessages"
+ closemenu="none"
+ oncommand="PanelUI.folderViewMenuOnCommand(event);"/>
+ <toolbarbutton id="appmenu_smartFolders"
+ class="subviewbutton subviewbutton-iconic"
+ value="smart"
+ data-l10n-id="show-smart-folders-label"
+ type="checkbox"
+ name="viewmessages"
+ closemenu="none"
+ oncommand="PanelUI.folderViewMenuOnCommand(event);"/>
+ <toolbarbutton id="appmenu_unreadFolders"
+ class="subviewbutton subviewbutton-iconic"
+ value="unread"
+ data-l10n-id="show-unread-folders-label"
+ type="checkbox"
+ name="viewmessages"
+ closemenu="none"
+ oncommand="PanelUI.folderViewMenuOnCommand(event);"/>
+ <toolbarbutton id="appmenu_favoriteFolders"
+ class="subviewbutton subviewbutton-iconic"
+ value="favorite"
+ data-l10n-id="show-favorite-folders-label"
+ type="checkbox"
+ name="viewmessages"
+ closemenu="none"
+ oncommand="PanelUI.folderViewMenuOnCommand(event);"/>
+ <toolbarbutton id="appmenu_recentFolders"
+ class="subviewbutton subviewbutton-iconic"
+ value="recent"
+ data-l10n-id="show-recent-folders-label"
+ type="checkbox"
+ name="viewmessages"
+ closemenu="none"
+ oncommand="PanelUI.folderViewMenuOnCommand(event);"/>
+ <toolbarseparator/>
+ <toolbarbutton id="appmenu_tagsFolders"
+ class="subviewbutton subviewbutton-iconic"
+ value="tags"
+ data-l10n-id="show-tags-folders-label"
+ type="checkbox"
+ name="viewmessages"
+ closemenu="none"
+ oncommand="PanelUI.folderViewMenuOnCommand(event);"/>
+ <toolbarseparator id="appmenu_compactPropertiesSeparator"/>
+ <toolbarbutton id="appmenu_compactMode"
+ class="subviewbutton subviewbutton-iconic"
+ value="compact"
+ data-l10n-id="folder-toolbar-toggle-folder-compact-view"
+ type="checkbox"
+ name="viewmessages"
+ closemenu="none"
+ oncommand="PanelUI.folderCompactMenuOnCommand(event)"/>
+ <toolbarseparator id="appmenu_favoritePropertiesSeparator"/>
+ <toolbarbutton id="appmenu_favoriteFolder"
+ class="subviewbutton subviewbutton-iconic"
+ type="checkbox"
+ label="&menuFavoriteFolder.label;"
+ checked="false"
+ command="cmd_toggleFavoriteFolder"/>
+ <toolbarbutton id="appmenu_properties"
+ class="subviewbutton subviewbutton-iconic"
+ command="cmd_properties"/>
+ </vbox>
+ </panelview>
+
+ <!-- View / Messages / Tags -->
+ <!-- Dynamically populated when shown. -->
+ <panelview id="appMenu-viewMessagesTagsView"
+ title="&viewTags.label;"
+ class="PanelUI-subView"
+ oncommand="ViewChangeByMenuitem(event.target);">
+ <vbox class="panel-subview-body"/>
+ </panelview>
+
+ <!-- View / Messages / Custom Views -->
+ <!-- Dynamically populated when shown. -->
+ <panelview id="appMenu-viewMessagesCustomViewsView"
+ title="&viewCustomViews.label;"
+ class="PanelUI-subView"
+ oncommand="ViewChangeByMenuitem(event.target);">
+ <vbox class="panel-subview-body"/>
+ </panelview>
+
+ <!-- Tools -->
+ <panelview id="appMenu-toolsView"
+ data-l10n-id="appmenu-tools-panel-title"
+ class="PanelUI-subView">
+ <vbox class="panel-subview-body">
+ <toolbarbutton id="appmenu_import"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-tools-import"
+ oncommand="toImport();"/>
+ <toolbarbutton id="appmenu_export"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-tools-export"
+ oncommand="toExport();"/>
+ <toolbarseparator id="importExportSeparator"/>
+ <toolbarbutton id="appmenu_searchCmd"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-tools-message-search"
+ key="key_searchMail"
+ command="cmd_searchMessages"/>
+ <toolbarbutton id="appmenu_filtersCmd"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-tools-message-filters"
+ oncommand="MsgFilters();"/>
+ <toolbarbutton id="appmenu_manageKeysOpenPGP"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="openpgp-manage-keys-openpgp-cmd"
+ oncommand="openKeyManager()"/>
+ <toolbarbutton id="appmenu_openSavedFilesWnd"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-tools-download-manager"
+ key="key_savedFiles"
+ oncommand="openSavedFilesWnd();"/>
+ <toolbarbutton id="appmenu_activityManager"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-tools-activity-manager"
+ oncommand="openActivityMgr();"/>
+ <toolbarseparator id="devToolsSeparator"/>
+ <toolbarbutton id="appmenu_devtoolsToolbox"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-tools-dev-tools"
+ key="key_devtoolsToolbox"
+ oncommand="BrowserToolboxLauncher.init();"/>
+ </vbox>
+ </panelview>
+
+ <!-- Help -->
+ <panelview id="appMenu-helpView"
+ data-l10n-id="appmenu-help-panel-title"
+ class="PanelUI-subView">
+ <vbox class="panel-subview-body">
+ <toolbarbutton id="appmenu_openHelp"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-help-get-help"
+ key="key_openHelp"
+ oncommand="openSupportURL();"/>
+ <toolbarbutton id="appmenu_openTour"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-help-explore-features"
+ oncommand="openLinkText(event, 'tourURL');"/>
+ <toolbarbutton id="appmenu_keyboardShortcuts"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-help-shortcuts"
+ oncommand="openLinkText(event, 'keyboardShortcutsURL');"/>
+ <toolbarseparator/>
+ <toolbarbutton id="appmenu_getInvolved"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-help-get-involved"
+ oncommand="openLinkText(event, 'getInvolvedURL');"/>
+ <toolbarbutton id="appmenu_makeDonation"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-help-donation"
+ oncommand="openLinkText(event, 'donateURL');"/>
+ <toolbarbutton id="appmenu_submitFeedback"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-help-share-feedback"
+ oncommand="openLinkText(event, 'feedbackURL');"/>
+ <toolbarseparator/>
+ <toolbarbutton id="appmenu_troubleshootMode"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-help-enter-troubleshoot-mode2"
+ oncommand="safeModeRestart();"/>
+ <toolbarbutton id="appmenu_troubleshootingInfo"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-help-troubleshooting-info"
+ oncommand="openAboutSupport();"/>
+ <toolbarseparator/>
+ <toolbarbutton id="appmenu_about"
+ class="subviewbutton subviewbutton-iconic"
+ data-l10n-id="appmenu-help-about-product"
+ oncommand="openAboutDialog();"/>
+ </vbox>
+ </panelview>
+ </panelmultiview>
+</panel>
diff --git a/comm/mail/components/customizableui/content/panelUI.js b/comm/mail/components/customizableui/content/panelUI.js
new file mode 100644
index 0000000000..bad418abb4
--- /dev/null
+++ b/comm/mail/components/customizableui/content/panelUI.js
@@ -0,0 +1,882 @@
+/* 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/. */
+
+/* import-globals-from ../../../base/content/globalOverlay.js */
+/* import-globals-from ../../../base/content/mailCore.js */
+/* import-globals-from ../../../base/content/mailWindowOverlay.js */
+/* import-globals-from ../../../base/content/messenger.js */
+/* import-globals-from ../../../extensions/mailviews/content/msgViewPickerOverlay.js */
+
+var { ExtensionParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionParent.sys.mjs"
+);
+var { ExtensionSupport } = ChromeUtils.import(
+ "resource:///modules/ExtensionSupport.jsm"
+);
+var { ShortcutUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/ShortcutUtils.sys.mjs"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { UIDensity } = ChromeUtils.import("resource:///modules/UIDensity.jsm");
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs",
+ CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
+ PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
+});
+ChromeUtils.defineModuleGetter(
+ this,
+ "ExtensionsUI",
+ "resource:///modules/ExtensionsUI.jsm"
+);
+
+/**
+ * Maintains the state and dispatches events for the main menu panel.
+ */
+const PanelUI = {
+ /** Panel events that we listen for. */
+ get kEvents() {
+ return [
+ "popupshowing",
+ "popupshown",
+ "popuphiding",
+ "popuphidden",
+ "ViewShowing",
+ ];
+ },
+ /**
+ * Used for lazily getting and memoizing elements from the document. Lazy
+ * getters are set in init, and memoizing happens after the first retrieval.
+ */
+ get kElements() {
+ return {
+ mainView: "appMenu-mainView",
+ multiView: "appMenu-multiView",
+ menuButton: "button-appmenu",
+ panel: "appMenu-popup",
+ addonNotificationContainer: "appMenu-addon-banners",
+ navbar: "mail-bar3",
+ };
+ },
+
+ kAppMenuButtons: new Set(),
+
+ _initialized: false,
+ _notifications: null,
+
+ init() {
+ this._initElements();
+ this.initAppMenuButton("button-appmenu", "mail-toolbox");
+
+ this.menuButton = this.menuButtonMail;
+
+ Services.obs.addObserver(this, "fullscreen-nav-toolbox");
+ Services.obs.addObserver(this, "appMenu-notifications");
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "autoHideToolbarInFullScreen",
+ "browser.fullscreen.autohide",
+ false,
+ (pref, previousValue, newValue) => {
+ // On OSX, or with autohide preffed off, MozDOMFullscreen is the only
+ // event we care about, since fullscreen should behave just like non
+ // fullscreen. Otherwise, we don't want to listen to these because
+ // we'd just be spamming ourselves with both of them whenever a user
+ // opened a video.
+ if (newValue) {
+ window.removeEventListener("MozDOMFullscreen:Entered", this);
+ window.removeEventListener("MozDOMFullscreen:Exited", this);
+ window.addEventListener("fullscreen", this);
+ } else {
+ window.addEventListener("MozDOMFullscreen:Entered", this);
+ window.addEventListener("MozDOMFullscreen:Exited", this);
+ window.removeEventListener("fullscreen", this);
+ }
+
+ this._updateNotifications(false);
+ },
+ autoHidePref => autoHidePref && Services.appinfo.OS !== "Darwin"
+ );
+
+ if (this.autoHideToolbarInFullScreen) {
+ window.addEventListener("fullscreen", this);
+ } else {
+ window.addEventListener("MozDOMFullscreen:Entered", this);
+ window.addEventListener("MozDOMFullscreen:Exited", this);
+ }
+
+ window.addEventListener("activate", this);
+
+ Services.obs.notifyObservers(
+ null,
+ "appMenu-notifications-request",
+ "refresh"
+ );
+
+ this._initialized = true;
+ },
+
+ _initElements() {
+ for (let [k, v] of Object.entries(this.kElements)) {
+ // Need to do fresh let-bindings per iteration
+ let getKey = k;
+ let id = v;
+ this.__defineGetter__(getKey, function () {
+ delete this[getKey];
+ // eslint-disable-next-line consistent-return
+ return (this[getKey] = document.getElementById(id));
+ });
+ }
+ },
+
+ initAppMenuButton(id, toolboxId) {
+ let button = document.getElementById(id);
+ if (!button) {
+ // If not in the document, the button should be in the toolbox palette,
+ // which isn't part of the document.
+ let toolbox = document.getElementById(toolboxId);
+ if (toolbox) {
+ button = toolbox.palette.querySelector(`#${id}`);
+ }
+ }
+
+ if (button) {
+ button.addEventListener("mousedown", PanelUI);
+ button.addEventListener("keypress", PanelUI);
+
+ this.kAppMenuButtons.add(button);
+ }
+ },
+
+ _eventListenersAdded: false,
+ _ensureEventListenersAdded() {
+ if (this._eventListenersAdded) {
+ return;
+ }
+ this._addEventListeners();
+ },
+
+ _addEventListeners() {
+ for (let event of this.kEvents) {
+ this.panel.addEventListener(event, this);
+ }
+ this._eventListenersAdded = true;
+ },
+
+ _removeEventListeners() {
+ for (let event of this.kEvents) {
+ this.panel.removeEventListener(event, this);
+ }
+ this._eventListenersAdded = false;
+ },
+
+ uninit() {
+ this._removeEventListeners();
+
+ Services.obs.removeObserver(this, "fullscreen-nav-toolbox");
+ Services.obs.removeObserver(this, "appMenu-notifications");
+
+ window.removeEventListener("MozDOMFullscreen:Entered", this);
+ window.removeEventListener("MozDOMFullscreen:Exited", this);
+ window.removeEventListener("fullscreen", this);
+ window.removeEventListener("activate", this);
+
+ [this.menuButtonMail, this.menuButtonChat].forEach(button => {
+ // There's no chat button in the messageWindow.xhtml context.
+ if (button) {
+ button.removeEventListener("mousedown", this);
+ button.removeEventListener("keypress", this);
+ }
+ });
+ },
+
+ /**
+ * Opens the menu panel if it's closed, or closes it if it's open.
+ *
+ * @param event the event that triggers the toggle.
+ */
+ toggle(event) {
+ // Don't show the panel if the window is in customization mode,
+ // since this button doubles as an exit path for the user in this case.
+ if (document.documentElement.hasAttribute("customizing")) {
+ return;
+ }
+
+ // Since we have several menu buttons, make sure the current one is used.
+ // This works for now, but in the long run, if we're showing badges etc.
+ // then the current menuButton needs to be set when the app's view/tab
+ // changes, not just when the menu is toggled.
+ this.menuButton = event.target;
+
+ this._ensureEventListenersAdded();
+ if (this.panel.state == "open") {
+ this.hide();
+ } else if (this.panel.state == "closed") {
+ this.show(event);
+ }
+ },
+
+ /**
+ * Opens the menu panel. If the event target has a child with the
+ * toolbarbutton-icon attribute, the panel will be anchored on that child.
+ * Otherwise, the panel is anchored on the event target itself.
+ *
+ * @param aEvent the event (if any) that triggers showing the menu.
+ */
+ show(aEvent) {
+ this._ensureShortcutsShown();
+ (async () => {
+ await this.ensureReady();
+
+ if (
+ this.panel.state == "open" ||
+ document.documentElement.hasAttribute("customizing")
+ ) {
+ return;
+ }
+
+ let domEvent = null;
+ if (aEvent && aEvent.type != "command") {
+ domEvent = aEvent;
+ }
+
+ // We try to use the event.target to account for clicks triggered
+ // from the #button-chat-appmenu. In case the opening of the menu isn't
+ // triggered by a click event, fallback to the main menu button as anchor.
+ let anchor = this._getPanelAnchor(
+ aEvent ? aEvent.target : this.menuButton
+ );
+ await PanelMultiView.openPopup(this.panel, anchor, {
+ triggerEvent: domEvent,
+ });
+ })().catch(console.error);
+ },
+
+ /**
+ * If the menu panel is being shown, hide it.
+ */
+ hide() {
+ if (document.documentElement.hasAttribute("customizing")) {
+ return;
+ }
+
+ PanelMultiView.hidePopup(this.panel);
+ },
+
+ observe(subject, topic, status) {
+ switch (topic) {
+ case "fullscreen-nav-toolbox":
+ if (this._notifications) {
+ this._updateNotifications(false);
+ }
+ break;
+ case "appMenu-notifications":
+ // Don't initialize twice.
+ if (status == "init" && this._notifications) {
+ break;
+ }
+ this._notifications = AppMenuNotifications.notifications;
+ this._updateNotifications(true);
+ break;
+ }
+ },
+
+ handleEvent(event) {
+ // Ignore context menus and menu button menus showing and hiding:
+ if (event.type.startsWith("popup") && event.target != this.panel) {
+ return;
+ }
+ switch (event.type) {
+ case "popupshowing":
+ initAppMenuPopup();
+ // Fall through
+ case "popupshown":
+ if (event.type == "popupshown") {
+ CustomizableUI.addPanelCloseListeners(this.panel);
+ }
+ // Fall through
+ case "popuphiding":
+ // Fall through
+ case "popuphidden":
+ this._updateNotifications();
+ this._updatePanelButton(event.target);
+ if (event.type == "popuphidden") {
+ CustomizableUI.removePanelCloseListeners(this.panel);
+ }
+ break;
+ case "mousedown":
+ if (event.button == 0) {
+ this.toggle(event);
+ }
+ break;
+ case "keypress":
+ if (event.key == " " || event.key == "Enter") {
+ this.toggle(event);
+ event.stopPropagation();
+ }
+ break;
+ case "MozDOMFullscreen:Entered":
+ case "MozDOMFullscreen:Exited":
+ case "fullscreen":
+ case "activate":
+ this._updateNotifications();
+ break;
+ case "ViewShowing":
+ PanelUI._handleViewShowingEvent(event);
+ break;
+ }
+ },
+
+ /**
+ * When a ViewShowing event happens when a <panelview> element is shown,
+ * do any required set up for that particular view.
+ *
+ * @param {ViewShowingEvent} event - ViewShowing event.
+ */
+ _handleViewShowingEvent(event) {
+ // Typically event.target for "ViewShowing" is a <panelview> element.
+ PanelUI._ensureShortcutsShown(event.target);
+
+ switch (event.target.id) {
+ case "appMenu-foldersView":
+ this._onFoldersViewShow(event);
+ break;
+ case "appMenu-addonsView":
+ initAddonPrefsMenu(
+ event.target.querySelector(".panel-subview-body"),
+ "toolbarbutton",
+ "subviewbutton subviewbutton-iconic",
+ "subviewbutton subviewbutton-iconic"
+ );
+ break;
+ case "appMenu-toolbarsView":
+ onViewToolbarsPopupShowing(
+ event,
+ "mail-toolbox",
+ document.getElementById("appmenu_quickFilterBar"),
+ "toolbarbutton",
+ "subviewbutton subviewbutton-iconic",
+ true
+ );
+ break;
+ case "appMenu-preferencesLayoutView":
+ PanelUI._onPreferencesLayoutViewShow(event);
+ break;
+ // View
+ case "appMenu-viewMessagesTagsView":
+ PanelUI._refreshDynamicView(event, RefreshTagsPopup);
+ break;
+ case "appMenu-viewMessagesCustomViewsView":
+ PanelUI._refreshDynamicView(event, RefreshCustomViewsPopup);
+ break;
+ }
+ },
+
+ /**
+ * Refreshes some views that are dynamically populated. Typically called by
+ * event listeners responding to a ViewShowing event. It calls a given refresh
+ * function (that populates the view), passing appmenu-specific arguments.
+ *
+ * @param {ViewShowingEvent} event - ViewShowing event.
+ * @param {Function} refreshFunction - Function that refreshes a particular view.
+ */
+ _refreshDynamicView(event, refreshFunction) {
+ refreshFunction(
+ event.target.querySelector(".panel-subview-body"),
+ "toolbarbutton",
+ "subviewbutton subviewbutton-iconic",
+ "toolbarseparator"
+ );
+ },
+
+ get isReady() {
+ return !!this._isReady;
+ },
+
+ /**
+ * 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
+ * mode will handle calling beginBatchUpdate and endBatchUpdate.
+ *
+ * @returns a Promise that resolves once the panel is ready to roll.
+ */
+ async ensureReady() {
+ if (this._isReady) {
+ return;
+ }
+
+ await window.delayedStartupPromise;
+ this._ensureEventListenersAdded();
+ this.panel.hidden = false;
+ this._isReady = true;
+ },
+
+ /**
+ * Shows a subview in the panel with a given ID.
+ *
+ * @param aViewId the ID of the subview to show.
+ * @param aAnchor the element that spawned the subview.
+ */
+ async showSubView(aViewId, aAnchor) {
+ this._ensureEventListenersAdded();
+ let viewNode = document.getElementById(aViewId);
+ if (!viewNode) {
+ console.error("Could not show panel subview with id: " + aViewId);
+ return;
+ }
+
+ if (!aAnchor) {
+ console.error(
+ "Expected an anchor when opening subview with id: " + aViewId
+ );
+ return;
+ }
+
+ let container = aAnchor.closest("panelmultiview");
+ if (container) {
+ container.showSubView(aViewId, aAnchor);
+ }
+ },
+
+ /**
+ * NB: The enable- and disableSingleSubviewPanelAnimations methods only
+ * affect the hiding/showing animations of single-subview panels (tempPanel
+ * in the showSubView method).
+ */
+ disableSingleSubviewPanelAnimations() {
+ this._disableAnimations = true;
+ },
+
+ enableSingleSubviewPanelAnimations() {
+ this._disableAnimations = false;
+ },
+
+ /**
+ * Sets the anchor node into the open or closed state, depending
+ * on the state of the panel.
+ */
+ _updatePanelButton() {
+ this.menuButton.open =
+ this.panel.state == "open" || this.panel.state == "showing";
+ },
+
+ /**
+ * Event handler for showing the Preferences/Layout view. Removes "checked"
+ * from all layout menu items and then checks the current layout menu item.
+ *
+ * @param {ViewShowingEvent} event - ViewShowing event.
+ */
+ _onPreferencesLayoutViewShow(event) {
+ event.target
+ .querySelectorAll("[name='viewlayoutgroup']")
+ .forEach(item => item.removeAttribute("checked"));
+
+ InitViewLayoutStyleMenu(event, true);
+ },
+
+ /**
+ * Event listener for showing the Folders view.
+ *
+ * @param {ViewShowingEvent} event - ViewShowing event.
+ */
+ _onFoldersViewShow(event) {
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ let folder = about3Pane.gFolder;
+
+ const paneHeaderMenuitem = event.target.querySelector(
+ '[name="paneheader"]'
+ );
+ if (about3Pane.folderPane.isFolderPaneHeaderHidden()) {
+ paneHeaderMenuitem.removeAttribute("checked");
+ } else {
+ paneHeaderMenuitem.setAttribute("checked", "true");
+ }
+
+ let { activeModes, canBeCompact, isCompact } = about3Pane.folderPane;
+ if (isCompact) {
+ activeModes.push("compact");
+ }
+
+ for (let item of event.target.querySelectorAll('[name="viewmessages"]')) {
+ let mode = item.getAttribute("value");
+ if (activeModes.includes(mode)) {
+ item.setAttribute("checked", "true");
+ if (mode == "all") {
+ item.disabled = activeModes.length == 1;
+ }
+ } else {
+ item.removeAttribute("checked");
+ }
+ if (mode == "compact") {
+ item.disabled = !canBeCompact;
+ }
+ }
+
+ goUpdateCommand("cmd_properties");
+ let propertiesMenuItem = document.getElementById("appmenu_properties");
+ if (folder?.server.type == "nntp") {
+ document.l10n.setAttributes(
+ propertiesMenuItem,
+ "menu-edit-newsgroup-properties"
+ );
+ } else {
+ document.l10n.setAttributes(
+ propertiesMenuItem,
+ "menu-edit-folder-properties"
+ );
+ }
+
+ let favoriteFolderMenu = document.getElementById("appmenu_favoriteFolder");
+ if (folder?.getFlag(Ci.nsMsgFolderFlags.Favorite)) {
+ favoriteFolderMenu.setAttribute("checked", "true");
+ } else {
+ favoriteFolderMenu.removeAttribute("checked");
+ }
+ },
+
+ _onToolsMenuShown(event) {
+ let noAccounts = MailServices.accounts.accounts.length == 0;
+ event.target.querySelector("#appmenu_searchCmd").disabled = noAccounts;
+ event.target.querySelector("#appmenu_filtersCmd").disabled = noAccounts;
+ },
+
+ _updateNotifications(notificationsChanged) {
+ let notifications = this._notifications;
+ if (!notifications || !notifications.length) {
+ if (notificationsChanged) {
+ this._clearAllNotifications();
+ }
+ return;
+ }
+
+ let doorhangers = 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;
+ if (n.options.onDismissed) {
+ n.options.onDismissed(window);
+ }
+ });
+ this._clearBadge();
+ 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 && this.autoHideToolbarInFullScreen) ||
+ Services.focus.activeWindow !== window
+ ) {
+ this._showBadge(doorhangers[0]);
+ this._showBannerItem(doorhangers[0]);
+ } else {
+ this._clearBadge();
+ }
+ } else {
+ this._showBadge(notifications[0]);
+ this._showBannerItem(notifications[0]);
+ }
+ },
+
+ _clearAllNotifications() {
+ this._clearBadge();
+ this._clearBannerItem();
+ },
+
+ _formatDescriptionMessage(n) {
+ let text = {};
+ let array = n.options.message.split("<>");
+ text.start = array[0] || "";
+ text.name = n.options.name || "";
+ text.end = array[1] || "";
+ return text;
+ },
+
+ _showBadge(notification) {
+ let badgeStatus = this._getBadgeStatus(notification);
+ for (let menuButton of this.kAppMenuButtons) {
+ menuButton.setAttribute("badge-status", badgeStatus);
+ }
+ },
+
+ // "Banner item" here refers to an item in the hamburger panel menu. They will
+ // typically show up as a colored row in the panel.
+ _showBannerItem(notification) {
+ const supportedIds = [
+ "update-downloading",
+ "update-available",
+ "update-manual",
+ "update-unsupported",
+ "update-restart",
+ ];
+ if (!supportedIds.includes(notification.id)) {
+ return;
+ }
+
+ if (!this._panelBannerItem) {
+ this._panelBannerItem = this.mainView.querySelector(".panel-banner-item");
+ }
+
+ let l10nId = "appmenuitem-banner-" + notification.id;
+ document.l10n.setAttributes(this._panelBannerItem, l10nId);
+
+ this._panelBannerItem.setAttribute("notificationid", notification.id);
+ this._panelBannerItem.hidden = false;
+ this._panelBannerItem.notification = notification;
+ },
+
+ _clearBadge() {
+ for (let menuButton of this.kAppMenuButtons) {
+ menuButton.removeAttribute("badge-status");
+ }
+ },
+
+ _clearBannerItem() {
+ if (this._panelBannerItem) {
+ this._panelBannerItem.notification = null;
+ this._panelBannerItem.hidden = true;
+ }
+ },
+
+ _onNotificationButtonEvent(event, type) {
+ let notificationEl = getNotificationFromElement(event.target);
+
+ if (!notificationEl) {
+ throw new Error(
+ "PanelUI._onNotificationButtonEvent: couldn't find notification element"
+ );
+ }
+
+ if (!notificationEl.notification) {
+ throw new Error(
+ "PanelUI._onNotificationButtonEvent: couldn't find notification"
+ );
+ }
+
+ let notification = notificationEl.notification;
+
+ if (type == "secondarybuttoncommand") {
+ AppMenuNotifications.callSecondaryAction(window, notification);
+ } else {
+ AppMenuNotifications.callMainAction(window, notification, true);
+ }
+ },
+
+ _onBannerItemSelected(event) {
+ let target = event.target;
+ if (!target.notification) {
+ throw new Error(
+ "menucommand target has no associated action/notification"
+ );
+ }
+
+ event.stopPropagation();
+ AppMenuNotifications.callMainAction(window, target.notification, false);
+ },
+
+ _getPopupId(notification) {
+ return "appMenu-" + notification.id + "-notification";
+ },
+
+ _getBadgeStatus(notification) {
+ return notification.id;
+ },
+
+ _getPanelAnchor(candidate) {
+ let iconAnchor = candidate.badgeStack || candidate.icon;
+ return iconAnchor || candidate;
+ },
+
+ _ensureShortcutsShown(view = this.mainView) {
+ if (view.hasAttribute("added-shortcuts")) {
+ return;
+ }
+ view.setAttribute("added-shortcuts", "true");
+ for (let button of view.querySelectorAll("toolbarbutton[key]")) {
+ let keyId = button.getAttribute("key");
+ let key = document.getElementById(keyId);
+ if (!key) {
+ continue;
+ }
+ button.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(key));
+ }
+ },
+
+ folderViewMenuOnCommand(event) {
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ if (!about3Pane) {
+ return;
+ }
+
+ let mode = event.target.getAttribute("value");
+ if (mode == "toggle-header") {
+ about3Pane.folderPane.toggleHeader(event.target.hasAttribute("checked"));
+ return;
+ }
+
+ let activeModes = about3Pane.folderPane.activeModes;
+ let index = activeModes.indexOf(mode);
+ if (event.target.hasAttribute("checked")) {
+ if (index == -1) {
+ activeModes.push(mode);
+ }
+ } else if (index >= 0) {
+ activeModes.splice(index, 1);
+ }
+ about3Pane.folderPane.activeModes = activeModes;
+
+ this._onFoldersViewShow({ target: event.target.parentNode });
+ },
+
+ folderCompactMenuOnCommand(event) {
+ let about3Pane = document.getElementById("tabmail").currentAbout3Pane;
+ if (!about3Pane) {
+ return;
+ }
+
+ about3Pane.folderPane.isCompact = event.target.hasAttribute("checked");
+ },
+
+ setUIDensity(event) {
+ // Loops through all available options and uncheck them. This is necessary
+ // since the toolbarbuttons don't uncheck themselves even if they're radio.
+ for (let item of event.originalTarget
+ .closest(".panel-subview-body")
+ .querySelectorAll("toolbarbutton")) {
+ // Skip this item if it's the one clicked.
+ if (item == event.originalTarget) {
+ continue;
+ }
+
+ item.removeAttribute("checked");
+ }
+ // Update the UI density.
+ UIDensity.setMode(event.originalTarget.mode);
+ },
+};
+
+XPCOMUtils.defineConstant(this, "PanelUI", PanelUI);
+
+/**
+ * Gets the currently selected locale for display.
+ *
+ * @returns the selected locale
+ */
+function getLocale() {
+ return Services.locale.appLocaleAsBCP47;
+}
+
+/**
+ * Given a DOM node inside a <popupnotification>, return the parent <popupnotification>.
+ */
+function getNotificationFromElement(aElement) {
+ return aElement.closest("popupnotification");
+}
+
+/**
+ * This object is Thunderbird's version of the same object in
+ * browser/base/content/browser-addons.js.
+ */
+var gExtensionsNotifications = {
+ initialized: false,
+ init() {
+ this.updateAlerts();
+ this.boundUpdate = this.updateAlerts.bind(this);
+ ExtensionsUI.on("change", this.boundUpdate);
+ this.initialized = true;
+ },
+
+ uninit() {
+ // uninit() can race ahead of init() in some cases, if that happens,
+ // we have no handler to remove.
+ if (!this.initialized) {
+ return;
+ }
+ ExtensionsUI.off("change", this.boundUpdate);
+ },
+
+ get l10n() {
+ if (this._l10n) {
+ return this._l10n;
+ }
+ return (this._l10n = new Localization(
+ ["messenger/addonNotifications.ftl", "branding/brand.ftl"],
+ true
+ ));
+ },
+
+ _createAddonButton(l10nId, addon, callback) {
+ let text = this.l10n.formatValueSync(l10nId, { addonName: addon.name });
+ let button = document.createXULElement("toolbarbutton");
+ button.setAttribute("wrap", "true");
+ button.setAttribute("label", text);
+ button.setAttribute("tooltiptext", text);
+ const DEFAULT_EXTENSION_ICON =
+ "chrome://messenger/skin/icons/new/compact/extension.svg";
+ button.setAttribute("image", addon.iconURL || DEFAULT_EXTENSION_ICON);
+ button.className = "addon-banner-item subviewbutton";
+
+ button.addEventListener("command", callback);
+ PanelUI.addonNotificationContainer.appendChild(button);
+ },
+
+ updateAlerts() {
+ let gBrowser = document.getElementById("tabmail");
+ let sideloaded = ExtensionsUI.sideloaded;
+ let updates = ExtensionsUI.updates;
+
+ let container = PanelUI.addonNotificationContainer;
+
+ while (container.firstChild) {
+ container.firstChild.remove();
+ }
+
+ let items = 0;
+ for (let update of updates) {
+ if (++items > 4) {
+ break;
+ }
+ this._createAddonButton(
+ "webext-perms-update-menu-item",
+ update.addon,
+ evt => {
+ ExtensionsUI.showUpdate(gBrowser, update);
+ }
+ );
+ }
+
+ for (let addon of sideloaded) {
+ if (++items > 4) {
+ break;
+ }
+ this._createAddonButton("webext-perms-sideload-menu-item", addon, evt => {
+ // We need to hide the main menu manually because the toolbarbutton is
+ // removed immediately while processing this event, and PanelUI is
+ // unable to identify which panel should be closed automatically.
+ PanelUI.hide();
+ ExtensionsUI.showSideloaded(gBrowser, addon);
+ });
+ }
+ },
+};
+
+addEventListener("unload", () => gExtensionsNotifications.uninit(), {
+ once: true,
+});
diff --git a/comm/mail/components/customizableui/moz.build b/comm/mail/components/customizableui/moz.build
new file mode 100644
index 0000000000..4bc53e73ea
--- /dev/null
+++ b/comm/mail/components/customizableui/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "content",
+]
+
+EXTRA_JS_MODULES += [
+ "CustomizableUI.sys.mjs",
+ "PanelMultiView.sys.mjs",
+]