--- 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
@@ -1429,16 +1429,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;