Bug 1268020 - Implement "tools_menu" context
MozReview-Commit-ID: KPUsBbqyQTC
--- a/browser/components/extensions/ext-menus.js
+++ b/browser/components/extensions/ext-menus.js
@@ -298,69 +298,47 @@ var gMenuBuilder = {
itemsToCleanUp: new Set(),
};
// Called from pageAction or browserAction popup.
global.actionContextMenu = function(contextData) {
gMenuBuilder.buildActionContextMenu(contextData);
};
+const contextsMap = {
+ onAudio: "audio",
+ onEditableArea: "editable",
+ inFrame: "frame",
+ onImage: "image",
+ onLink: "link",
+ onPassword: "password",
+ isTextSelected: "selection",
+ onVideo: "video",
+
+ onBrowserAction: "browser_action",
+ onPageAction: "page_action",
+ onTab: "tab",
+ inToolsMenu: "tools_menu",
+};
+
const getMenuContexts = contextData => {
let contexts = new Set();
- if (contextData.inFrame) {
- contexts.add("frame");
- }
-
- if (contextData.isTextSelected) {
- contexts.add("selection");
- }
-
- if (contextData.onLink) {
- contexts.add("link");
- }
-
- if (contextData.onEditableArea) {
- contexts.add("editable");
- }
-
- if (contextData.onPassword) {
- contexts.add("password");
- }
-
- if (contextData.onImage) {
- contexts.add("image");
- }
-
- if (contextData.onVideo) {
- contexts.add("video");
- }
-
- if (contextData.onAudio) {
- contexts.add("audio");
- }
-
- if (contextData.onPageAction) {
- contexts.add("page_action");
- }
-
- if (contextData.onBrowserAction) {
- contexts.add("browser_action");
- }
-
- if (contextData.onTab) {
- contexts.add("tab");
+ for (const [key, value] of Object.entries(contextsMap)) {
+ if (contextData[key]) {
+ contexts.add(value);
+ }
}
if (contexts.size === 0) {
contexts.add("page");
}
// New non-content contexts supported in Firefox are not part of "all".
- if (!contextData.onTab) {
+ if (!contextData.onTab && !contextData.inToolsMenu) {
contexts.add("all");
}
return contexts;
};
function MenuItem(extension, createProperties, isRoot = false) {
this.extension = extension;
@@ -577,47 +555,58 @@ MenuItem.prototype = {
}
}
return true;
},
};
// While any extensions are active, this Tracker registers to observe/listen
-// for contex-menu events from both content and chrome.
+// 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");
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", menuTracker);
+ for (const id of this.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});
}
},
};
--- a/browser/components/extensions/schemas/menus.json
+++ b/browser/components/extensions/schemas/menus.json
@@ -17,34 +17,42 @@
}]
}
]
},
{
"namespace": "contextMenus",
"permissions": ["contextMenus"],
"description": "Use the browser.contextMenus API to add items to the browser's context menu. You can choose what types of objects your context menu additions apply to, such as images, hyperlinks, and pages.",
- "$import": "menus"
+ "$import": "menus",
+ "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' and 'tools_menu'."
+ }
+ ]
},
{
"namespace": "menus",
"permissions": ["menus"],
"description": "Use the browser.menus API to add items to the browser's menus. You can choose what types of objects your context menu additions apply to, such as images, hyperlinks, and pages.",
"properties": {
"ACTION_MENU_TOP_LEVEL_LIMIT": {
"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_menus.js
+++ b/browser/components/extensions/test/browser/browser_ext_menus.js
@@ -250,8 +250,64 @@ add_task(async function test_multiple_co
await closeExtensionContextMenu(popup.firstChild);
const info = await extension.awaitMessage("click");
is(info.menuItemId, "child", "onClicked the correct item");
await BrowserTestUtils.removeTab(tab);
await extension.unload();
});
+
+add_task(async function test_tools_menu() {
+ const first = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ async background() {
+ await browser.menus.create({title: "alpha", contexts: ["tools_menu"]});
+ await browser.menus.create({title: "beta", contexts: ["tools_menu"]});
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ const second = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ async background() {
+ await browser.menus.create({title: "gamma", contexts: ["tools_menu"]});
+ browser.menus.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 = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+ await first.startup();
+ await second.startup();
+
+ await first.awaitMessage("ready");
+ const tabId = await second.awaitMessage("ready");
+ const menu = await 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 = await 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");
+
+ await BrowserTestUtils.removeTab(tab);
+ await first.unload();
+ await second.unload();
+});
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -8,16 +8,17 @@
* getBrowserActionPopup getPageActionPopup
* closeBrowserAction closePageAction
* promisePopupShown promisePopupHidden
* openContextMenu closeContextMenu
* openContextMenuInSidebar openContextMenuInPopup
* openExtensionContextMenu closeExtensionContextMenu
* openActionContextMenu openSubmenu closeActionContextMenu
* openTabContextMenu closeTabContextMenu
+ * openToolsMenu closeToolsMenu
* imageBuffer imageBufferFromDataURI
* getListStyleImage getPanelForNode
* awaitExtensionPanel awaitPopupResize
* promiseContentDimensions alterContent
* promisePrefChangeObserved openContextMenuInFrame
* promiseAnimationFrame getCustomizableUIPanelID
*/
@@ -331,16 +332,45 @@ async function closeExtensionContextMenu
let popupHiddenPromise = BrowserTestUtils.waitForEvent(contentAreaContextMenu, "popuphidden");
EventUtils.synthesizeMouseAtCenter(itemToSelect, modifiers);
await popupHiddenPromise;
// Bug 1351638: parent menu fails to close intermittently, make sure it does.
contentAreaContextMenu.hidePopup();
}
+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;
+}
+
async 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);
await shown;
return menu;
}