key-shortcuts.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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. const Services = require("Services");
  6. const EventEmitter = require("devtools/shared/event-emitter");
  7. const isOSX = Services.appinfo.OS === "Darwin";
  8. const {KeyCodes} = require("devtools/client/shared/keycodes");
  9. // List of electron keys mapped to DOM API (DOM_VK_*) key code
  10. const ElectronKeysMapping = {
  11. "F1": "DOM_VK_F1",
  12. "F2": "DOM_VK_F2",
  13. "F3": "DOM_VK_F3",
  14. "F4": "DOM_VK_F4",
  15. "F5": "DOM_VK_F5",
  16. "F6": "DOM_VK_F6",
  17. "F7": "DOM_VK_F7",
  18. "F8": "DOM_VK_F8",
  19. "F9": "DOM_VK_F9",
  20. "F10": "DOM_VK_F10",
  21. "F11": "DOM_VK_F11",
  22. "F12": "DOM_VK_F12",
  23. "F13": "DOM_VK_F13",
  24. "F14": "DOM_VK_F14",
  25. "F15": "DOM_VK_F15",
  26. "F16": "DOM_VK_F16",
  27. "F17": "DOM_VK_F17",
  28. "F18": "DOM_VK_F18",
  29. "F19": "DOM_VK_F19",
  30. "F20": "DOM_VK_F20",
  31. "F21": "DOM_VK_F21",
  32. "F22": "DOM_VK_F22",
  33. "F23": "DOM_VK_F23",
  34. "F24": "DOM_VK_F24",
  35. "Space": "DOM_VK_SPACE",
  36. "Backspace": "DOM_VK_BACK_SPACE",
  37. "Delete": "DOM_VK_DELETE",
  38. "Insert": "DOM_VK_INSERT",
  39. "Return": "DOM_VK_RETURN",
  40. "Enter": "DOM_VK_RETURN",
  41. "Up": "DOM_VK_UP",
  42. "Down": "DOM_VK_DOWN",
  43. "Left": "DOM_VK_LEFT",
  44. "Right": "DOM_VK_RIGHT",
  45. "Home": "DOM_VK_HOME",
  46. "End": "DOM_VK_END",
  47. "PageUp": "DOM_VK_PAGE_UP",
  48. "PageDown": "DOM_VK_PAGE_DOWN",
  49. "Escape": "DOM_VK_ESCAPE",
  50. "Esc": "DOM_VK_ESCAPE",
  51. "Tab": "DOM_VK_TAB",
  52. "VolumeUp": "DOM_VK_VOLUME_UP",
  53. "VolumeDown": "DOM_VK_VOLUME_DOWN",
  54. "VolumeMute": "DOM_VK_VOLUME_MUTE",
  55. "PrintScreen": "DOM_VK_PRINTSCREEN",
  56. };
  57. /**
  58. * Helper to listen for keyboard events decribed in .properties file.
  59. *
  60. * let shortcuts = new KeyShortcuts({
  61. * window
  62. * });
  63. * shortcuts.on("Ctrl+F", event => {
  64. * // `event` is the KeyboardEvent which relates to the key shortcuts
  65. * });
  66. *
  67. * @param DOMWindow window
  68. * The window object of the document to listen events from.
  69. * @param DOMElement target
  70. * Optional DOM Element on which we should listen events from.
  71. * If omitted, we listen for all events fired on `window`.
  72. */
  73. function KeyShortcuts({ window, target }) {
  74. this.window = window;
  75. this.target = target || window;
  76. this.keys = new Map();
  77. this.eventEmitter = new EventEmitter();
  78. this.target.addEventListener("keydown", this);
  79. }
  80. /*
  81. * Parse an electron-like key string and return a normalized object which
  82. * allow efficient match on DOM key event. The normalized object matches DOM
  83. * API.
  84. *
  85. * @param DOMWindow window
  86. * Any DOM Window object, just to fetch its `KeyboardEvent` object
  87. * @param String str
  88. * The shortcut string to parse, following this document:
  89. * https://github.com/electron/electron/blob/master/docs/api/accelerator.md
  90. */
  91. KeyShortcuts.parseElectronKey = function (window, str) {
  92. let modifiers = str.split("+");
  93. let key = modifiers.pop();
  94. let shortcut = {
  95. ctrl: false,
  96. meta: false,
  97. alt: false,
  98. shift: false,
  99. // Set for character keys
  100. key: undefined,
  101. // Set for non-character keys
  102. keyCode: undefined,
  103. };
  104. for (let mod of modifiers) {
  105. if (mod === "Alt") {
  106. shortcut.alt = true;
  107. } else if (["Command", "Cmd"].includes(mod)) {
  108. shortcut.meta = true;
  109. } else if (["CommandOrControl", "CmdOrCtrl"].includes(mod)) {
  110. if (isOSX) {
  111. shortcut.meta = true;
  112. } else {
  113. shortcut.ctrl = true;
  114. }
  115. } else if (["Control", "Ctrl"].includes(mod)) {
  116. shortcut.ctrl = true;
  117. } else if (mod === "Shift") {
  118. shortcut.shift = true;
  119. } else {
  120. console.error("Unsupported modifier:", mod, "from key:", str);
  121. return null;
  122. }
  123. }
  124. // Plus is a special case. It's a character key and shouldn't be matched
  125. // against a keycode as it is only accessible via Shift/Capslock
  126. if (key === "Plus") {
  127. key = "+";
  128. }
  129. if (typeof key === "string" && key.length === 1) {
  130. // Match any single character
  131. shortcut.key = key.toLowerCase();
  132. } else if (key in ElectronKeysMapping) {
  133. // Maps the others manually to DOM API DOM_VK_*
  134. key = ElectronKeysMapping[key];
  135. shortcut.keyCode = KeyCodes[key];
  136. // Used only to stringify the shortcut
  137. shortcut.keyCodeString = key;
  138. shortcut.key = key;
  139. } else {
  140. console.error("Unsupported key:", key);
  141. return null;
  142. }
  143. return shortcut;
  144. };
  145. KeyShortcuts.stringify = function (shortcut) {
  146. let list = [];
  147. if (shortcut.alt) {
  148. list.push("Alt");
  149. }
  150. if (shortcut.ctrl) {
  151. list.push("Ctrl");
  152. }
  153. if (shortcut.meta) {
  154. list.push("Cmd");
  155. }
  156. if (shortcut.shift) {
  157. list.push("Shift");
  158. }
  159. let key;
  160. if (shortcut.key) {
  161. key = shortcut.key.toUpperCase();
  162. } else {
  163. key = shortcut.keyCodeString;
  164. }
  165. list.push(key);
  166. return list.join("+");
  167. };
  168. KeyShortcuts.prototype = {
  169. destroy() {
  170. this.target.removeEventListener("keydown", this);
  171. this.keys.clear();
  172. },
  173. doesEventMatchShortcut(event, shortcut) {
  174. if (shortcut.meta != event.metaKey) {
  175. return false;
  176. }
  177. if (shortcut.ctrl != event.ctrlKey) {
  178. return false;
  179. }
  180. if (shortcut.alt != event.altKey) {
  181. return false;
  182. }
  183. if (shortcut.shift != event.shiftKey) {
  184. // Shift is a special modifier, it may implicitely be required if the expected key
  185. // is a special character accessible via shift.
  186. let isAlphabetical = event.key && event.key.match(/[a-zA-Z]/);
  187. // OSX: distinguish cmd+[key] from cmd+shift+[key] shortcuts (Bug 1300458)
  188. let cmdShortcut = shortcut.meta && !shortcut.alt && !shortcut.ctrl;
  189. if (isAlphabetical || cmdShortcut) {
  190. return false;
  191. }
  192. }
  193. if (shortcut.keyCode) {
  194. return event.keyCode == shortcut.keyCode;
  195. } else if (event.key in ElectronKeysMapping) {
  196. return ElectronKeysMapping[event.key] === shortcut.key;
  197. }
  198. // get the key from the keyCode if key is not provided.
  199. let key = event.key || String.fromCharCode(event.keyCode);
  200. // For character keys, we match if the final character is the expected one.
  201. // But for digits we also accept indirect match to please azerty keyboard,
  202. // which requires Shift to be pressed to get digits.
  203. return key.toLowerCase() == shortcut.key ||
  204. (shortcut.key.match(/[0-9]/) &&
  205. event.keyCode == shortcut.key.charCodeAt(0));
  206. },
  207. handleEvent(event) {
  208. for (let [key, shortcut] of this.keys) {
  209. if (this.doesEventMatchShortcut(event, shortcut)) {
  210. this.eventEmitter.emit(key, event);
  211. }
  212. }
  213. },
  214. on(key, listener) {
  215. if (typeof listener !== "function") {
  216. throw new Error("KeyShortcuts.on() expects a function as " +
  217. "second argument");
  218. }
  219. if (!this.keys.has(key)) {
  220. let shortcut = KeyShortcuts.parseElectronKey(this.window, key);
  221. // The key string is wrong and we were unable to compute the key shortcut
  222. if (!shortcut) {
  223. return;
  224. }
  225. this.keys.set(key, shortcut);
  226. }
  227. this.eventEmitter.on(key, listener);
  228. },
  229. off(key, listener) {
  230. this.eventEmitter.off(key, listener);
  231. },
  232. };
  233. exports.KeyShortcuts = KeyShortcuts;