Bug 1268020 - Implement "tools_menu" context
MozReview-Commit-ID: JRJSIYDR0JM
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -306,23 +306,29 @@ function getContexts(contextData) {
if (contextData.onPageAction) {
contexts.add("page_action");
}
if (contextData.onBrowserAction) {
contexts.add("browser_action");
}
+ if (contextData.onTab) {
+ contexts.add("tab");
+ }
+
+ if (contextData.inToolsMenu) {
+ contexts.add("tools_menu");
+ }
+
if (contexts.size === 0) {
contexts.add("page");
}
- if (contextData.onTab) {
- contexts.add("tab");
- } else {
+ if (!contextData.onTab && !contextData.inToolsMenu) {
contexts.add("all");
}
return contexts;
}
function MenuItem(extension, createProperties, isRoot = false) {
this.extension = extension;
@@ -537,70 +543,81 @@ MenuItem.prototype = {
}
}
return true;
},
};
// While any extensions are active, this Tracker registers to observe/listen
-// for contex-menu events from both content and chrome.
-const contextMenuTracker = {
+// for menu events from both Tools and context menus, both content and chrome.
+const menuTracker = {
+ menuIds: ["menu_ToolsPopup", "tabContextMenu"],
+
register() {
Services.obs.addObserver(this, "on-build-contextmenu", false);
for (const window of windowTracker.browserWindows()) {
this.onWindowOpen(window);
}
windowTracker.addOpenListener(this.onWindowOpen);
},
unregister() {
Services.obs.removeObserver(this, "on-build-contextmenu");
for (const window of windowTracker.browserWindows()) {
- const menu = window.document.getElementById("tabContextMenu");
- menu.removeEventListener("popupshowing", this);
+ for (const id of this.menuIds) {
+ const menu = window.document.getElementById(id);
+ menu.removeEventListener("popupshowing", this);
+ }
}
windowTracker.removeOpenListener(this.onWindowOpen);
},
observe(subject, topic, data) {
subject = subject.wrappedJSObject;
gMenuBuilder.build(subject);
},
onWindowOpen(window) {
- const menu = window.document.getElementById("tabContextMenu");
- menu.addEventListener("popupshowing", contextMenuTracker);
+ for (const id of menuTracker.menuIds) {
+ const menu = window.document.getElementById(id);
+ menu.addEventListener("popupshowing", menuTracker);
+ }
},
handleEvent(event) {
const menu = event.target;
+ if (menu.id === "menu_ToolsPopup") {
+ const tab = tabTracker.activeTab;
+ const pageUrl = tab.linkedBrowser.currentURI.spec;
+ gMenuBuilder.build({menu, tab, pageUrl, inToolsMenu: true});
+ }
if (menu.id === "tabContextMenu") {
const trigger = menu.triggerNode;
const tab = trigger.localName === "tab" ? trigger : tabTracker.activeTab;
const pageUrl = tab.linkedBrowser.currentURI.spec;
gMenuBuilder.build({menu, tab, pageUrl, onTab: true});
}
},
};
var gExtensionCount = 0;
/* eslint-disable mozilla/balanced-listeners */
extensions.on("startup", (type, extension) => {
gContextMenuMap.set(extension, new Map());
if (++gExtensionCount == 1) {
- contextMenuTracker.register();
+ menuTracker.register();
}
});
extensions.on("shutdown", (type, extension) => {
gContextMenuMap.delete(extension);
gRootItems.delete(extension);
if (--gExtensionCount == 0) {
- contextMenuTracker.unregister();
+ menuTracker.unregister();
}
});
/* eslint-enable mozilla/balanced-listeners */
extensions.registerSchemaAPI("contextMenus", "addon_parent", context => {
let {extension} = context;
return {
contextMenus: {
--- a/browser/components/extensions/schemas/context_menus.json
+++ b/browser/components/extensions/schemas/context_menus.json
@@ -26,18 +26,18 @@
"value": 6,
"description": "The maximum number of top level extension items that can be added to an extension action context menu. Any items beyond this limit will be ignored."
}
},
"types": [
{
"id": "ContextType",
"type": "string",
- "enum": ["all", "page", "frame", "selection", "link", "editable", "password", "image", "video", "audio", "launcher", "browser_action", "page_action", "tab"],
- "description": "The different contexts a menu can appear in. Specifying 'all' is equivalent to the combination of all other contexts except for 'tab'."
+ "enum": ["all", "page", "frame", "selection", "link", "editable", "password", "image", "video", "audio", "launcher", "browser_action", "page_action", "tab", "tools_menu"],
+ "description": "The different contexts a menu can appear in. Specifying 'all' is equivalent to the combination of all other contexts except for 'tab' and 'tools_menu'."
},
{
"id": "ItemType",
"type": "string",
"enum": ["normal", "checkbox", "radio", "separator"],
"description": "The type of menu item."
},
{
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus_chrome.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_chrome.js
@@ -125,8 +125,64 @@ add_task(function* test_tabContextMenu()
const click = yield first.awaitMessage("click");
is(click.info.pageUrl, "http://example.com/", "Click info pageUrl is correct");
is(click.tab.id, tabId, "Click event tab ID is correct");
yield BrowserTestUtils.removeTab(tab);
yield first.unload();
yield second.unload();
});
+
+add_task(function* test_tools_menu() {
+ const first = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus"],
+ },
+ async background() {
+ await browser.contextMenus.create({title: "alpha", contexts: ["tools_menu"]});
+ await browser.contextMenus.create({title: "beta", contexts: ["tools_menu"]});
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ const second = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["contextMenus"],
+ },
+ async background() {
+ await browser.contextMenus.create({title: "gamma", contexts: ["tools_menu"]});
+ browser.contextMenus.onClicked.addListener((info, tab) => {
+ browser.test.sendMessage("click", {info, tab});
+ });
+
+ const [tab] = await browser.tabs.query({active: true});
+ browser.test.sendMessage("ready", tab.id);
+ },
+ });
+
+ const tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+ yield first.startup();
+ yield second.startup();
+
+ yield first.awaitMessage("ready");
+ const tabId = yield second.awaitMessage("ready");
+ const menu = yield openToolsMenu();
+
+ const [separator, submenu, gamma] = Array.from(menu.children).slice(-3);
+ is(separator.tagName, "menuseparator", "Separator before first extension item");
+
+ is(submenu.tagName, "menu", "Correct submenu type");
+ is(submenu.getAttribute("label"), "Generated extension", "Correct submenu title");
+ is(submenu.firstChild.children.length, 2, "Correct number of submenu items");
+
+ is(gamma.tagName, "menuitem", "Third menu item type is correct");
+ is(gamma.getAttribute("label"), "gamma", "Third menu item label is correct");
+
+ closeToolsMenu(gamma);
+
+ const click = yield second.awaitMessage("click");
+ is(click.info.pageUrl, "http://example.com/", "Click info pageUrl is correct");
+ is(click.tab.id, tabId, "Click event tab ID is correct");
+
+ yield BrowserTestUtils.removeTab(tab);
+ yield first.unload();
+ yield second.unload();
+});
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -7,16 +7,17 @@
* clickBrowserAction clickPageAction
* getBrowserActionPopup getPageActionPopup
* closeBrowserAction closePageAction
* promisePopupShown promisePopupHidden
* openContextMenu closeContextMenu
* openExtensionContextMenu closeExtensionContextMenu
* openActionContextMenu openSubmenu closeActionContextMenu
* openTabContextMenu closeTabContextMenu
+ * openToolsMenu closeToolsMenu
* imageBuffer getListStyleImage getPanelForNode
* awaitExtensionPanel awaitPopupResize
* promiseContentDimensions alterContent
*/
const {AppConstants} = Cu.import("resource://gre/modules/AppConstants.jsm", {});
const {CustomizableUI} = Cu.import("resource:///modules/CustomizableUI.jsm", {});
@@ -258,16 +259,45 @@ function* openExtensionContextMenu(selec
function* closeExtensionContextMenu(itemToSelect, modifiers = {}) {
let contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
let popupHiddenPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden");
EventUtils.synthesizeMouseAtCenter(itemToSelect, modifiers);
yield popupHiddenPromise;
}
+async function openToolsMenu(win = window) {
+ const node = win.document.getElementById("tools-menu");
+ const menu = win.document.getElementById("menu_ToolsPopup");
+ const shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+ if (AppConstants.platform === "macosx") {
+ // We can't open menubar items on OSX, so mocking instead.
+ menu.dispatchEvent(new MouseEvent("popupshowing"));
+ menu.dispatchEvent(new MouseEvent("popupshown"));
+ } else {
+ node.open = true;
+ }
+ await shown;
+ return menu;
+}
+
+function closeToolsMenu(itemToSelect, win = window) {
+ const menu = win.document.getElementById("menu_ToolsPopup");
+ const hidden = BrowserTestUtils.waitForEvent(menu, "popuphidden");
+ if (AppConstants.platform === "macosx") {
+ // Mocking on OSX, see above.
+ itemToSelect.doCommand();
+ menu.dispatchEvent(new MouseEvent("popuphiding"));
+ menu.dispatchEvent(new MouseEvent("popuphidden"));
+ } else {
+ EventUtils.synthesizeMouseAtCenter(itemToSelect, {}, win);
+ }
+ return hidden;
+}
+
function* openChromeContextMenu(menuId, target, win = window) {
const node = win.document.querySelector(target);
const menu = win.document.getElementById(menuId);
const shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
EventUtils.synthesizeMouseAtCenter(node, {type: "contextmenu"}, win);
yield shown;
return menu;
}