diff options
Diffstat (limited to '')
-rw-r--r-- | devtools/client/framework/browser-menus.js | 340 |
1 files changed, 340 insertions, 0 deletions
diff --git a/devtools/client/framework/browser-menus.js b/devtools/client/framework/browser-menus.js new file mode 100644 index 0000000000..bc070fc78d --- /dev/null +++ b/devtools/client/framework/browser-menus.js @@ -0,0 +1,340 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * This module inject dynamically menu items into browser UI. + * + * Menu definitions are fetched from: + * - devtools/client/menus for top level entires + * - devtools/client/definitions for tool-specifics entries + */ + +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); +const MENUS_L10N = new LocalizationHelper( + "devtools/client/locales/menus.properties" +); + +loader.lazyRequireGetter( + this, + "gDevTools", + "resource://devtools/client/framework/devtools.js", + true +); +loader.lazyRequireGetter( + this, + "gDevToolsBrowser", + "resource://devtools/client/framework/devtools-browser.js", + true +); +loader.lazyRequireGetter( + this, + "Telemetry", + "resource://devtools/client/shared/telemetry.js" +); + +let telemetry = null; + +// Keep list of inserted DOM Elements in order to remove them on unload +// Maps browser xul document => list of DOM Elements +const FragmentsCache = new Map(); + +function l10n(key) { + return MENUS_L10N.getStr(key); +} + +/** + * Create a xul:menuitem element + * + * @param {HTMLDocument} doc + * The document to which menus are to be added. + * @param {String} id + * Element id. + * @param {String} label + * Menu label. + * @param {String} accesskey (optional) + * Access key of the menuitem, used as shortcut while opening the menu. + * @param {Boolean} isCheckbox (optional) + * If true, the menuitem will act as a checkbox and have an optional + * tick on its left. + * @param {String} appMenuL10nId (optional) + * A Fluent key to set the appmenu-data-l10n-id attribute of the menuitem + * to. This can then be used to show a different string when cloning the + * menuitem to show in the AppMenu or panel contexts. + * + * @return XULMenuItemElement + */ +function createMenuItem({ + doc, + id, + label, + accesskey, + isCheckbox, + appMenuL10nId, +}) { + const menuitem = doc.createXULElement("menuitem"); + menuitem.id = id; + menuitem.setAttribute("label", label); + if (accesskey) { + menuitem.setAttribute("accesskey", accesskey); + } + if (isCheckbox) { + menuitem.setAttribute("type", "checkbox"); + menuitem.setAttribute("autocheck", "false"); + } + if (appMenuL10nId) { + menuitem.setAttribute("appmenu-data-l10n-id", appMenuL10nId); + } + return menuitem; +} + +/** + * Add a menu entry for a tool definition + * + * @param {Object} toolDefinition + * Tool definition of the tool to add a menu entry. + * @param {HTMLDocument} doc + * The document to which the tool menu item is to be added. + */ +function createToolMenuElements(toolDefinition, doc) { + const id = toolDefinition.id; + const menuId = "menuitem_" + id; + + // Prevent multiple entries for the same tool. + if (doc.getElementById(menuId)) { + return null; + } + + const oncommand = async function (id, event) { + try { + const window = event.target.ownerDocument.defaultView; + await gDevToolsBrowser.selectToolCommand(window, id, Cu.now()); + sendEntryPointTelemetry(window); + } catch (e) { + console.error(`Exception while opening ${id}: ${e}\n${e.stack}`); + } + }.bind(null, id); + + const menuitem = createMenuItem({ + doc, + id: "menuitem_" + id, + label: toolDefinition.menuLabel || toolDefinition.label, + accesskey: toolDefinition.accesskey, + appMenuL10nId: toolDefinition.appMenuL10nId, + }); + // Refer to the key in order to display the key shortcut at menu ends + // This <key> element is being created by devtools/client/devtools-startup.js + menuitem.setAttribute("key", "key_" + id); + menuitem.addEventListener("command", oncommand); + + return menuitem; +} + +/** + * Send entry point telemetry explaining how the devtools were launched when + * launched from the System Menu.. This functionality also lives inside + * `devtools/startup/devtools-startup.js` but that codepath is only used the + * first time a toolbox is opened for a tab. + */ +function sendEntryPointTelemetry(window) { + if (!telemetry) { + telemetry = new Telemetry(); + } + + telemetry.addEventProperty(window, "open", "tools", null, "shortcut", ""); + + telemetry.addEventProperty( + window, + "open", + "tools", + null, + "entrypoint", + "SystemMenu" + ); +} + +/** + * Create xul menuitem, key elements for a given tool. + * And then insert them into browser DOM. + * + * @param {HTMLDocument} doc + * The document to which the tool is to be registered. + * @param {Object} toolDefinition + * Tool definition of the tool to register. + * @param {Object} prevDef + * The tool definition after which the tool menu item is to be added. + */ +function insertToolMenuElements(doc, toolDefinition, prevDef) { + const menuitem = createToolMenuElements(toolDefinition, doc); + if (!menuitem) { + return; + } + + let ref; + if (prevDef) { + const menuitem = doc.getElementById("menuitem_" + prevDef.id); + ref = menuitem?.nextSibling ? menuitem.nextSibling : null; + } else { + ref = doc.getElementById("menu_devtools_remotedebugging"); + } + + if (ref) { + ref.parentNode.insertBefore(menuitem, ref); + } +} +exports.insertToolMenuElements = insertToolMenuElements; + +/** + * Remove a tool's menuitem from a window + * + * @param {string} toolId + * Id of the tool to add a menu entry for + * @param {HTMLDocument} doc + * The document to which the tool menu item is to be removed from + */ +function removeToolFromMenu(toolId, doc) { + const key = doc.getElementById("key_" + toolId); + if (key) { + key.remove(); + } + + const menuitem = doc.getElementById("menuitem_" + toolId); + if (menuitem) { + menuitem.remove(); + } +} +exports.removeToolFromMenu = removeToolFromMenu; + +/** + * Add all tools to the developer tools menu of a window. + * + * @param {HTMLDocument} doc + * The document to which the tool items are to be added. + */ +function addAllToolsToMenu(doc) { + const fragMenuItems = doc.createDocumentFragment(); + + for (const toolDefinition of gDevTools.getToolDefinitionArray()) { + if (!toolDefinition.inMenu) { + continue; + } + + const menuItem = createToolMenuElements(toolDefinition, doc); + + if (!menuItem) { + continue; + } + + fragMenuItems.appendChild(menuItem); + } + + const mps = doc.getElementById("menu_devtools_remotedebugging"); + if (mps) { + mps.parentNode.insertBefore(fragMenuItems, mps); + } +} + +/** + * Add global menus that are not panel specific. + * + * @param {HTMLDocument} doc + * The document to which menus are to be added. + */ +function addTopLevelItems(doc) { + const menuItems = doc.createDocumentFragment(); + + const { menuitems } = require("resource://devtools/client/menus.js"); + for (const item of menuitems) { + if (item.separator) { + const separator = doc.createXULElement("menuseparator"); + separator.id = item.id; + menuItems.appendChild(separator); + } else { + const { id, l10nKey } = item; + + // Create a <menuitem> + const menuitem = createMenuItem({ + doc, + id, + label: l10n(l10nKey + ".label"), + accesskey: l10n(l10nKey + ".accesskey"), + isCheckbox: item.checkbox, + appMenuL10nId: item.appMenuL10nId, + }); + menuitem.addEventListener("command", item.oncommand); + menuItems.appendChild(menuitem); + + if (item.keyId) { + menuitem.setAttribute("key", "key_" + item.keyId); + } + } + } + + // Cache all nodes before insertion to be able to remove them on unload + const nodes = []; + for (const node of menuItems.children) { + nodes.push(node); + } + FragmentsCache.set(doc, nodes); + + const menu = doc.getElementById("menuWebDeveloperPopup"); + menu.appendChild(menuItems); + + // There is still "Page Source" and "Task Manager" menuitems hardcoded + // into browser.xhtml. Instead of manually inserting everything around it, + // move them to the expected position. + const pageSourceMenu = doc.getElementById("menu_pageSource"); + const extensionsForDevelopersMenu = doc.getElementById( + "extensionsForDevelopers" + ); + menu.insertBefore(pageSourceMenu, extensionsForDevelopersMenu); + + const taskManagerMenu = doc.getElementById("menu_taskManager"); + const remoteDebuggingMenu = doc.getElementById( + "menu_devtools_remotedebugging" + ); + menu.insertBefore(taskManagerMenu, remoteDebuggingMenu); +} + +/** + * Remove global menus that are not panel specific. + * + * @param {HTMLDocument} doc + * The document to which menus are to be added. + */ +function removeTopLevelItems(doc) { + const nodes = FragmentsCache.get(doc); + if (!nodes) { + return; + } + FragmentsCache.delete(doc); + for (const node of nodes) { + node.remove(); + } +} + +/** + * Add menus to a browser document + * + * @param {HTMLDocument} doc + * The document to which menus are to be added. + */ +exports.addMenus = function (doc) { + addTopLevelItems(doc); + + addAllToolsToMenu(doc); +}; + +/** + * Remove menus from a browser document + * + * @param {HTMLDocument} doc + * The document to which menus are to be removed. + */ +exports.removeMenus = function (doc) { + // We only remove top level entries. Per-tool entries are removed while + // unregistering each tool. + removeTopLevelItems(doc); +}; |