author | Drew Willcoxon <adw@mozilla.com> |
Fri, 27 Oct 2017 17:39:38 -0400 | |
changeset 389048 | b5b0ac43e2e89c84a670d590862b4d0d22d069d1 |
parent 389047 | 6a3ca81dc56ab7687ca70b1b98fbe397aa3fa388 |
child 389049 | d887685b52ae3890123648911f884873de91c42a |
push id | 32777 |
push user | archaeopteryx@coole-files.de |
push date | Mon, 30 Oct 2017 22:44:45 +0000 |
treeherder | mozilla-central@dd0f265a1300 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | Gijs |
bugs | 1395387 |
milestone | 58.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/base/content/browser-pageActions.js +++ b/browser/base/content/browser-pageActions.js @@ -90,29 +90,30 @@ var BrowserPageActions = { /** * Adds or removes as necessary DOM nodes for the action in the panel. * * @param action (PageActions.Action, required) * The action to place. */ placeActionInPanel(action) { let insertBeforeID = PageActions.nextActionIDInPanel(action); - let id = this._panelButtonNodeIDForActionID(action.id); + let id = this.panelButtonNodeIDForActionID(action.id); let node = document.getElementById(id); if (!node) { let panelViewNode; [node, panelViewNode] = this._makePanelButtonNodeForAction(action); node.id = id; let insertBeforeNode = null; if (insertBeforeID) { let insertBeforeNodeID = - this._panelButtonNodeIDForActionID(insertBeforeID); + this.panelButtonNodeIDForActionID(insertBeforeID); insertBeforeNode = document.getElementById(insertBeforeNodeID); } this.mainViewBodyNode.insertBefore(node, insertBeforeNode); + this.updateAction(action); action.onPlacedInPanel(node); if (panelViewNode) { action.subview.onPlaced(panelViewNode); } } }, _makePanelButtonNodeForAction(action) { @@ -122,20 +123,16 @@ var BrowserPageActions = { } let buttonNode = document.createElement("toolbarbutton"); buttonNode.classList.add( "subviewbutton", "subviewbutton-iconic", "pageAction-panel-button" ); - buttonNode.setAttribute("label", action.title); - if (action.iconURL) { - buttonNode.style.listStyleImage = `url('${action.iconURL}')`; - } if (action.nodeAttributes) { for (let name in action.nodeAttributes) { buttonNode.setAttribute(name, action.nodeAttributes[name]); } } let panelViewNode = null; if (action.subview) { buttonNode.classList.add("subviewbutton-nav"); @@ -172,28 +169,59 @@ var BrowserPageActions = { buttonNode.addEventListener("command", event => { button.onCommand(event, buttonNode); }); bodyNode.appendChild(buttonNode); } return panelViewNode; }, - _toggleActivatedActionPanelForAction(action) { - let panelNode = this.activatedActionPanelNode; + /** + * Shows or hides a panel for an action. You can supply your own panel; + * otherwise one is created. + * + * @param action (PageActions.Action, required) + * The action for which to toggle the panel. If the action is in the + * urlbar, then the panel will be anchored to it. Otherwise, a + * suitable anchor will be used. + * @param panelNode (DOM node, optional) + * The panel to use. This method takes a hands-off approach with + * regard to your panel in terms of attributes, styling, etc. + */ + togglePanelForAction(action, panelNode = null) { + let aaPanelNode = this.activatedActionPanelNode; if (panelNode) { - panelNode.hidePopup(); - return null; + if (panelNode.state != "closed") { + panelNode.hidePopup(); + return; + } + if (aaPanelNode) { + aaPanelNode.hidePopup(); + } + } else if (aaPanelNode) { + aaPanelNode.hidePopup(); + return; + } else { + panelNode = this._makeActivatedActionPanelForAction(action); } - // Before creating the panel, get the anchor node for it because it'll throw - // if there isn't one (which shouldn't happen, but still). + // Hide the main panel before showing the action's panel. + this.panelNode.hidePopup(); + let anchorNode = this.panelAnchorNodeForAction(action); + anchorNode.setAttribute("open", "true"); + panelNode.addEventListener("popuphiding", () => { + anchorNode.removeAttribute("open"); + }, { once: true }); - panelNode = document.createElement("panel"); + panelNode.openPopup(anchorNode, "bottomcenter topright"); + }, + + _makeActivatedActionPanelForAction(action) { + let panelNode = document.createElement("panel"); panelNode.id = this._activatedActionPanelID; panelNode.classList.add("cui-widget-panel"); panelNode.setAttribute("actionID", action.id); panelNode.setAttribute("role", "group"); panelNode.setAttribute("type", "arrow"); panelNode.setAttribute("flip", "slide"); panelNode.setAttribute("noautofocus", "true"); panelNode.setAttribute("tabspecific", "true"); @@ -221,38 +249,30 @@ var BrowserPageActions = { popupSet.appendChild(panelNode); panelNode.addEventListener("popuphidden", () => { if (iframeNode) { action.onIframeHidden(iframeNode, panelNode); } panelNode.remove(); }, { once: true }); - panelNode.addEventListener("popuphiding", () => { - if (iframeNode) { + if (iframeNode) { + panelNode.addEventListener("popupshown", () => { + action.onIframeShown(iframeNode, panelNode); + }, { once: true }); + panelNode.addEventListener("popuphiding", () => { action.onIframeHiding(iframeNode, panelNode); - } - anchorNode.removeAttribute("open"); - }, { once: true }); + }, { once: true }); + } if (panelViewNode) { action.subview.onPlaced(panelViewNode); action.subview.onShowing(panelViewNode); } - // Hide the main page action panel before showing the activated-action - // panel. - this.panelNode.hidePopup(); - panelNode.openPopup(anchorNode, "bottomcenter topright"); - anchorNode.setAttribute("open", "true"); - - if (iframeNode) { - action.onIframeShown(iframeNode, panelNode); - } - return panelNode; }, // For tests. get _disablePanelAnimations() { return this.__disablePanelAnimations || false; }, set _disablePanelAnimations(val) { @@ -269,24 +289,24 @@ var BrowserPageActions = { * be anchored. If the action is null, a sensible anchor is returned. * * @param action (PageActions.Action, optional) * The action you want to anchor. * @return (DOM node, nonnull) The node to which the action should be * anchored. */ panelAnchorNodeForAction(action, event) { - // Try each of the following nodes in order, using the first that's visible. if (event && event.target.closest("panel")) { return this.mainButtonNode; } + // Try each of the following nodes in order, using the first that's visible. let potentialAnchorNodeIDs = [ action && action.anchorIDOverride, - action && this._urlbarButtonNodeIDForActionID(action.id), + action && this.urlbarButtonNodeIDForActionID(action.id), this.mainButtonNode.id, "identity-icon", ]; let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); for (let id of potentialAnchorNodeIDs) { if (id) { let node = document.getElementById(id); @@ -313,17 +333,17 @@ var BrowserPageActions = { /** * Adds or removes as necessary a DOM node for the given action in the urlbar. * * @param action (PageActions.Action, required) * The action to place. */ placeActionInUrlbar(action) { let insertBeforeID = PageActions.nextActionIDInUrlbar(action); - let id = this._urlbarButtonNodeIDForActionID(action.id); + let id = this.urlbarButtonNodeIDForActionID(action.id); let node = document.getElementById(id); if (!action.shownInUrlbar) { if (node) { if (action.__urlbarNodeInMarkup) { node.hidden = true; } else { node.remove(); @@ -342,46 +362,41 @@ var BrowserPageActions = { node.id = id; } if (newlyPlaced) { let parentNode = this.mainButtonNode.parentNode; let insertBeforeNode = null; if (insertBeforeID) { let insertBeforeNodeID = - this._urlbarButtonNodeIDForActionID(insertBeforeID); + this.urlbarButtonNodeIDForActionID(insertBeforeID); insertBeforeNode = document.getElementById(insertBeforeNodeID); } parentNode.insertBefore(node, insertBeforeNode); + this.updateAction(action); action.onPlacedInUrlbar(node); // urlbar buttons should always have tooltips, so if the node doesn't have // one, then as a last resort use the label of the corresponding panel // button. Why not set tooltiptext to action.title when the node is // created? Because the consumer may set a title dynamically. if (!node.hasAttribute("tooltiptext")) { - let panelNodeID = this._panelButtonNodeIDForActionID(action.id); + let panelNodeID = this.panelButtonNodeIDForActionID(action.id); let panelNode = document.getElementById(panelNodeID); if (panelNode) { node.setAttribute("tooltiptext", panelNode.getAttribute("label")); } } } }, _makeUrlbarButtonNode(action) { let buttonNode = document.createElement("image"); buttonNode.classList.add("urlbar-icon", "urlbar-page-action"); buttonNode.setAttribute("role", "button"); - if (action.tooltip) { - buttonNode.setAttribute("tooltiptext", action.tooltip); - } - if (action.iconURL) { - buttonNode.style.listStyleImage = `url('${action.iconURL}')`; - } buttonNode.setAttribute("context", "pageActionPanelContextMenu"); buttonNode.addEventListener("contextmenu", event => { BrowserPageActions.onContextMenu(event); }); if (action.nodeAttributes) { for (let name in action.nodeAttributes) { buttonNode.setAttribute(name, action.nodeAttributes[name]); } @@ -399,85 +414,142 @@ var BrowserPageActions = { * The action to remove. */ removeAction(action) { this._removeActionFromPanel(action); this._removeActionFromUrlbar(action); }, _removeActionFromPanel(action) { - let id = this._panelButtonNodeIDForActionID(action.id); + let id = this.panelButtonNodeIDForActionID(action.id); let node = document.getElementById(id); if (node) { node.remove(); } if (action.subview) { let panelViewNodeID = this._panelViewNodeIDForActionID(action.id, false); let panelViewNode = document.getElementById(panelViewNodeID); if (panelViewNode) { panelViewNode.remove(); } } // If there are now no more non-built-in actions, remove the separator // between the built-ins and non-built-ins. if (!PageActions.nonBuiltInActions.length) { let separator = document.getElementById( - this._panelButtonNodeIDForActionID( + this.panelButtonNodeIDForActionID( PageActions.ACTION_ID_BUILT_IN_SEPARATOR ) ); if (separator) { separator.remove(); } } }, _removeActionFromUrlbar(action) { - let id = this._urlbarButtonNodeIDForActionID(action.id); + let id = this.urlbarButtonNodeIDForActionID(action.id); let node = document.getElementById(id); if (node) { node.remove(); } }, /** - * Updates the DOM nodes of an action to reflect its changed iconURL. + * Updates the DOM nodes of an action to reflect either a changed property or + * all properties. * * @param action (PageActions.Action, required) * The action to update. + * @param nameToUpdate (string, optional) + * The property's name. If not given, then DOM nodes will be updated + * to reflect the current values of all properties. */ - updateActionIconURL(action) { - let url = action.iconURL ? `url('${action.iconURL}')` : null; + updateAction(action, nameToUpdate = null) { + let names = nameToUpdate ? [nameToUpdate] : [ + "disabled", + "iconURL", + "title", + "tooltip", + ]; + for (let name of names) { + let upper = name[0].toUpperCase() + name.substr(1); + this[`_updateAction${upper}`](action); + } + }, + + _updateActionDisabled(action) { let nodeIDs = [ - this._panelButtonNodeIDForActionID(action.id), - this._urlbarButtonNodeIDForActionID(action.id), + this.panelButtonNodeIDForActionID(action.id), + this.urlbarButtonNodeIDForActionID(action.id), ]; for (let nodeID of nodeIDs) { let node = document.getElementById(nodeID); if (node) { - if (url) { - node.style.listStyleImage = url; + if (action.getDisabled(window)) { + node.setAttribute("disabled", "true"); } else { - node.style.removeProperty("list-style-image"); + node.removeAttribute("disabled"); } } } }, - /** - * Updates the DOM nodes of an action to reflect its changed title. - * - * @param action (PageActions.Action, required) - * The action to update. - */ - updateActionTitle(action) { - let id = this._panelButtonNodeIDForActionID(action.id); - let node = document.getElementById(id); + _updateActionIconURL(action) { + let nodeIDs = [ + this.panelButtonNodeIDForActionID(action.id), + this.urlbarButtonNodeIDForActionID(action.id), + ]; + for (let nodeID of nodeIDs) { + let node = document.getElementById(nodeID); + if (node) { + for (let size of [16, 32]) { + let url = action.iconURLForSize(size, window); + let prop = `--pageAction-image-${size}px`; + if (url) { + node.style.setProperty(prop, `url("${url}")`); + } else { + node.style.removeProperty(prop); + } + } + } + } + }, + + _updateActionTitle(action) { + let title = action.getTitle(window); + if (!title) { + // `title` is a required action property, but the bookmark action's is an + // empty string since its actual title is set via + // BookmarkingUI.updateBookmarkPageMenuItem(). The purpose of this early + // return is to ignore that empty title. + return; + } + let attrNamesByNodeIDFnName = { + panelButtonNodeIDForActionID: "label", + urlbarButtonNodeIDForActionID: "aria-label", + }; + for (let [fnName, attrName] of Object.entries(attrNamesByNodeIDFnName)) { + let nodeID = this[fnName](action.id); + let node = document.getElementById(nodeID); + if (node) { + node.setAttribute(attrName, title); + } + } + // tooltiptext falls back to the title, so update it, too. + this._updateActionTooltip(action); + }, + + _updateActionTooltip(action) { + let node = document.getElementById( + this.urlbarButtonNodeIDForActionID(action.id) + ); if (node) { - node.setAttribute("label", action.title); + let tooltip = action.getTooltip(window) || action.getTitle(window); + node.setAttribute("tooltiptext", tooltip); } }, doCommandForAction(action, event, buttonNode) { if (event && event.type == "click" && event.button != 0) { return; } PageActions.logTelemetry("used", action, buttonNode); @@ -491,17 +563,17 @@ var BrowserPageActions = { this.multiViewNode.showSubView(panelViewNode, buttonNode); return; } // Otherwise, hide the main popup in case it was open: this.panelNode.hidePopup(); // Toggle the activated action's panel if necessary if (action.subview || action.wantsIframe) { - this._toggleActivatedActionPanelForAction(action); + this.togglePanelForAction(action); return; } // Otherwise, run the action. action.onCommand(event, buttonNode); }, /** @@ -530,23 +602,35 @@ var BrowserPageActions = { } actionID = this._actionIDForNodeID(n.id); action = PageActions.actionForID(actionID); } } return action; }, - // The ID of the given action's top-level button in the panel. - _panelButtonNodeIDForActionID(actionID) { + /** + * The ID of the given action's top-level button in the main panel. + * + * @param actionID (string, required) + * The action ID. + * @return (string) The ID of the action's button in the main panel. + */ + panelButtonNodeIDForActionID(actionID) { return `pageAction-panel-${actionID}`; }, - // The ID of the given action's button in the urlbar. - _urlbarButtonNodeIDForActionID(actionID) { + /** + * The ID of the given action's button in the urlbar. + * + * @param actionID (string, required) + * The action ID. + * @return (string) The ID of the action's urlbar button node. + */ + urlbarButtonNodeIDForActionID(actionID) { let action = PageActions.actionForID(actionID); if (action && action.urlbarIDOverride) { return action.urlbarIDOverride; } return `pageAction-urlbar-${actionID}`; }, // The ID of the given action's panelview. @@ -613,17 +697,17 @@ var BrowserPageActions = { * Show the page action panel * * @param event (DOM event, optional) * The event that triggers showing the panel. (such as a mouse click, * if the user clicked something to open the panel) */ showPanel(event = null) { for (let action of PageActions.actions) { - let buttonNodeID = this._panelButtonNodeIDForActionID(action.id); + let buttonNodeID = this.panelButtonNodeIDForActionID(action.id); let buttonNode = document.getElementById(buttonNodeID); action.onShowingInPanel(buttonNode); } this.panelNode.hidden = false; this.panelNode.addEventListener("popuphiding", () => { this.mainButtonNode.removeAttribute("open"); }, {once: true}); @@ -688,30 +772,47 @@ var BrowserPageActions = { let telemetryType = this._contextAction.shownInUrlbar ? "removed" : "added"; PageActions.logTelemetry(telemetryType, this._contextAction); this._contextAction.shownInUrlbar = !this._contextAction.shownInUrlbar; }, _contextAction: null, /** - * A bunch of strings (labels for actions and the like) are defined in DTD, - * but actions are created in JS. So what we do is add a bunch of attributes - * to the page action panel's definition in the markup, whose values hold - * these DTD strings. Then when each built-in action is set up, we get the - * related strings from the panel node and set up the action's node with them. + * Titles for a few of the built-in actions are defined in DTD, but the + * actions are created in JS. So what we do is for each title, set an + * attribute in markup on the main page action panel whose value is the DTD + * string. In gBuiltInActions, where the built-in actions are defined, we set + * the action's initial title to the name of this attribute. Then when the + * action is set up, we get the action's current title, and then get the + * attribute on the main panel whose name is that title. If the attribute + * exists, then its value is the actual title, and we update the action with + * this title. Otherwise the action's title has already been set up in this + * manner. * - * The convention is to set for example the "title" property in an action's JS - * definition to the name of the attribute on the panel node that holds the - * actual title string. Then call this function, passing the action's related - * DOM node and the name of the attribute that you are setting on the DOM - * node -- "label" or "title" in this example (either will do). + * @param action (PageActions.Action, required) + * The action whose title you're setting. + */ + takeActionTitleFromPanel(action) { + let titleOrAttrNameOnPanel = action.getTitle(); + let attrValueOnPanel = this.panelNode.getAttribute(titleOrAttrNameOnPanel); + if (attrValueOnPanel) { + this.panelNode.removeAttribute(titleOrAttrNameOnPanel); + action.setTitle(attrValueOnPanel); + } + }, + + /** + * This is similar to takeActionTitleFromPanel, except it sets an attribute on + * a DOM node instead of setting the title on an action. The point is to map + * attributes on the node to strings on the main panel. Use this for DOM + * nodes that don't correspond to actions, like buttons in subviews. * * @param node (DOM node, required) - * The node of an action you're setting up. + * The node you're setting up. * @param attrName (string, required) * The name of the attribute *on the node you're setting up*. */ takeNodeAttributeFromPanel(node, attrName) { let panelAttrName = node.getAttribute(attrName); if (!panelAttrName && attrName == "title") { attrName = "label"; panelAttrName = node.getAttribute(attrName); @@ -729,16 +830,17 @@ var BrowserPageActions = { */ onLocationChange() { for (let action of PageActions.actions) { action.onLocationChange(window); } }, }; + var BrowserPageActionFeedback = { /** * The feedback page action panel DOM node (DOM node) */ get panelNode() { delete this.panelNode; return this.panelNode = document.getElementById("pageActionFeedback"); }, @@ -773,16 +875,17 @@ var BrowserPageActionFeedback = { }, Services.prefs.getIntPref("browser.pageActions.feedbackTimeoutMS", 1120)); }, {once: true}); this.panelNode.addEventListener("popuphidden", () => { this.feedbackAnimationBox.removeAttribute("animate"); }, {once: true}); }, }; + // built-in actions below ////////////////////////////////////////////////////// // bookmark BrowserPageActions.bookmark = { onShowingInPanel(buttonNode) { // Update the button label via the bookmark observer. BookmarkingUI.updateBookmarkPageMenuItem(); }, @@ -791,45 +894,48 @@ BrowserPageActions.bookmark = { BrowserPageActions.panelNode.hidePopup(); BookmarkingUI.onStarCommand(event); }, }; // copy URL BrowserPageActions.copyURL = { onPlacedInPanel(buttonNode) { - BrowserPageActions.takeNodeAttributeFromPanel(buttonNode, "title"); + let action = PageActions.actionForID("copyURL"); + BrowserPageActions.takeActionTitleFromPanel(action); }, onCommand(event, buttonNode) { BrowserPageActions.panelNode.hidePopup(); Cc["@mozilla.org/widget/clipboardhelper;1"] .getService(Ci.nsIClipboardHelper) .copyString(gURLBar.makeURIReadable(gBrowser.selectedBrowser.currentURI).displaySpec); let action = PageActions.actionForID("copyURL"); BrowserPageActionFeedback.show(action, event); }, }; // email link BrowserPageActions.emailLink = { onPlacedInPanel(buttonNode) { - BrowserPageActions.takeNodeAttributeFromPanel(buttonNode, "title"); + let action = PageActions.actionForID("emailLink"); + BrowserPageActions.takeActionTitleFromPanel(action); }, onCommand(event, buttonNode) { BrowserPageActions.panelNode.hidePopup(); MailIntegration.sendLinkForBrowser(gBrowser.selectedBrowser); }, }; // send to device BrowserPageActions.sendToDevice = { onPlacedInPanel(buttonNode) { - BrowserPageActions.takeNodeAttributeFromPanel(buttonNode, "title"); + let action = PageActions.actionForID("sendToDevice"); + BrowserPageActions.takeActionTitleFromPanel(action); }, onSubviewPlaced(panelViewNode) { let bodyNode = panelViewNode.firstChild; for (let node of bodyNode.childNodes) { BrowserPageActions.takeNodeAttributeFromPanel(node, "title"); BrowserPageActions.takeNodeAttributeFromPanel(node, "shortcut"); }
--- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -1430,16 +1430,37 @@ toolbarpaletteitem[place="palette"][hidd } /* Page action panel */ #pageAction-panel-sendToDevice-subview-body:not([state="notready"]) > #pageAction-panel-sendToDevice-notReady, #pageAction-urlbar-sendToDevice-subview-body:not([state="notready"]) > #pageAction-urlbar-sendToDevice-notReady { display: none; } +/* Page action buttons */ +.pageAction-panel-button > .toolbarbutton-icon { + list-style-image: var(--pageAction-image-16px, inherit); +} +.urlbar-page-action { + list-style-image: var(--pageAction-image-16px, inherit); +} +@media (min-resolution: 1.1dppx) { + .pageAction-panel-button > .toolbarbutton-icon { + list-style-image: var(--pageAction-image-32px, inherit); + } + .urlbar-page-action { + list-style-image: var(--pageAction-image-32px, inherit); + } +} + +.urlbar-page-action[disabled] { + pointer-events: none; + -moz-user-focus: ignore; +} + /* WebExtension Sidebars */ #sidebar-box[sidebarcommand$="-sidebar-action"] > #sidebar-header > #sidebar-switcher-target > #sidebar-icon { list-style-image: var(--webextension-menuitem-image, inherit); -moz-context-properties: fill; fill: currentColor; width: 16px; height: 16px; }
--- a/browser/base/content/test/urlbar/browser_page_action_menu.js +++ b/browser/base/content/test/urlbar/browser_page_action_menu.js @@ -543,17 +543,17 @@ add_task(async function sendToDevice_inU registerCleanupFunction(cleanUp); // Add Send to Device to the urlbar. let action = PageActions.actionForID("sendToDevice"); action.shownInUrlbar = true; // Click it to open its panel. let urlbarButton = document.getElementById( - BrowserPageActions._urlbarButtonNodeIDForActionID(action.id) + BrowserPageActions.urlbarButtonNodeIDForActionID(action.id) ); Assert.ok(!urlbarButton.disabled); let panelPromise = promisePanelShown(BrowserPageActions._activatedActionPanelID); EventUtils.synthesizeMouseAtCenter(urlbarButton, {}); await panelPromise; Assert.equal(urlbarButton.getAttribute("open"), "true", "Button has open attribute");
--- a/browser/extensions/pocket/bootstrap.js +++ b/browser/extensions/pocket/bootstrap.js @@ -213,17 +213,17 @@ var PocketPageAction = { // Sets or removes the "pocketed" attribute on the Pocket urlbar button as // necessary. updateUrlbarNodeState(browserWindow) { if (!this.pageAction) { return; } let {BrowserPageActions} = browserWindow; let urlbarNode = browserWindow.document.getElementById( - BrowserPageActions._urlbarButtonNodeIDForActionID(this.pageAction.id) + BrowserPageActions.urlbarButtonNodeIDForActionID(this.pageAction.id) ); if (!urlbarNode) { return; } let browser = browserWindow.gBrowser.selectedBrowser; let pocketedInnerWindowID = this.innerWindowIDsByBrowser.get(browser); if (pocketedInnerWindowID == browser.innerWindowID) { // The current window in this browser is pocketed.
--- a/browser/modules/PageActions.jsm +++ b/browser/modules/PageActions.jsm @@ -205,17 +205,17 @@ this.PageActions = { } else if (gBuiltInActions.find(a => a.id == action.id)) { // A built-in action. These are always added on init before all other // actions, one after the other, so just push onto the array. this._builtInActions.push(action); } else { // A non-built-in action, like a non-bundled extension potentially. // Keep this list sorted by title. let index = BinarySearch.insertionIndexOf((a1, a2) => { - return a1.title.localeCompare(a2.title); + return a1.getTitle().localeCompare(a2.getTitle()); }, this._nonBuiltInActions, action); this._nonBuiltInActions.splice(index, 0, action); } if (this._persistedActions.ids.includes(action.id)) { // The action has been seen before. Override its shownInUrlbar value // with the persisted value. Set the private version of that property // so that onActionToggledShownInUrlbar isn't called, which happens when @@ -346,48 +346,16 @@ this.PageActions = { this._storePersistedActions(); for (let bpa of allBrowserPageActions()) { bpa.removeAction(action); } }, /** - * Call this when an action's iconURL changes. - * - * @param action (Action object, required) - * The action whose iconURL property changed. - */ - onActionSetIconURL(action) { - if (!this.actionForID(action.id)) { - // This may be called before the action has been added. - return; - } - for (let bpa of allBrowserPageActions()) { - bpa.updateActionIconURL(action); - } - }, - - /** - * Call this when an action's title changes. - * - * @param action (Action object, required) - * The action whose title property changed. - */ - onActionSetTitle(action) { - if (!this.actionForID(action.id)) { - // This may be called before the action has been added. - return; - } - for (let bpa of allBrowserPageActions()) { - bpa.updateActionTitle(action); - } - }, - - /** * Call this when an action's shownInUrlbar property changes. * * @param action (Action object, required) * The action whose shownInUrlbar property changed. */ onActionToggledShownInUrlbar(action) { if (!this.actionForID(action.id)) { // This may be called before the action has been added. @@ -472,103 +440,114 @@ this.PageActions = { // action IDs ordered by position in urlbar idsInUrlbar: [], }, }; /** * A single page action. * - * @param options (object, required) - * An object with the following properties: - * @param id (string, required) - * The action's ID. Treat this like the ID of a DOM node. - * @param title (string, required) - * The action's title. - * @param anchorIDOverride (string, optional) - * Pass a string for this property if to override which element - * that the temporary panel is anchored to. - * @param iconURL (string, optional) - * The URL of the action's icon. Usually you want to specify an - * icon in CSS, but this option is useful if that would be a pain - * for some reason -- like your code is in an embedded - * WebExtension. - * @param nodeAttributes (object, optional) - * An object of name-value pairs. Each pair will be added as - * an attribute to DOM nodes created for this action. - * @param onBeforePlacedInWindow (function, optional) - * Called before the action is placed in the window: - * onBeforePlacedInWindow(window) - * * window: The window that the action will be placed in. - * @param onCommand (function, optional) - * Called when the action is clicked, but only if it has neither - * a subview nor an iframe: - * onCommand(event, buttonNode) - * * event: The triggering event. - * * buttonNode: The button node that was clicked. - * @param onIframeHiding (function, optional) - * Called when the action's iframe is hiding: - * onIframeHiding(iframeNode, parentPanelNode) - * * iframeNode: The iframe. - * * parentPanelNode: The panel node in which the iframe is - * shown. - * @param onIframeHidden (function, optional) - * Called when the action's iframe is hidden: - * onIframeHidden(iframeNode, parentPanelNode) - * * iframeNode: The iframe. - * * parentPanelNode: The panel node in which the iframe is - * shown. - * @param onIframeShown (function, optional) - * Called when the action's iframe is shown to the user: - * onIframeShown(iframeNode, parentPanelNode) - * * iframeNode: The iframe. - * * parentPanelNode: The panel node in which the iframe is - * shown. - * @param onLocationChange (function, optional) - * Called after tab switch or when the current <browser>'s - * location changes: - * onLocationChange(browserWindow) - * * browserWindow: The browser window containing the tab switch - * or changed <browser>. - * @param onPlacedInPanel (function, optional) - * Called when the action is added to the page action panel in - * a browser window: - * onPlacedInPanel(buttonNode) - * * buttonNode: The action's node in the page action panel. - * @param onPlacedInUrlbar (function, optional) - * Called when the action is added to the urlbar in a browser - * window: - * onPlacedInUrlbar(buttonNode) - * * buttonNode: The action's node in the urlbar. - * @param onShowingInPanel (function, optional) - * Called when a browser window's page action panel is showing: - * onShowingInPanel(buttonNode) - * * buttonNode: The action's node in the page action panel. - * @param shownInUrlbar (bool, optional) - * Pass true to show the action in the urlbar, false otherwise. - * False by default. - * @param subview (object, optional) - * An options object suitable for passing to the Subview - * constructor, if you'd like the action to have a subview. See - * the subview constructor for info on this object's properties. - * @param tooltip (string, optional) - * The action's button tooltip text. - * @param urlbarIDOverride (string, optional) - * Usually the ID of the action's button in the urlbar will be - * generated automatically. Pass a string for this property to - * override that with your own ID. - * @param wantsIframe (bool, optional) - * Pass true to make an action that shows an iframe in a panel - * when clicked. + * Each action can have both per-browser-window state and global state. + * Per-window state takes precedence over global state. This is reflected in + * the title, tooltip, disabled, and icon properties. Each of these properties + * has a getter method and setter method that takes a browser window. Pass null + * to get the action's global state. Pass a browser window to get the per- + * window state. However, if you pass a window and the action has no state for + * that window, then the global state will be returned. + * + * `options` is a required object with the following properties. Regarding the + * properties discussed in the previous paragraph, the values in `options` set + * global state. + * + * @param id (string, required) + * The action's ID. Treat this like the ID of a DOM node. + * @param title (string, required) + * The action's title. + * @param anchorIDOverride (string, optional) + * Pass a string to override the node to which the action's activated- + * action panel is anchored. + * @param disabled (bool, optional) + * Pass true to cause the action to be disabled initially in all browser + * windows. False by default. + * @param iconURL (string or object, optional) + * The URL string of the action's icon. Usually you want to specify an + * icon in CSS, but this option is useful if that would be a pain for + * some reason. You can also pass an object that maps pixel sizes to + * URLs, like { 16: url16, 32: url32 }. The best size for the user's + * screen will be used. + * @param nodeAttributes (object, optional) + * An object of name-value pairs. Each pair will be added as an + * attribute to DOM nodes created for this action. + * @param onBeforePlacedInWindow (function, optional) + * Called before the action is placed in the window: + * onBeforePlacedInWindow(window) + * * window: The window that the action will be placed in. + * @param onCommand (function, optional) + * Called when the action is clicked, but only if it has neither a + * subview nor an iframe: + * onCommand(event, buttonNode) + * * event: The triggering event. + * * buttonNode: The button node that was clicked. + * @param onIframeHiding (function, optional) + * Called when the action's iframe is hiding: + * onIframeHiding(iframeNode, parentPanelNode) + * * iframeNode: The iframe. + * * parentPanelNode: The panel node in which the iframe is shown. + * @param onIframeHidden (function, optional) + * Called when the action's iframe is hidden: + * onIframeHidden(iframeNode, parentPanelNode) + * * iframeNode: The iframe. + * * parentPanelNode: The panel node in which the iframe is shown. + * @param onIframeShown (function, optional) + * Called when the action's iframe is shown to the user: + * onIframeShown(iframeNode, parentPanelNode) + * * iframeNode: The iframe. + * * parentPanelNode: The panel node in which the iframe is shown. + * @param onLocationChange (function, optional) + * Called after tab switch or when the current <browser>'s location + * changes: + * onLocationChange(browserWindow) + * * browserWindow: The browser window containing the tab switch or + * changed <browser>. + * @param onPlacedInPanel (function, optional) + * Called when the action is added to the page action panel in a browser + * window: + * onPlacedInPanel(buttonNode) + * * buttonNode: The action's node in the page action panel. + * @param onPlacedInUrlbar (function, optional) + * Called when the action is added to the urlbar in a browser window: + * onPlacedInUrlbar(buttonNode) + * * buttonNode: The action's node in the urlbar. + * @param onShowingInPanel (function, optional) + * Called when a browser window's page action panel is showing: + * onShowingInPanel(buttonNode) + * * buttonNode: The action's node in the page action panel. + * @param shownInUrlbar (bool, optional) + * Pass true to show the action in the urlbar, false otherwise. False by + * default. + * @param subview (object, optional) + * An options object suitable for passing to the Subview constructor, if + * you'd like the action to have a subview. See the subview constructor + * for info on this object's properties. + * @param tooltip (string, optional) + * The action's button tooltip text. + * @param urlbarIDOverride (string, optional) + * Usually the ID of the action's button in the urlbar will be generated + * automatically. Pass a string for this property to override that with + * your own ID. + * @param wantsIframe (bool, optional) + * Pass true to make an action that shows an iframe in a panel when + * clicked. */ function Action(options) { setProperties(this, options, { id: true, title: !options._isSeparator, anchorIDOverride: false, + disabled: false, iconURL: false, labelForHistogram: false, nodeAttributes: false, onBeforePlacedInWindow: false, onCommand: false, onIframeHiding: false, onIframeHidden: false, onIframeShown: false, @@ -604,28 +583,16 @@ function Action(options) { }); if (this._subview) { this._subview = new Subview(options.subview); } } Action.prototype = { /** - * The action's icon URL (string, nullable) - */ - get iconURL() { - return this._iconURL; - }, - set iconURL(url) { - this._iconURL = url; - PageActions.onActionSetIconURL(this); - return this._iconURL; - }, - - /** * The action's ID (string, nonnull) */ get id() { return this._id; }, /** * Attribute name => value mapping to set on nodes created for this action @@ -645,36 +612,123 @@ Action.prototype = { if (this.shownInUrlbar != shown) { this._shownInUrlbar = shown; PageActions.onActionToggledShownInUrlbar(this); } return this.shownInUrlbar; }, /** + * The action's disabled state (bool, nonnull) + */ + getDisabled(browserWindow = null) { + return !!this._getProperty("disabled", browserWindow); + }, + setDisabled(value, browserWindow = null) { + return this._setProperty("disabled", !!value, browserWindow); + }, + + /** + * The action's icon URL string, or an object mapping sizes to URL strings + * (string or object, nullable) + */ + getIconURL(browserWindow = null) { + return this._getProperty("iconURL", browserWindow); + }, + setIconURL(value, browserWindow = null) { + return this._setProperty("iconURL", value, browserWindow); + }, + + /** * The action's title (string, nonnull) */ - get title() { - return this._title; + getTitle(browserWindow = null) { + return this._getProperty("title", browserWindow); }, - set title(title) { - this._title = title || ""; - PageActions.onActionSetTitle(this); - return this._title; + setTitle(value, browserWindow = null) { + return this._setProperty("title", value, browserWindow); }, /** * The action's tooltip (string, nullable) */ - get tooltip() { - return this._tooltip; + getTooltip(browserWindow = null) { + return this._getProperty("tooltip", browserWindow); + }, + setTooltip(value, browserWindow = null) { + return this._setProperty("tooltip", value, browserWindow); }, /** - * Override for the ID of the action's temporary panel anchor (string, nullable) + * Sets a property, optionally for a particular browser window. + * + * @param name (string, required) + * The (non-underscored) name of the property. + * @param value + * The value. + * @param browserWindow (DOM window, optional) + * If given, then the property will be set in this window's state, not + * globally. + */ + _setProperty(name, value, browserWindow) { + if (!browserWindow) { + // Set the global state. + this[`_${name}`] = value; + } else { + // Set the per-window state. + let props = this._propertiesByBrowserWindow.get(browserWindow); + if (!props) { + props = {}; + this._propertiesByBrowserWindow.set(browserWindow, props); + } + props[name] = value; + } + // This may be called before the action has been added. + if (PageActions.actionForID(this.id)) { + for (let bpa of allBrowserPageActions(browserWindow)) { + bpa.updateAction(this, name); + } + } + return value; + }, + + /** + * Gets a property, optionally for a particular browser window. + * + * @param name (string, required) + * The (non-underscored) name of the property. + * @param browserWindow (DOM window, optional) + * If given, then the property will be fetched from this window's + * state. If the property does not exist in the window's state, or if + * no window is given, then the global value is returned. + * @return The property value. + */ + _getProperty(name, browserWindow) { + if (browserWindow) { + // Try the per-window state. + let props = this._propertiesByBrowserWindow.get(browserWindow); + if (props && name in props) { + return props[name]; + } + } + // Fall back to the global state. + return this[`_${name}`]; + }, + + // maps browser windows => object with properties for that window + get _propertiesByBrowserWindow() { + if (!this.__propertiesByBrowserWindow) { + this.__propertiesByBrowserWindow = new WeakMap(); + } + return this.__propertiesByBrowserWindow; + }, + + /** + * Override for the ID of the action's activated-action panel anchor (string, + * nullable) */ get anchorIDOverride() { return this._anchorIDOverride; }, /** * Override for the ID of the action's urlbar node (string, nullable) */ @@ -696,16 +750,53 @@ Action.prototype = { return this._subview; }, get labelForHistogram() { return this._labelForHistogram || this._id; }, /** + * Returns the URL of the best icon to use given a preferred size. The best + * icon is the one with the smallest size that's equal to or bigger than the + * preferred size. Returns null if the action has no icon URL. + * + * @param peferredSize (number, required) + * The icon size you prefer. + * @return The URL of the best icon, or null. + */ + iconURLForSize(preferredSize, browserWindow) { + let iconURL = this.getIconURL(browserWindow); + if (!iconURL) { + return null; + } + if (typeof(iconURL) == "string") { + return iconURL; + } + if (typeof(iconURL) == "object") { + // This case is copied from ExtensionParent.jsm so that our image logic is + // the same, so that WebExtensions page action tests that deal with icons + // pass. + let bestSize = null; + if (iconURL[preferredSize]) { + bestSize = preferredSize; + } else if (iconURL[2 * preferredSize]) { + bestSize = 2 * preferredSize; + } else { + let sizes = Object.keys(iconURL) + .map(key => parseInt(key, 10)) + .sort((a, b) => a - b); + bestSize = sizes.find(candidate => candidate > preferredSize) || sizes.pop(); + } + return iconURL[bestSize]; + } + return null; + }, + + /** * Performs the command for an action. If the action has an onCommand * handler, then it's called. If the action has a subview or iframe, then a * panel is opened, displaying the subview or iframe. * * @param browserWindow (DOM window, required) * The browser window in which to perform the action. */ doCommand(browserWindow) { @@ -841,35 +932,32 @@ Action.prototype = { } }; this.PageActions.Action = Action; /** * A Subview represents a PanelUI panelview that your actions can show. + * `options` is a required object with the following properties. * - * @param options (object, required) - * An object with the following properties: - * @param buttons (array, optional) - * An array of buttons to show in the subview. Each item in the - * array must be an options object suitable for passing to the - * Button constructor. See the Button constructor for - * information on these objects' properties. - * @param onPlaced (function, optional) - * Called when the subview is added to its parent panel in a - * browser window: - * onPlaced(panelViewNode) - * * panelViewNode: The panelview node represented by this - * Subview. - * @param onShowing (function, optional) - * Called when the subview is showing in a browser window: - * onShowing(panelViewNode) - * * panelViewNode: The panelview node represented by this - * Subview. + * @param buttons (array, optional) + * An array of buttons to show in the subview. Each item in the array + * must be an options object suitable for passing to the Button + * constructor. See the Button constructor for information on these + * objects' properties. + * @param onPlaced (function, optional) + * Called when the subview is added to its parent panel in a browser + * window: + * onPlaced(panelViewNode) + * * panelViewNode: The panelview node represented by this Subview. + * @param onShowing (function, optional) + * Called when the subview is showing in a browser window: + * onShowing(panelViewNode) + * * panelViewNode: The panelview node represented by this Subview. */ function Subview(options) { setProperties(this, options, { buttons: false, onPlaced: false, onShowing: false, }); this._buttons = (this._buttons || []).map(buttonOptions => { @@ -909,36 +997,34 @@ Subview.prototype = { } } }; this.PageActions.Subview = Subview; /** - * A button that can be shown in a subview. + * A button that can be shown in a subview. `options` is a required object with + * the following properties. * - * @param options (object, required) - * An object with the following properties: - * @param id (string, required) - * The button's ID. This will not become the ID of a DOM node by - * itself, but it will be used to generate DOM node IDs. But in - * terms of spaces and weird characters and such, do treat this - * like a DOM node ID. - * @param title (string, required) - * The button's title. - * @param disabled (bool, required) - * Pass true to disable the button. - * @param onCommand (function, optional) - * Called when the button is clicked: - * onCommand(event, buttonNode) - * * event: The triggering event. - * * buttonNode: The node that was clicked. - * @param shortcut (string, optional) - * The button's shortcut text. + * @param id (string, required) + * The button's ID. This will not become the ID of a DOM node by itself, + * but it will be used to generate DOM node IDs. But in terms of spaces + * and weird characters and such, do treat this like a DOM node ID. + * @param title (string, required) + * The button's title. + * @param disabled (bool, required) + * Pass true to disable the button. + * @param onCommand (function, optional) + * Called when the button is clicked: + * onCommand(event, buttonNode) + * * event: The triggering event. + * * buttonNode: The node that was clicked. + * @param shortcut (string, optional) + * The button's shortcut text. */ function Button(options) { setProperties(this, options, { id: true, title: true, disabled: false, onCommand: false, shortcut: false, @@ -1009,16 +1095,18 @@ this.PageActions.ACTION_ID_BUILT_IN_SEPA // new actions. var gBuiltInActions = [ // bookmark { id: ACTION_ID_BOOKMARK, urlbarIDOverride: "star-button-box", _urlbarNodeInMarkup: true, + // The title is set in browser-pageActions.js by calling + // BookmarkingUI.updateBookmarkPageMenuItem(). title: "", shownInUrlbar: true, nodeAttributes: { observes: "bookmarkThisPageBroadcaster", }, onShowingInPanel(buttonNode) { browserPageActions(buttonNode).bookmark.onShowingInPanel(buttonNode); }, @@ -1100,26 +1188,43 @@ function browserPageActions(obj) { if (obj.BrowserPageActions) { return obj.BrowserPageActions; } return obj.ownerGlobal.BrowserPageActions; } /** * A generator function for all open browser windows. + * + * @param browserWindow (DOM window, optional) + * If given, then only this window will be yielded. That may sound + * pointless, but it can make callers nicer to write since they don't + * need two separate cases, one where a window is given and another where + * it isn't. */ -function* allBrowserWindows() { +function* allBrowserWindows(browserWindow = null) { + if (browserWindow) { + yield browserWindow; + return; + } let windows = Services.wm.getEnumerator("navigator:browser"); while (windows.hasMoreElements()) { yield windows.getNext(); } } -function* allBrowserPageActions() { - for (let win of allBrowserWindows()) { +/** + * A generator function for BrowserPageActions objects in all open windows. + * + * @param browserWindow (DOM window, optional) + * If given, then the BrowserPageActions for only this window will be + * yielded. + */ +function* allBrowserPageActions(browserWindow = null) { + for (let win of allBrowserWindows(browserWindow)) { yield browserPageActions(win); } } /** * A simple function that sets properties on a given object while doing basic * required-properties checking. If a required property isn't specified in the * given options object, or if the options object has properties that aren't in
--- a/browser/modules/test/browser/browser_PageActions.js +++ b/browser/modules/test/browser/browser_PageActions.js @@ -35,18 +35,18 @@ add_task(async function simple() { let tooltip = "Test simple tooltip"; let onCommandCallCount = 0; let onPlacedInPanelCallCount = 0; let onPlacedInUrlbarCallCount = 0; let onShowingInPanelCallCount = 0; let onCommandExpectedButtonID; - let panelButtonID = BrowserPageActions._panelButtonNodeIDForActionID(id); - let urlbarButtonID = BrowserPageActions._urlbarButtonNodeIDForActionID(id); + let panelButtonID = BrowserPageActions.panelButtonNodeIDForActionID(id); + let urlbarButtonID = BrowserPageActions.urlbarButtonNodeIDForActionID(id); let initialActions = PageActions.actions; let action = PageActions.addAction(new PageActions.Action({ iconURL, id, nodeAttributes, title, @@ -69,23 +69,23 @@ add_task(async function simple() { }, onShowingInPanel(buttonNode) { onShowingInPanelCallCount++; Assert.ok(buttonNode, "buttonNode should be non-null: " + buttonNode); Assert.equal(buttonNode.id, panelButtonID, "buttonNode.id"); }, })); - Assert.equal(action.iconURL, iconURL, "iconURL"); + Assert.equal(action.getIconURL(), iconURL, "iconURL"); Assert.equal(action.id, id, "id"); Assert.deepEqual(action.nodeAttributes, nodeAttributes, "nodeAttributes"); Assert.equal(action.shownInUrlbar, false, "shownInUrlbar"); Assert.equal(action.subview, null, "subview"); - Assert.equal(action.title, title, "title"); - Assert.equal(action.tooltip, tooltip, "tooltip"); + Assert.equal(action.getTitle(), title, "title"); + Assert.equal(action.getTooltip(), tooltip, "tooltip"); Assert.equal(action.urlbarIDOverride, null, "urlbarIDOverride"); Assert.equal(action.wantsIframe, false, "wantsIframe"); Assert.ok(!("__insertBeforeActionID" in action), "__insertBeforeActionID"); Assert.ok(!("__isSeparator" in action), "__isSeparator"); Assert.ok(!("__urlbarNodeInMarkup" in action), "__urlbarNodeInMarkup"); Assert.equal(onPlacedInPanelCallCount, 1, @@ -110,32 +110,33 @@ add_task(async function simple() { "actionForID should be action"); Assert.ok(PageActions._persistedActions.ids.includes(action.id), "PageActions should record action in its list of seen actions"); // The action's panel button should have been created. let panelButtonNode = document.getElementById(panelButtonID); Assert.notEqual(panelButtonNode, null, "panelButtonNode"); - Assert.equal(panelButtonNode.getAttribute("label"), action.title, "label"); + Assert.equal(panelButtonNode.getAttribute("label"), action.getTitle(), + "label"); for (let name in action.nodeAttributes) { Assert.ok(panelButtonNode.hasAttribute(name), "Has attribute: " + name); Assert.equal(panelButtonNode.getAttribute(name), action.nodeAttributes[name], "Equal attribute: " + name); } // The panel button should be the last node in the panel, and its previous // sibling should be the separator between the built-in actions and non-built- // in actions. Assert.equal(panelButtonNode.nextSibling, null, "nextSibling"); Assert.notEqual(panelButtonNode.previousSibling, null, "previousSibling"); Assert.equal( panelButtonNode.previousSibling.id, - BrowserPageActions._panelButtonNodeIDForActionID( + BrowserPageActions.panelButtonNodeIDForActionID( PageActions.ACTION_ID_BUILT_IN_SEPARATOR ), "previousSibling.id" ); // The action's urlbar button should not have been created. let urlbarButtonNode = document.getElementById(urlbarButtonID); Assert.equal(urlbarButtonNode, null, "urlbarButtonNode"); @@ -173,19 +174,20 @@ add_task(async function simple() { // Click the urlbar button. onCommandExpectedButtonID = urlbarButtonID; EventUtils.synthesizeMouseAtCenter(urlbarButtonNode, {}); Assert.equal(onCommandCallCount, 2, "onCommandCallCount should be inc'ed"); // Set a new title. let newTitle = title + " new title"; - action.title = newTitle; - Assert.equal(action.title, newTitle, "New title"); - Assert.equal(panelButtonNode.getAttribute("label"), action.title, "New label"); + action.setTitle(newTitle); + Assert.equal(action.getTitle(), newTitle, "New title"); + Assert.equal(panelButtonNode.getAttribute("label"), action.getTitle(), + "New label"); // Now that shownInUrlbar has been toggled, make sure that it sticks across // app restarts. Simulate that by "unregistering" the action (not by removing // it, which is more permanent) and then registering it again. // unregister PageActions._actionsByID.delete(action.id); let index = PageActions._nonBuiltInActions.findIndex(a => a.id == action.id); @@ -218,17 +220,17 @@ add_task(async function simple() { "actionForID should be null"); Assert.ok(!PageActions._persistedActions.ids.includes(action.id), "PageActions should remove action from its list of seen actions"); // The separator between the built-in actions and non-built-in actions should // be gone now, too. let separatorNode = document.getElementById( - BrowserPageActions._panelButtonNodeIDForActionID( + BrowserPageActions.panelButtonNodeIDForActionID( PageActions.ACTION_ID_BUILT_IN_SEPARATOR ) ); Assert.equal(separatorNode, null, "No separator"); Assert.ok(!BrowserPageActions.mainViewBodyNode .lastChild.localName.includes("separator"), "Last child should not be separator"); }); @@ -240,18 +242,18 @@ add_task(async function withSubview() { let onActionCommandCallCount = 0; let onActionPlacedInPanelCallCount = 0; let onActionPlacedInUrlbarCallCount = 0; let onSubviewPlacedCount = 0; let onSubviewShowingCount = 0; let onButtonCommandCallCount = 0; - let panelButtonID = BrowserPageActions._panelButtonNodeIDForActionID(id); - let urlbarButtonID = BrowserPageActions._urlbarButtonNodeIDForActionID(id); + let panelButtonID = BrowserPageActions.panelButtonNodeIDForActionID(id); + let urlbarButtonID = BrowserPageActions.urlbarButtonNodeIDForActionID(id); let panelViewIDPanel = BrowserPageActions._panelViewNodeIDForActionID(id, false); let panelViewIDUrlbar = BrowserPageActions._panelViewNodeIDForActionID(id, true); let onSubviewPlacedExpectedPanelViewID = panelViewIDPanel; let onSubviewShowingExpectedPanelViewID; @@ -428,18 +430,18 @@ add_task(async function withSubview() { add_task(async function withIframe() { let id = "test-iframe"; let onCommandCallCount = 0; let onPlacedInPanelCallCount = 0; let onPlacedInUrlbarCallCount = 0; let onIframeShownCount = 0; - let panelButtonID = BrowserPageActions._panelButtonNodeIDForActionID(id); - let urlbarButtonID = BrowserPageActions._urlbarButtonNodeIDForActionID(id); + let panelButtonID = BrowserPageActions.panelButtonNodeIDForActionID(id); + let urlbarButtonID = BrowserPageActions.urlbarButtonNodeIDForActionID(id); let action = PageActions.addAction(new PageActions.Action({ iconURL: "chrome://browser/skin/mail.svg", id, shownInUrlbar: true, title: "Test iframe", wantsIframe: true, onCommand(event, buttonNode) { @@ -552,17 +554,17 @@ add_task(async function withIframe() { urlbarButtonNode = document.getElementById(urlbarButtonID); Assert.equal(urlbarButtonNode, null, "urlbarButtonNode"); }); // Tests an action with the _insertBeforeActionID option set. add_task(async function insertBeforeActionID() { let id = "test-insertBeforeActionID"; - let panelButtonID = BrowserPageActions._panelButtonNodeIDForActionID(id); + let panelButtonID = BrowserPageActions.panelButtonNodeIDForActionID(id); let initialActions = PageActions.actions; let initialBuiltInActions = PageActions.builtInActions; let initialNonBuiltInActions = PageActions.nonBuiltInActions; let initialBookmarkSeparatorIndex = PageActions.actions.findIndex(a => { return a.id == PageActions.ACTION_ID_BOOKMARK_SEPARATOR; }); @@ -601,27 +603,27 @@ add_task(async function insertBeforeActi let panelButtonNode = document.getElementById(panelButtonID); Assert.notEqual(panelButtonNode, null, "panelButtonNode"); // The button's next sibling should be the bookmark separator. Assert.notEqual(panelButtonNode.nextSibling, null, "panelButtonNode.nextSibling"); Assert.equal( panelButtonNode.nextSibling.id, - BrowserPageActions._panelButtonNodeIDForActionID( + BrowserPageActions.panelButtonNodeIDForActionID( PageActions.ACTION_ID_BOOKMARK_SEPARATOR ), "panelButtonNode.nextSibling.id" ); // The separator between the built-in and non-built-in actions should not have // been created. Assert.equal( document.getElementById( - BrowserPageActions._panelButtonNodeIDForActionID( + BrowserPageActions.panelButtonNodeIDForActionID( PageActions.ACTION_ID_BUILT_IN_SEPARATOR ) ), null, "Separator should be gone" ); action.remove(); @@ -664,48 +666,48 @@ add_task(async function multipleNonBuilt let actualAction = PageActions.nonBuiltInActions[i]; Assert.equal(actualAction.id, idPrefix + expectedIndex, "actualAction.id for index: " + i); } // Check the button nodes in the panel. let expectedIndex = 1; let buttonNode = document.getElementById( - BrowserPageActions._panelButtonNodeIDForActionID(idPrefix + expectedIndex) + BrowserPageActions.panelButtonNodeIDForActionID(idPrefix + expectedIndex) ); Assert.notEqual(buttonNode, null, "buttonNode"); Assert.notEqual(buttonNode.previousSibling, null, "buttonNode.previousSibling"); Assert.equal( buttonNode.previousSibling.id, - BrowserPageActions._panelButtonNodeIDForActionID( + BrowserPageActions.panelButtonNodeIDForActionID( PageActions.ACTION_ID_BUILT_IN_SEPARATOR ), "buttonNode.previousSibling.id" ); for (let i = 0; i < actions.length; i++) { Assert.notEqual(buttonNode, null, "buttonNode at index: " + i); Assert.equal( buttonNode.id, - BrowserPageActions._panelButtonNodeIDForActionID(idPrefix + expectedIndex), + BrowserPageActions.panelButtonNodeIDForActionID(idPrefix + expectedIndex), "buttonNode.id at index: " + i ); buttonNode = buttonNode.nextSibling; expectedIndex++; } Assert.equal(buttonNode, null, "Nothing should come after the last button"); for (let action of actions) { action.remove(); } // The separator between the built-in and non-built-in actions should be gone. Assert.equal( document.getElementById( - BrowserPageActions._panelButtonNodeIDForActionID( + BrowserPageActions.panelButtonNodeIDForActionID( PageActions.ACTION_ID_BUILT_IN_SEPARATOR ) ), null, "Separator should be gone" ); }); @@ -745,17 +747,17 @@ add_task(async function nonBuiltFirst() Assert.deepEqual(PageActions.builtInActions.map(a => a.id), [], "PageActions.builtInActions should be empty"); Assert.deepEqual(PageActions.nonBuiltInActions.map(a => a.id), [action.id], "Action should be in PageActions.nonBuiltInActions"); // Check the panel. Assert.deepEqual( Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id), - [BrowserPageActions._panelButtonNodeIDForActionID(action.id)], + [BrowserPageActions.panelButtonNodeIDForActionID(action.id)], "Action should be in panel" ); // Now add back all the actions. for (let a of initialActions) { PageActions.addAction(a); } @@ -780,17 +782,17 @@ add_task(async function nonBuiltFirst() ); // Check the panel. Assert.deepEqual( Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id), initialActions.map(a => a.id).concat( [PageActions.ACTION_ID_BUILT_IN_SEPARATOR], [action.id] - ).map(id => BrowserPageActions._panelButtonNodeIDForActionID(id)), + ).map(id => BrowserPageActions.panelButtonNodeIDForActionID(id)), "Panel should contain all actions" ); // Remove the test action. action.remove(); // Check the actions. Assert.deepEqual( @@ -807,17 +809,17 @@ add_task(async function nonBuiltFirst() PageActions.nonBuiltInActions.map(a => a.id), [], "Action should no longer be in PageActions.nonBuiltInActions" ); // Check the panel. Assert.deepEqual( Array.map(BrowserPageActions.mainViewBodyNode.childNodes, n => n.id), - initialActions.map(a => BrowserPageActions._panelButtonNodeIDForActionID(a.id)), + initialActions.map(a => BrowserPageActions.panelButtonNodeIDForActionID(a.id)), "Action should no longer be in panel" ); }); // Makes sure that urlbar nodes appear in the correct order in a new window. add_task(async function urlbarOrderNewWindow() { // Make some new actions. @@ -879,17 +881,17 @@ add_task(async function urlbarOrderNewWi node; node = node.nextSibling) { actualUrlbarNodeIDs.push(node.id); } // Now check that they're in the right order. Assert.deepEqual( actualUrlbarNodeIDs, - ids.map(id => win.BrowserPageActions._urlbarButtonNodeIDForActionID(id)), + ids.map(id => win.BrowserPageActions.urlbarButtonNodeIDForActionID(id)), "Expected actions in new window's urlbar" ); // Done, clean up. await BrowserTestUtils.closeWindow(win); for (let action of actions) { action.remove(); } @@ -959,26 +961,90 @@ add_task(async function migrate1() { node; node = node.nextSibling) { actualUrlbarNodeIDs.push(node.id); } // Now check that they're in the right order. Assert.deepEqual( actualUrlbarNodeIDs, - orderedIDs.map(id => win.BrowserPageActions._urlbarButtonNodeIDForActionID(id)), + orderedIDs.map(id => win.BrowserPageActions.urlbarButtonNodeIDForActionID(id)), "Expected actions in new window's urlbar" ); // Done, clean up. await BrowserTestUtils.closeWindow(win); Services.prefs.clearUserPref(PageActions.PREF_PERSISTED_ACTIONS); PageActions.actionForID("copyURL")._shownInUrlbar = false; }); + +// Opens a new browser window and makes sure per-window state works right. +add_task(async function perWindowState() { + // Add a test action. + let title = "Test perWindowState"; + let action = PageActions.addAction(new PageActions.Action({ + iconURL: "chrome://browser/skin/mail.svg", + id: "test-perWindowState", + shownInUrlbar: true, + title, + })); + + // Open a new browser window and load an actionable page so that the action + // shows up in it. + let newWindow = await BrowserTestUtils.openNewBrowserWindow(); + await BrowserTestUtils.openNewForegroundTab({ + gBrowser: newWindow.gBrowser, + url: "http://example.com/", + }); + + // Set a new title globally. + let newGlobalTitle = title + " new title"; + action.setTitle(newGlobalTitle); + Assert.equal(action.getTitle(), newGlobalTitle, + "Title: global"); + Assert.equal(action.getTitle(window), newGlobalTitle, + "Title: old window"); + Assert.equal(action.getTitle(newWindow), newGlobalTitle, + "Title: new window"); + + // The action's panel button nodes should be updated in both windows. + let panelButtonID = + BrowserPageActions.panelButtonNodeIDForActionID(action.id); + for (let win of [window, newWindow]) { + let panelButtonNode = win.document.getElementById(panelButtonID); + Assert.equal(panelButtonNode.getAttribute("label"), newGlobalTitle, + "Panel button label should be global title"); + } + + // Set a new title in the new window. + let newPerWinTitle = title + " new title in new window"; + action.setTitle(newPerWinTitle, newWindow); + Assert.equal(action.getTitle(), newGlobalTitle, + "Title: global should remain same"); + Assert.equal(action.getTitle(window), newGlobalTitle, + "Title: old window should remain same"); + Assert.equal(action.getTitle(newWindow), newPerWinTitle, + "Title: new window should be new"); + + // The action's panel button node should be updated in the new window but the + // same in the old window. + let panelButtonNode1 = document.getElementById(panelButtonID); + Assert.equal(panelButtonNode1.getAttribute("label"), newGlobalTitle, + "Panel button label in old window"); + let panelButtonNode2 = newWindow.document.getElementById(panelButtonID); + Assert.equal(panelButtonNode2.getAttribute("label"), newPerWinTitle, + "Panel button label in new window"); + + // Done, clean up. + await BrowserTestUtils.closeWindow(newWindow); + action.remove(); +}); + + function promisePageActionPanelOpen() { let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); return BrowserTestUtils.waitForCondition(() => { // Wait for the main page action button to become visible. It's hidden for // some URIs, so depending on when this is called, it may not yet be quite // visible. It's up to the caller to make sure it will be visible. info("Waiting for main page action button to have non-0 size");
--- a/browser/themes/shared/urlbar-searchbar.inc.css +++ b/browser/themes/shared/urlbar-searchbar.inc.css @@ -191,16 +191,20 @@ height: 28px; padding: var(--urlbar-icon-padding); -moz-context-properties: fill, fill-opacity; fill: currentColor; fill-opacity: 0.6; color: inherit; } +.urlbar-page-action[disabled] { + fill-opacity: 0.3; +} + :root[uidensity=compact] .urlbar-icon { width: 24px; height: 24px; } :root[uidensity=touch] .urlbar-icon { width: 30px; height: 30px;