browser-menus.js (9326B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 /** 8 * This module inject dynamically menu items into browser UI. 9 * 10 * Menu definitions are fetched from: 11 * - devtools/client/menus for top level entires 12 * - devtools/client/definitions for tool-specifics entries 13 */ 14 15 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); 16 const MENUS_L10N = new LocalizationHelper( 17 "devtools/client/locales/menus.properties" 18 ); 19 20 loader.lazyRequireGetter( 21 this, 22 "gDevTools", 23 "resource://devtools/client/framework/devtools.js", 24 true 25 ); 26 loader.lazyRequireGetter( 27 this, 28 "gDevToolsBrowser", 29 "resource://devtools/client/framework/devtools-browser.js", 30 true 31 ); 32 loader.lazyRequireGetter( 33 this, 34 "Telemetry", 35 "resource://devtools/client/shared/telemetry.js" 36 ); 37 38 let telemetry = null; 39 40 // Keep list of inserted DOM Elements in order to remove them on unload 41 // Maps browser xul document => list of DOM Elements 42 const FragmentsCache = new Map(); 43 44 function l10n(key) { 45 return MENUS_L10N.getStr(key); 46 } 47 48 /** 49 * Create a xul:menuitem element 50 * 51 * @param {HTMLDocument} doc 52 * The document to which menus are to be added. 53 * @param {string} id 54 * Element id. 55 * @param {string} label 56 * Menu label. 57 * @param {string} accesskey (optional) 58 * Access key of the menuitem, used as shortcut while opening the menu. 59 * @param {boolean} isCheckbox (optional) 60 * If true, the menuitem will act as a checkbox and have an optional 61 * tick on its left. 62 * @param {string} appMenuL10nId (optional) 63 * A Fluent key to set the appmenu-data-l10n-id attribute of the menuitem 64 * to. This can then be used to show a different string when cloning the 65 * menuitem to show in the AppMenu or panel contexts. 66 * 67 * @return XULMenuItemElement 68 */ 69 function createMenuItem({ 70 doc, 71 id, 72 label, 73 accesskey, 74 isCheckbox, 75 appMenuL10nId, 76 }) { 77 const menuitem = doc.createXULElement("menuitem"); 78 menuitem.id = id; 79 menuitem.setAttribute("label", label); 80 if (accesskey) { 81 menuitem.setAttribute("accesskey", accesskey); 82 } 83 if (isCheckbox) { 84 menuitem.setAttribute("type", "checkbox"); 85 menuitem.setAttribute("autocheck", "false"); 86 } 87 if (appMenuL10nId) { 88 menuitem.setAttribute("appmenu-data-l10n-id", appMenuL10nId); 89 } 90 return menuitem; 91 } 92 93 /** 94 * Add a menu entry for a tool definition 95 * 96 * @param {object} toolDefinition 97 * Tool definition of the tool to add a menu entry. 98 * @param {HTMLDocument} doc 99 * The document to which the tool menu item is to be added. 100 */ 101 function createToolMenuElements(toolDefinition, doc) { 102 const id = toolDefinition.id; 103 const menuId = "menuitem_" + id; 104 105 // Prevent multiple entries for the same tool. 106 if (doc.getElementById(menuId)) { 107 return null; 108 } 109 110 const oncommand = async function (id, event) { 111 try { 112 const window = event.target.ownerDocument.defaultView; 113 await gDevToolsBrowser.selectToolCommand(window, id, ChromeUtils.now()); 114 sendEntryPointTelemetry(window); 115 } catch (e) { 116 console.error(`Exception while opening ${id}: ${e}\n${e.stack}`); 117 } 118 }.bind(null, id); 119 120 const menuitem = createMenuItem({ 121 doc, 122 id: "menuitem_" + id, 123 label: toolDefinition.menuLabel || toolDefinition.label, 124 accesskey: toolDefinition.accesskey, 125 appMenuL10nId: toolDefinition.appMenuL10nId, 126 }); 127 // Refer to the key in order to display the key shortcut at menu ends 128 // This <key> element is being created by devtools/client/devtools-startup.js 129 menuitem.setAttribute("key", "key_" + id); 130 menuitem.addEventListener("command", oncommand); 131 132 return menuitem; 133 } 134 135 /** 136 * Send entry point telemetry explaining how the devtools were launched when 137 * launched from the System Menu.. This functionality also lives inside 138 * `devtools/startup/devtools-startup.js` but that codepath is only used the 139 * first time a toolbox is opened for a tab. 140 */ 141 function sendEntryPointTelemetry(window) { 142 if (!telemetry) { 143 telemetry = new Telemetry(); 144 } 145 146 telemetry.addEventProperty(window, "open", "tools", null, "shortcut", ""); 147 148 telemetry.addEventProperty( 149 window, 150 "open", 151 "tools", 152 null, 153 "entrypoint", 154 "SystemMenu" 155 ); 156 } 157 158 /** 159 * Create xul menuitem, key elements for a given tool. 160 * And then insert them into browser DOM. 161 * 162 * @param {HTMLDocument} doc 163 * The document to which the tool is to be registered. 164 * @param {object} toolDefinition 165 * Tool definition of the tool to register. 166 * @param {object} prevDef 167 * The tool definition after which the tool menu item is to be added. 168 */ 169 function insertToolMenuElements(doc, toolDefinition, prevDef) { 170 const menuitem = createToolMenuElements(toolDefinition, doc); 171 if (!menuitem) { 172 return; 173 } 174 175 let ref; 176 if (prevDef) { 177 const menuitem = doc.getElementById("menuitem_" + prevDef.id); 178 ref = menuitem?.nextSibling ? menuitem.nextSibling : null; 179 } else { 180 ref = doc.getElementById("menu_devtools_remotedebugging"); 181 } 182 183 if (ref) { 184 ref.parentNode.insertBefore(menuitem, ref); 185 } 186 } 187 exports.insertToolMenuElements = insertToolMenuElements; 188 189 /** 190 * Remove a tool's menuitem from a window 191 * 192 * @param {string} toolId 193 * Id of the tool to add a menu entry for 194 * @param {HTMLDocument} doc 195 * The document to which the tool menu item is to be removed from 196 */ 197 function removeToolFromMenu(toolId, doc) { 198 const key = doc.getElementById("key_" + toolId); 199 if (key) { 200 key.remove(); 201 } 202 203 const menuitem = doc.getElementById("menuitem_" + toolId); 204 if (menuitem) { 205 menuitem.remove(); 206 } 207 } 208 exports.removeToolFromMenu = removeToolFromMenu; 209 210 /** 211 * Add all tools to the developer tools menu of a window. 212 * 213 * @param {HTMLDocument} doc 214 * The document to which the tool items are to be added. 215 */ 216 function addAllToolsToMenu(doc) { 217 const fragMenuItems = doc.createDocumentFragment(); 218 219 for (const toolDefinition of gDevTools.getToolDefinitionArray()) { 220 if (!toolDefinition.inMenu) { 221 continue; 222 } 223 224 const menuItem = createToolMenuElements(toolDefinition, doc); 225 226 if (!menuItem) { 227 continue; 228 } 229 230 fragMenuItems.appendChild(menuItem); 231 } 232 233 const mps = doc.getElementById("menu_devtools_remotedebugging"); 234 if (mps) { 235 mps.parentNode.insertBefore(fragMenuItems, mps); 236 } 237 } 238 239 /** 240 * Add global menus that are not panel specific. 241 * 242 * @param {HTMLDocument} doc 243 * The document to which menus are to be added. 244 */ 245 function addTopLevelItems(doc) { 246 const menuItems = doc.createDocumentFragment(); 247 248 const { menuitems } = require("resource://devtools/client/menus.js"); 249 for (const item of menuitems) { 250 if (item.separator) { 251 const separator = doc.createXULElement("menuseparator"); 252 separator.id = item.id; 253 menuItems.appendChild(separator); 254 } else { 255 const { id, l10nKey } = item; 256 257 // Create a <menuitem> 258 const menuitem = createMenuItem({ 259 doc, 260 id, 261 label: l10n(l10nKey + ".label"), 262 accesskey: l10n(l10nKey + ".accesskey"), 263 isCheckbox: item.checkbox, 264 appMenuL10nId: item.appMenuL10nId, 265 }); 266 menuitem.addEventListener("command", item.oncommand); 267 menuItems.appendChild(menuitem); 268 269 if (item.keyId) { 270 menuitem.setAttribute("key", "key_" + item.keyId); 271 } 272 } 273 } 274 275 // Cache all nodes before insertion to be able to remove them on unload 276 const nodes = []; 277 for (const node of menuItems.children) { 278 nodes.push(node); 279 } 280 FragmentsCache.set(doc, nodes); 281 282 const menu = doc.getElementById("menuWebDeveloperPopup"); 283 menu.appendChild(menuItems); 284 285 // There is still "Page Source" and "Task Manager" menuitems hardcoded 286 // into browser.xhtml. Instead of manually inserting everything around it, 287 // move them to the expected position. 288 const pageSourceMenu = doc.getElementById("menu_pageSource"); 289 const extensionsForDevelopersMenu = doc.getElementById( 290 "extensionsForDevelopers" 291 ); 292 menu.insertBefore(pageSourceMenu, extensionsForDevelopersMenu); 293 294 const taskManagerMenu = doc.getElementById("menu_taskManager"); 295 const remoteDebuggingMenu = doc.getElementById( 296 "menu_devtools_remotedebugging" 297 ); 298 menu.insertBefore(taskManagerMenu, remoteDebuggingMenu); 299 } 300 301 /** 302 * Remove global menus that are not panel specific. 303 * 304 * @param {HTMLDocument} doc 305 * The document to which menus are to be added. 306 */ 307 function removeTopLevelItems(doc) { 308 const nodes = FragmentsCache.get(doc); 309 if (!nodes) { 310 return; 311 } 312 FragmentsCache.delete(doc); 313 for (const node of nodes) { 314 node.remove(); 315 } 316 } 317 318 /** 319 * Add menus to a browser document 320 * 321 * @param {HTMLDocument} doc 322 * The document to which menus are to be added. 323 */ 324 exports.addMenus = function (doc) { 325 addTopLevelItems(doc); 326 327 addAllToolsToMenu(doc); 328 }; 329 330 /** 331 * Remove menus from a browser document 332 * 333 * @param {HTMLDocument} doc 334 * The document to which menus are to be removed. 335 */ 336 exports.removeMenus = function (doc) { 337 // We only remove top level entries. Per-tool entries are removed while 338 // unregistering each tool. 339 removeTopLevelItems(doc); 340 };