browser-menus.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  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. "use strict";
  5. /**
  6. * This module inject dynamically menu items and key shortcuts into browser UI.
  7. *
  8. * Menu and shortcut definitions are fetched from:
  9. * - devtools/client/menus for top level entires
  10. * - devtools/client/definitions for tool-specifics entries
  11. */
  12. const {LocalizationHelper} = require("devtools/shared/l10n");
  13. const MENUS_L10N = new LocalizationHelper("devtools/client/locales/menus.properties");
  14. loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
  15. loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
  16. // Keep list of inserted DOM Elements in order to remove them on unload
  17. // Maps browser xul document => list of DOM Elements
  18. const FragmentsCache = new Map();
  19. function l10n(key) {
  20. return MENUS_L10N.getStr(key);
  21. }
  22. /**
  23. * Create a xul:key element
  24. *
  25. * @param {XULDocument} doc
  26. * The document to which keys are to be added.
  27. * @param {String} id
  28. * key's id, automatically prefixed with "key_".
  29. * @param {String} shortcut
  30. * The key shortcut value.
  31. * @param {String} keytext
  32. * If `shortcut` refers to a function key, refers to the localized
  33. * string to describe a non-character shortcut.
  34. * @param {String} modifiers
  35. * Space separated list of modifier names.
  36. * @param {Function} oncommand
  37. * The function to call when the shortcut is pressed.
  38. *
  39. * @return XULKeyElement
  40. */
  41. function createKey({ doc, id, shortcut, keytext, modifiers, oncommand }) {
  42. let k = doc.createElement("key");
  43. k.id = "key_" + id;
  44. if (shortcut.startsWith("VK_")) {
  45. k.setAttribute("keycode", shortcut);
  46. if (keytext) {
  47. k.setAttribute("keytext", keytext);
  48. }
  49. } else {
  50. k.setAttribute("key", shortcut);
  51. }
  52. if (modifiers) {
  53. k.setAttribute("modifiers", modifiers);
  54. }
  55. // Bug 371900: command event is fired only if "oncommand" attribute is set.
  56. k.setAttribute("oncommand", ";");
  57. k.addEventListener("command", oncommand);
  58. return k;
  59. }
  60. /**
  61. * Create a xul:menuitem element
  62. *
  63. * @param {XULDocument} doc
  64. * The document to which keys are to be added.
  65. * @param {String} id
  66. * Element id.
  67. * @param {String} label
  68. * Menu label.
  69. * @param {String} accesskey (optional)
  70. * Access key of the menuitem, used as shortcut while opening the menu.
  71. * @param {Boolean} isCheckbox (optional)
  72. * If true, the menuitem will act as a checkbox and have an optional
  73. * tick on its left.
  74. *
  75. * @return XULMenuItemElement
  76. */
  77. function createMenuItem({ doc, id, label, accesskey, isCheckbox }) {
  78. let menuitem = doc.createElement("menuitem");
  79. menuitem.id = id;
  80. menuitem.setAttribute("label", label);
  81. if (accesskey) {
  82. menuitem.setAttribute("accesskey", accesskey);
  83. }
  84. if (isCheckbox) {
  85. menuitem.setAttribute("type", "checkbox");
  86. menuitem.setAttribute("autocheck", "false");
  87. }
  88. return menuitem;
  89. }
  90. /**
  91. * Add a <key> to <keyset id="devtoolsKeyset">.
  92. * Appending a <key> element is not always enough. The <keyset> needs
  93. * to be detached and reattached to make sure the <key> is taken into
  94. * account (see bug 832984).
  95. *
  96. * @param {XULDocument} doc
  97. * The document to which keys are to be added
  98. * @param {XULElement} or {DocumentFragment} keys
  99. * Keys to add
  100. */
  101. function attachKeybindingsToBrowser(doc, keys) {
  102. let devtoolsKeyset = doc.getElementById("devtoolsKeyset");
  103. if (!devtoolsKeyset) {
  104. devtoolsKeyset = doc.createElement("keyset");
  105. devtoolsKeyset.setAttribute("id", "devtoolsKeyset");
  106. }
  107. devtoolsKeyset.appendChild(keys);
  108. let mainKeyset = doc.getElementById("mainKeyset");
  109. mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset);
  110. }
  111. /**
  112. * Add a menu entry for a tool definition
  113. *
  114. * @param {Object} toolDefinition
  115. * Tool definition of the tool to add a menu entry.
  116. * @param {XULDocument} doc
  117. * The document to which the tool menu item is to be added.
  118. */
  119. function createToolMenuElements(toolDefinition, doc) {
  120. let id = toolDefinition.id;
  121. let appmenuId = "appmenuitem_" + id;
  122. let menuId = "menuitem_" + id;
  123. // Prevent multiple entries for the same tool.
  124. if (doc.getElementById(appmenuId) || doc.getElementById(menuId)) {
  125. return;
  126. }
  127. let oncommand = function (id, event) {
  128. let window = event.target.ownerDocument.defaultView;
  129. gDevToolsBrowser.selectToolCommand(window.gBrowser, id);
  130. }.bind(null, id);
  131. let key = null;
  132. if (toolDefinition.key) {
  133. key = createKey({
  134. doc,
  135. id,
  136. shortcut: toolDefinition.key,
  137. modifiers: toolDefinition.modifiers,
  138. oncommand: oncommand
  139. });
  140. }
  141. let appmenuitem = createMenuItem({
  142. doc,
  143. id: "appmenuitem_" + id,
  144. label: toolDefinition.menuLabel || toolDefinition.label,
  145. accesskey: null
  146. });
  147. let menuitem = createMenuItem({
  148. doc,
  149. id: "menuitem_" + id,
  150. label: toolDefinition.menuLabel || toolDefinition.label,
  151. accesskey: toolDefinition.accesskey
  152. });
  153. if (key) {
  154. // Refer to the key in order to display the key shortcut at menu ends
  155. menuitem.setAttribute("key", key.id);
  156. }
  157. appmenuitem.addEventListener("command", oncommand);
  158. menuitem.addEventListener("command", oncommand);
  159. return {
  160. key,
  161. appmenuitem,
  162. menuitem
  163. };
  164. }
  165. /**
  166. * Create xul menuitem, key elements for a given tool.
  167. * And then insert them into browser DOM.
  168. *
  169. * @param {XULDocument} doc
  170. * The document to which the tool is to be registered.
  171. * @param {Object} toolDefinition
  172. * Tool definition of the tool to register.
  173. * @param {Object} prevDef
  174. * The tool definition after which the tool menu item is to be added.
  175. */
  176. function insertToolMenuElements(doc, toolDefinition, prevDef) {
  177. let { key, appmenuitem, menuitem } = createToolMenuElements(toolDefinition, doc);
  178. if (key) {
  179. attachKeybindingsToBrowser(doc, key);
  180. }
  181. let amp;
  182. if (prevDef) {
  183. let appmenuitem = doc.getElementById("appmenuitem_" + prevDef.id);
  184. amp = appmenuitem && appmenuitem.nextSibling ? appmenuitem.nextSibling : null;
  185. } else {
  186. amp = doc.getElementById("appmenu_devtools_separator");
  187. }
  188. if (amp) {
  189. amp.parentNode.insertBefore(appmenuitem, amp);
  190. }
  191. let mp;
  192. if (prevDef) {
  193. let menuitem = doc.getElementById("menuitem_" + prevDef.id);
  194. mp = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null;
  195. } else {
  196. mp = doc.getElementById("menu_devtools_separator");
  197. }
  198. if (mp) {
  199. mp.parentNode.insertBefore(menuitem, mp);
  200. }
  201. }
  202. exports.insertToolMenuElements = insertToolMenuElements;
  203. /**
  204. * Remove a tool's menuitem from a window
  205. *
  206. * @param {string} toolId
  207. * Id of the tool to add a menu entry for
  208. * @param {XULDocument} doc
  209. * The document to which the tool menu item is to be removed from
  210. */
  211. function removeToolFromMenu(toolId, doc) {
  212. let key = doc.getElementById("key_" + toolId);
  213. if (key) {
  214. key.remove();
  215. }
  216. let appmenuitem = doc.getElementById("appmenuitem_" + toolId);
  217. if (appmenuitem) {
  218. appmenuitem.remove();
  219. }
  220. let menuitem = doc.getElementById("menuitem_" + toolId);
  221. if (menuitem) {
  222. menuitem.remove();
  223. }
  224. }
  225. exports.removeToolFromMenu = removeToolFromMenu;
  226. /**
  227. * Add all tools to the developer tools menu of a window.
  228. *
  229. * @param {XULDocument} doc
  230. * The document to which the tool items are to be added.
  231. */
  232. function addAllToolsToMenu(doc) {
  233. let fragKeys = doc.createDocumentFragment();
  234. let fragAppMenuItems = doc.createDocumentFragment();
  235. let fragMenuItems = doc.createDocumentFragment();
  236. for (let toolDefinition of gDevTools.getToolDefinitionArray()) {
  237. if (!toolDefinition.inMenu) {
  238. continue;
  239. }
  240. let elements = createToolMenuElements(toolDefinition, doc);
  241. if (!elements) {
  242. continue;
  243. }
  244. if (elements.key) {
  245. fragKeys.appendChild(elements.key);
  246. }
  247. fragAppMenuItems.appendChild(elements.appmenuitem);
  248. fragMenuItems.appendChild(elements.menuitem);
  249. }
  250. attachKeybindingsToBrowser(doc, fragKeys);
  251. let amps = doc.getElementById("appmenu_devtools_separator");
  252. if (amps) {
  253. amps.parentNode.insertBefore(fragAppMenuItems, amps);
  254. }
  255. let mps = doc.getElementById("menu_devtools_separator");
  256. if (mps) {
  257. mps.parentNode.insertBefore(fragMenuItems, mps);
  258. }
  259. }
  260. /**
  261. * Add global menus and shortcuts that are not panel specific.
  262. *
  263. * @param {XULDocument} doc
  264. * The document to which keys and menus are to be added.
  265. */
  266. function addTopLevelItems(doc) {
  267. let keys = doc.createDocumentFragment();
  268. let appmenuItems = doc.createDocumentFragment();
  269. let menuItems = doc.createDocumentFragment();
  270. let { menuitems } = require("../menus");
  271. for (let item of menuitems) {
  272. if (item.separator) {
  273. let appseparator = doc.createElement("menuseparator");
  274. appseparator.id = "app" + item.id;
  275. let separator = doc.createElement("menuseparator");
  276. separator.id = item.id;
  277. appmenuItems.appendChild(appseparator);
  278. menuItems.appendChild(separator);
  279. } else {
  280. let { id, l10nKey } = item;
  281. // Create a <menuitem>
  282. let appmenuitem = createMenuItem({
  283. doc,
  284. id: "app" + id,
  285. label: l10n(l10nKey + ".label"),
  286. accesskey: null,
  287. isCheckbox: item.checkbox
  288. });
  289. let menuitem = createMenuItem({
  290. doc,
  291. id,
  292. label: l10n(l10nKey + ".label"),
  293. accesskey: l10n(l10nKey + ".accesskey"),
  294. isCheckbox: item.checkbox
  295. });
  296. appmenuitem.addEventListener("command", item.oncommand);
  297. menuitem.addEventListener("command", item.oncommand);
  298. appmenuItems.appendChild(appmenuitem);
  299. menuItems.appendChild(menuitem);
  300. if (item.key && l10nKey) {
  301. // Create a <key>
  302. let shortcut = l10n(l10nKey + ".key");
  303. let key = createKey({
  304. doc,
  305. id: item.key.id,
  306. shortcut: shortcut,
  307. keytext: shortcut.startsWith("VK_") ? l10n(l10nKey + ".keytext") : null,
  308. modifiers: item.key.modifiers,
  309. oncommand: item.oncommand
  310. });
  311. // Refer to the key in order to display the key shortcut at menu ends
  312. menuitem.setAttribute("key", key.id);
  313. keys.appendChild(key);
  314. }
  315. if (item.additionalKeys) {
  316. // Create additional <key>
  317. for (let key of item.additionalKeys) {
  318. let shortcut = l10n(key.l10nKey + ".key");
  319. let node = createKey({
  320. doc,
  321. id: key.id,
  322. shortcut: shortcut,
  323. keytext: shortcut.startsWith("VK_") ? l10n(key.l10nKey + ".keytext") : null,
  324. modifiers: key.modifiers,
  325. oncommand: item.oncommand
  326. });
  327. keys.appendChild(node);
  328. }
  329. }
  330. }
  331. }
  332. // Cache all nodes before insertion to be able to remove them on unload
  333. let nodes = [];
  334. for (let node of keys.children) {
  335. nodes.push(node);
  336. }
  337. for (let node of appmenuItems.children) {
  338. nodes.push(node);
  339. }
  340. for (let node of menuItems.children) {
  341. nodes.push(node);
  342. }
  343. FragmentsCache.set(doc, nodes);
  344. attachKeybindingsToBrowser(doc, keys);
  345. // There are hardcoded menu items in the Web Developer menus plus it is a
  346. // location of menu items via overlays from extensions so we want to make
  347. // sure the last seperator and the "Get More Tools..." items are last.
  348. // This will emulate the behavior when devtools menu items were actually
  349. // physically present in browser.xul
  350. // Tools > Web Developer
  351. let menu = doc.getElementById("menuWebDeveloperPopup");
  352. // Insert the Devtools Menu Items before everything else
  353. menu.insertBefore(menuItems, menu.firstChild);
  354. // Move the devtools last seperator and Get More Tools menu items to the bottom
  355. let menu_endSeparator = doc.getElementById("menu_devToolsEndSeparator");
  356. let menu_getMoreDevtools = doc.getElementById("menu_getMoreDevtools");
  357. menu.insertBefore(menu_getMoreDevtools, null);
  358. menu.insertBefore(menu_endSeparator, menu_getMoreDevtools);
  359. // Application Menu > Web Developer (If existant)
  360. let appmenu = doc.getElementById("appmenu_webDeveloper_popup");
  361. if (appmenu) {
  362. // Insert the Devtools Menu Items after the hardcoded idless seperator
  363. appmenu.insertBefore(appmenuItems, appmenu.childNodes[2].nextSibling);
  364. // Move the devtools last seperator and Get More Tools menu items to the bottom
  365. let appmenu_endSeparator = doc.getElementById("appmenu_devToolsEndSeparator");
  366. let appmenu_getMoreDevtools = doc.getElementById("appmenu_getMoreDevtools");
  367. appmenu.insertBefore(appmenu_getMoreDevtools, null);
  368. appmenu.insertBefore(appmenu_endSeparator, appmenu_getMoreDevtools);
  369. }
  370. }
  371. /**
  372. * Remove global menus and shortcuts that are not panel specific.
  373. *
  374. * @param {XULDocument} doc
  375. * The document to which keys and menus are to be added.
  376. */
  377. function removeTopLevelItems(doc) {
  378. let nodes = FragmentsCache.get(doc);
  379. if (!nodes) {
  380. return;
  381. }
  382. FragmentsCache.delete(doc);
  383. for (let node of nodes) {
  384. node.remove();
  385. }
  386. }
  387. /**
  388. * Add menus and shortcuts to a browser document
  389. *
  390. * @param {XULDocument} doc
  391. * The document to which keys and menus are to be added.
  392. */
  393. exports.addMenus = function (doc) {
  394. addTopLevelItems(doc);
  395. addAllToolsToMenu(doc);
  396. };
  397. /**
  398. * Remove menus and shortcuts from a browser document
  399. *
  400. * @param {XULDocument} doc
  401. * The document to which keys and menus are to be removed.
  402. */
  403. exports.removeMenus = function (doc) {
  404. // We only remove top level entries. Per-tool entries are removed while
  405. // unregistering each tool.
  406. removeTopLevelItems(doc);
  407. };