Bug 1395387 - Reconcile WebExtension page actions and Photon page actions: WebExtensions changes. r?mixedpuppy
MozReview-Commit-ID: n2eR3q1aZF
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -381,20 +381,16 @@ toolbarpaletteitem > toolbaritem[sdkstyl
list-style-image: var(--webextension-menupanel-image-light, inherit);
}
.webextension-browser-action[cui-areatype="menu-panel"]:-moz-lwtheme-darktext,
toolbarpaletteitem[place="palette"] > .webextension-browser-action:-moz-lwtheme-darktext {
list-style-image: var(--webextension-menupanel-image-dark, inherit);
}
- .webextension-page-action {
- list-style-image: var(--webextension-urlbar-image, inherit);
- }
-
.webextension-menuitem {
list-style-image: var(--webextension-menuitem-image, inherit);
}
}
@media (min-resolution: 1.1dppx) {
.webextension-browser-action {
list-style-image: var(--webextension-toolbar-image-2x, inherit);
@@ -417,20 +413,16 @@ toolbarpaletteitem > toolbaritem[sdkstyl
list-style-image: var(--webextension-menupanel-image-2x-light, inherit);
}
.webextension-browser-action[cui-areatype="menu-panel"]:-moz-lwtheme-darktext,
toolbarpaletteitem[place="palette"] > .webextension-browser-action:-moz-lwtheme-darktext {
list-style-image: var(--webextension-menupanel-image-2x-dark, inherit);
}
- .webextension-page-action {
- list-style-image: var(--webextension-urlbar-image-2x, inherit);
- }
-
.webextension-menuitem {
list-style-image: var(--webextension-menuitem-image-2x, inherit);
}
}
toolbarbutton.webextension-menuitem > .toolbarbutton-icon {
width: 16px;
height: 16px;
--- a/browser/components/extensions/ExtensionPopups.jsm
+++ b/browser/components/extensions/ExtensionPopups.jsm
@@ -369,42 +369,38 @@ class BasePopup {
/**
* A map of active popups for a given browser window.
*
* WeakMap[window -> WeakMap[Extension -> BasePopup]]
*/
BasePopup.instances = new DefaultWeakMap(() => new WeakMap());
class PanelPopup extends BasePopup {
- constructor(extension, imageNode, popupURL, browserStyle) {
- let document = imageNode.ownerDocument;
-
+ constructor(extension, document, popupURL, browserStyle) {
let panel = document.createElement("panel");
panel.setAttribute("id", makeWidgetId(extension.id) + "-panel");
panel.setAttribute("class", "browser-extension-panel");
panel.setAttribute("tabspecific", "true");
panel.setAttribute("type", "arrow");
panel.setAttribute("role", "group");
if (extension.remote) {
panel.setAttribute("remote", "true");
}
document.getElementById("mainPopupSet").appendChild(panel);
- super(extension, panel, popupURL, browserStyle);
-
- this.contentReady.then(() => {
- panel.openPopup(imageNode, "bottomcenter topright", 0, 0, false, false);
-
+ panel.addEventListener("popupshowing", () => {
let event = new this.window.CustomEvent("WebExtPopupLoaded", {
bubbles: true,
detail: {extension},
});
this.browser.dispatchEvent(event);
- });
+ }, {once: true});
+
+ super(extension, panel, popupURL, browserStyle);
}
get DESTROY_EVENT() {
return "popuphidden";
}
destroy() {
super.destroy();
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -1,26 +1,24 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
// The ext-* files are imported into the same scopes.
/* import-globals-from ext-browserAction.js */
/* import-globals-from ext-browser.js */
+XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
+ "resource:///modules/PageActions.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PanelPopup",
"resource:///modules/ExtensionPopups.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
"resource://gre/modules/TelemetryStopwatch.jsm");
-var {
- DefaultWeakMap,
-} = ExtensionUtils;
-
Cu.import("resource://gre/modules/ExtensionParent.jsm");
var {
IconDetails,
StartupCache,
} = ExtensionParent;
const popupOpenTimingHistogram = "WEBEXT_PAGEACTION_POPUP_OPEN_MS";
@@ -32,19 +30,18 @@ this.pageAction = class extends Extensio
static for(extension) {
return pageActionMap.get(extension);
}
async onManifestEntry(entryName) {
let {extension} = this;
let options = extension.manifest.page_action;
- this.iconData = new DefaultWeakMap(icons => this.getIconData(icons));
-
- this.id = makeWidgetId(extension.id) + "-page-action";
+ let widgetId = makeWidgetId(extension.id);
+ this.id = widgetId + "-page-action";
this.tabManager = extension.tabManager;
this.defaults = {
show: false,
title: options.default_title || extension.name,
popup: options.default_popup || "",
};
@@ -55,42 +52,44 @@ this.pageAction = class extends Extensio
"or not in your page_action options.");
}
this.tabContext = new TabContext(tab => Object.create(this.defaults),
extension);
this.tabContext.on("location-change", this.handleLocationChange.bind(this)); // eslint-disable-line mozilla/balanced-listeners
- // WeakMap[ChromeWindow -> <xul:image>]
- this.buttons = new WeakMap();
-
pageActionMap.set(extension, this);
this.defaults.icon = await StartupCache.get(
extension, ["pageAction", "default_icon"],
() => IconDetails.normalize({path: options.default_icon}, extension));
- this.iconData.set(
- this.defaults.icon,
- await StartupCache.get(
- extension, ["pageAction", "default_icon_data"],
- () => this.getIconData(this.defaults.icon)));
+ if (!this.browserPageAction) {
+ this.browserPageAction = PageActions.addAction(new PageActions.Action({
+ id: widgetId,
+ title: this.defaults.title,
+ iconURL: this.defaults.icon,
+ shownInUrlbar: true,
+ disabled: true,
+ onCommand: (event, buttonNode) => {
+ this.handleClick(event.target.ownerGlobal);
+ },
+ }));
+ }
}
onShutdown(reason) {
pageActionMap.delete(this.extension);
this.tabContext.shutdown();
- for (let window of windowTracker.browserWindows()) {
- if (this.buttons.has(window)) {
- this.buttons.get(window).remove();
- window.document.removeEventListener("popupshowing", this);
- }
+ if (this.browserPageAction) {
+ this.browserPageAction.remove();
+ this.browserPageAction = null;
}
}
// Returns the value of the property |prop| for the given tab, where
// |prop| is one of "show", "title", "icon", "popup".
getProperty(tab, prop) {
return this.tabContext.get(tab)[prop];
}
@@ -112,91 +111,34 @@ this.pageAction = class extends Extensio
}
}
// Updates the page action button in the given window to reflect the
// properties of the currently selected tab:
//
// Updates "tooltiptext" and "aria-label" to match "title" property.
// Updates "image" to match the "icon" property.
- // Shows or hides the icon, based on the "show" property.
+ // Enables or disables the icon, based on the "show" property.
updateButton(window) {
let tabData = this.tabContext.get(window.gBrowser.selectedTab);
-
- if (!(tabData.show || this.buttons.has(window))) {
- // Don't bother creating a button for a window until it actually
- // needs to be shown.
- return;
- }
-
- window.requestAnimationFrame(() => {
- let button = this.getButton(window);
-
- if (tabData.show) {
- // Update the title and icon only if the button is visible.
-
- let title = tabData.title || this.extension.name;
- button.setAttribute("tooltiptext", title);
- button.setAttribute("aria-label", title);
- button.classList.add("webextension-page-action");
-
- let {style} = this.iconData.get(tabData.icon);
-
- button.setAttribute("style", style);
- }
-
- button.hidden = !tabData.show;
- });
- }
-
- getIconData(icons) {
- let getIcon = size => {
- let {icon} = IconDetails.getPreferredIcon(icons, this.extension, size);
- // TODO: implement theme based icon for pageAction (Bug 1398156)
- return IconDetails.escapeUrl(icon);
- };
+ let title = tabData.title || this.extension.name;
+ this.browserPageAction.setTitle(title, window);
+ this.browserPageAction.setTooltip(title, window);
+ this.browserPageAction.setDisabled(!tabData.show, window);
- let style = `
- --webextension-urlbar-image: url("${getIcon(16)}");
- --webextension-urlbar-image-2x: url("${getIcon(32)}");
- `;
-
- return {style};
- }
-
- // Create an |image| node and add it to the |page-action-buttons|
- // container in the given window.
- addButton(window) {
- let document = window.document;
-
- let button = document.createElement("image");
- button.id = this.id;
- button.setAttribute("class", "urlbar-icon");
-
- button.addEventListener("click", this); // eslint-disable-line mozilla/balanced-listeners
-
- if (this.extension.hasPermission("menus") ||
- this.extension.hasPermission("contextMenus")) {
- document.addEventListener("popupshowing", this);
+ let iconURL;
+ if (typeof(tabData.icon) == "string") {
+ iconURL = IconDetails.escapeUrl(tabData.icon);
+ } else {
+ iconURL = Object.entries(tabData.icon).reduce((memo, [size, url]) => {
+ memo[size] = IconDetails.escapeUrl(url);
+ return memo;
+ }, {});
}
-
- document.getElementById("page-action-buttons").appendChild(button);
-
- return button;
- }
-
- // Returns the page action button for the given window, creating it if
- // it doesn't already exist.
- getButton(window) {
- if (!this.buttons.has(window)) {
- let button = this.addButton(window);
- this.buttons.set(window, button);
- }
-
- return this.buttons.get(window);
+ this.browserPageAction.setIconURL(iconURL, window);
}
/**
* Triggers this page action for the given window, with the same effects as
* if it were clicked by a user.
*
* This has no effect if the page action is hidden for the selected tab.
*
@@ -204,41 +146,16 @@ this.pageAction = class extends Extensio
*/
triggerAction(window) {
let pageAction = pageActionMap.get(this.extension);
if (pageAction.getProperty(window.gBrowser.selectedTab, "show")) {
pageAction.handleClick(window);
}
}
- handleEvent(event) {
- const window = event.target.ownerGlobal;
-
- switch (event.type) {
- case "click":
- if (event.button === 0) {
- this.handleClick(window);
- }
- break;
-
- case "popupshowing":
- const menu = event.target;
- const trigger = menu.triggerNode;
-
- if (menu.id === "toolbar-context-menu" && trigger && trigger.id === this.id) {
- global.actionContextMenu({
- extension: this.extension,
- onPageAction: true,
- menu: menu,
- });
- }
- break;
- }
- }
-
// Handles a click event on the page action button for the given
// window.
// If the page action has a |popup| property, a panel is opened to
// that URL. Otherwise, a "click" event is emitted, and dispatched to
// the any click listeners in the add-on.
async handleClick(window) {
TelemetryStopwatch.start(popupOpenTimingHistogram, this);
let tab = window.gBrowser.selectedTab;
@@ -246,19 +163,21 @@ this.pageAction = class extends Extensio
this.tabManager.addActiveTabPermission(tab);
// If the widget has a popup URL defined, we open a popup, but do not
// dispatch a click event to the extension.
// If it has no popup URL defined, we dispatch a click event, but do not
// open a popup.
if (popupURL) {
- let popup = new PanelPopup(this.extension, this.getButton(window),
- popupURL, this.browserStyle);
+ let popup = new PanelPopup(this.extension, window.document, popupURL,
+ this.browserStyle);
await popup.contentReady;
+ window.BrowserPageActions.togglePanelForAction(this.browserPageAction,
+ popup.panel);
TelemetryStopwatch.finish(popupOpenTimingHistogram, this);
} else {
TelemetryStopwatch.cancel(popupOpenTimingHistogram, this);
this.emit("click", tab);
}
}
handleLocationChange(eventType, tab, fromBrowse) {
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
@@ -255,17 +255,17 @@ add_task(async function testDetailsObjec
"data/a-x2.png": imageBuffer,
},
});
const RESOLUTION_PREF = "layout.css.devPixelsPerPx";
await extension.startup();
- let pageActionId = `${makeWidgetId(extension.id)}-page-action`;
+ let pageActionId = BrowserPageActions.urlbarButtonNodeIDForActionID(makeWidgetId(extension.id));
let browserActionWidget = getBrowserActionWidget(extension);
let tests = await extension.awaitMessage("ready");
for (let test of tests) {
extension.sendMessage("setIcon", test);
await extension.awaitMessage("iconSet");
await promiseAnimationFrame();
@@ -355,17 +355,17 @@ add_task(async function testPageActionIc
});
await extension.startup();
await extension.awaitMessage("ready");
await promiseAnimationFrame();
- let pageActionId = `${makeWidgetId(extension.id)}-page-action`;
+ let pageActionId = BrowserPageActions.urlbarButtonNodeIDForActionID(makeWidgetId(extension.id));
let pageActionImage = document.getElementById(pageActionId);
const iconURL = new URL(getListStyleImage(pageActionImage));
is(iconURL.pathname, "/common_cached_icon.png", "Got the expected pageAction icon url");
await extension.unload();
});
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon_permissions.js
@@ -97,17 +97,17 @@ add_task(async function testDefaultDetai
"baz/quux.png": imageBuffer,
"baz/quux@2x.png": imageBuffer,
},
});
await Promise.all([extension.startup(), extension.awaitMessage("ready")]);
let browserActionId = makeWidgetId(extension.id) + "-browser-action";
- let pageActionId = makeWidgetId(extension.id) + "-page-action";
+ let pageActionId = BrowserPageActions.urlbarButtonNodeIDForActionID(makeWidgetId(extension.id));
await promiseAnimationFrame();
let browserActionButton = document.getElementById(browserActionId);
let image = getListStyleImage(browserActionButton);
ok(expectedURL.test(image), `browser action image ${image} matches ${expectedURL}`);
--- a/browser/components/extensions/test/browser/browser_ext_menus.js
+++ b/browser/components/extensions/test/browser/browser_ext_menus.js
@@ -63,17 +63,20 @@ add_task(async function test_actionConte
}
const extension = ExtensionTestUtils.loadExtension({manifest, background});
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
await extension.startup();
const tabId = await extension.awaitMessage("ready");
- for (const kind of ["page", "browser"]) {
+ // TODO bug 1412170: Allow WebExtensions to hook into the browser page action
+ // context menu.
+// for (const kind of ["page", "browser"]) {
+ for (const kind of ["browser"]) {
const menu = await openActionContextMenu(extension, kind);
const [submenu, second, , , , last, separator] = menu.children;
is(submenu.tagName, "menu", "Correct submenu type");
is(submenu.label, "parent", "Correct submenu title");
const popup = await openSubmenu(submenu);
is(popup, submenu.firstChild, "Correct submenu opened");
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -388,17 +388,20 @@ function closeChromeContextMenu(menuId,
}
return hidden;
}
async function openActionContextMenu(extension, kind, win = window) {
// See comment from clickPageAction below.
SetPageProxyState("valid");
await promiseAnimationFrame(win);
- const id = `#${makeWidgetId(extension.id)}-${kind}-action`;
+ const id =
+ kind == "page" ?
+ `#${BrowserPageActions.urlbarButtonNodeIDForActionID(makeWidgetId(extension.id))}` :
+ `#${makeWidgetId(extension.id)}-${kind}-action`;
return openChromeContextMenu("toolbar-context-menu", id, win);
}
function closeActionContextMenu(itemToSelect, win = window) {
return closeChromeContextMenu("toolbar-context-menu", itemToSelect, win);
}
function openTabContextMenu(win = window) {
@@ -420,17 +423,18 @@ async function clickPageAction(extension
// identity info and icons such as page action buttons.
//
// Unfortunately, that doesn't happen automatically in browser chrome
// tests.
SetPageProxyState("valid");
await promiseAnimationFrame(win);
- let pageActionId = makeWidgetId(extension.id) + "-page-action";
+ let pageActionId = BrowserPageActions.urlbarButtonNodeIDForActionID(makeWidgetId(extension.id));
+
let elem = win.document.getElementById(pageActionId);
EventUtils.synthesizeMouseAtCenter(elem, {}, win);
return new Promise(SimpleTest.executeSoon);
}
function closePageAction(extension, win = window) {
let node = getPageActionPopup(extension, win);
--- a/browser/components/extensions/test/browser/head_pageAction.js
+++ b/browser/components/extensions/test/browser/head_pageAction.js
@@ -100,17 +100,17 @@ async function runTests(options) {
let pageActionId;
let currentWindow = window;
let windows = [];
function checkDetails(details) {
let image = currentWindow.document.getElementById(pageActionId);
if (details == null) {
- ok(image == null || image.hidden, "image is hidden");
+ ok(image == null || image.getAttribute("disabled") == "true", "image is disabled");
} else {
ok(image, "image exists");
is(getListStyleImage(image), details.icon, "icon URL is correct");
let title = details.title || options.manifest.name;
is(image.getAttribute("tooltiptext"), title, "image title is correct");
is(image.getAttribute("aria-label"), title, "image aria-label is correct");
@@ -118,17 +118,17 @@ async function runTests(options) {
}
}
let testNewWindows = 1;
let awaitFinish = new Promise(resolve => {
extension.onMessage("nextTest", async (expecting, testsRemaining) => {
if (!pageActionId) {
- pageActionId = `${makeWidgetId(extension.id)}-page-action`;
+ pageActionId = BrowserPageActions.urlbarButtonNodeIDForActionID(makeWidgetId(extension.id));
}
await promiseAnimationFrame(currentWindow);
checkDetails(expecting);
if (testsRemaining) {
extension.sendMessage("runNextTest");