summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework/browser-menus.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--devtools/client/framework/browser-menus.js340
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);
+};