summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/key-shortcuts.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/key-shortcuts.js')
-rw-r--r--devtools/client/shared/key-shortcuts.js251
1 files changed, 251 insertions, 0 deletions
diff --git a/devtools/client/shared/key-shortcuts.js b/devtools/client/shared/key-shortcuts.js
new file mode 100644
index 000000000..ec7d30bcb
--- /dev/null
+++ b/devtools/client/shared/key-shortcuts.js
@@ -0,0 +1,251 @@
+/* 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";
+
+const Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+const isOSX = Services.appinfo.OS === "Darwin";
+const {KeyCodes} = require("devtools/client/shared/keycodes");
+
+// List of electron keys mapped to DOM API (DOM_VK_*) key code
+const ElectronKeysMapping = {
+ "F1": "DOM_VK_F1",
+ "F2": "DOM_VK_F2",
+ "F3": "DOM_VK_F3",
+ "F4": "DOM_VK_F4",
+ "F5": "DOM_VK_F5",
+ "F6": "DOM_VK_F6",
+ "F7": "DOM_VK_F7",
+ "F8": "DOM_VK_F8",
+ "F9": "DOM_VK_F9",
+ "F10": "DOM_VK_F10",
+ "F11": "DOM_VK_F11",
+ "F12": "DOM_VK_F12",
+ "F13": "DOM_VK_F13",
+ "F14": "DOM_VK_F14",
+ "F15": "DOM_VK_F15",
+ "F16": "DOM_VK_F16",
+ "F17": "DOM_VK_F17",
+ "F18": "DOM_VK_F18",
+ "F19": "DOM_VK_F19",
+ "F20": "DOM_VK_F20",
+ "F21": "DOM_VK_F21",
+ "F22": "DOM_VK_F22",
+ "F23": "DOM_VK_F23",
+ "F24": "DOM_VK_F24",
+ "Space": "DOM_VK_SPACE",
+ "Backspace": "DOM_VK_BACK_SPACE",
+ "Delete": "DOM_VK_DELETE",
+ "Insert": "DOM_VK_INSERT",
+ "Return": "DOM_VK_RETURN",
+ "Enter": "DOM_VK_RETURN",
+ "Up": "DOM_VK_UP",
+ "Down": "DOM_VK_DOWN",
+ "Left": "DOM_VK_LEFT",
+ "Right": "DOM_VK_RIGHT",
+ "Home": "DOM_VK_HOME",
+ "End": "DOM_VK_END",
+ "PageUp": "DOM_VK_PAGE_UP",
+ "PageDown": "DOM_VK_PAGE_DOWN",
+ "Escape": "DOM_VK_ESCAPE",
+ "Esc": "DOM_VK_ESCAPE",
+ "Tab": "DOM_VK_TAB",
+ "VolumeUp": "DOM_VK_VOLUME_UP",
+ "VolumeDown": "DOM_VK_VOLUME_DOWN",
+ "VolumeMute": "DOM_VK_VOLUME_MUTE",
+ "PrintScreen": "DOM_VK_PRINTSCREEN",
+};
+
+/**
+ * Helper to listen for keyboard events decribed in .properties file.
+ *
+ * let shortcuts = new KeyShortcuts({
+ * window
+ * });
+ * shortcuts.on("Ctrl+F", event => {
+ * // `event` is the KeyboardEvent which relates to the key shortcuts
+ * });
+ *
+ * @param DOMWindow window
+ * The window object of the document to listen events from.
+ * @param DOMElement target
+ * Optional DOM Element on which we should listen events from.
+ * If omitted, we listen for all events fired on `window`.
+ */
+function KeyShortcuts({ window, target }) {
+ this.window = window;
+ this.target = target || window;
+ this.keys = new Map();
+ this.eventEmitter = new EventEmitter();
+ this.target.addEventListener("keydown", this);
+}
+
+/*
+ * Parse an electron-like key string and return a normalized object which
+ * allow efficient match on DOM key event. The normalized object matches DOM
+ * API.
+ *
+ * @param DOMWindow window
+ * Any DOM Window object, just to fetch its `KeyboardEvent` object
+ * @param String str
+ * The shortcut string to parse, following this document:
+ * https://github.com/electron/electron/blob/master/docs/api/accelerator.md
+ */
+KeyShortcuts.parseElectronKey = function (window, str) {
+ let modifiers = str.split("+");
+ let key = modifiers.pop();
+
+ let shortcut = {
+ ctrl: false,
+ meta: false,
+ alt: false,
+ shift: false,
+ // Set for character keys
+ key: undefined,
+ // Set for non-character keys
+ keyCode: undefined,
+ };
+ for (let mod of modifiers) {
+ if (mod === "Alt") {
+ shortcut.alt = true;
+ } else if (["Command", "Cmd"].includes(mod)) {
+ shortcut.meta = true;
+ } else if (["CommandOrControl", "CmdOrCtrl"].includes(mod)) {
+ if (isOSX) {
+ shortcut.meta = true;
+ } else {
+ shortcut.ctrl = true;
+ }
+ } else if (["Control", "Ctrl"].includes(mod)) {
+ shortcut.ctrl = true;
+ } else if (mod === "Shift") {
+ shortcut.shift = true;
+ } else {
+ console.error("Unsupported modifier:", mod, "from key:", str);
+ return null;
+ }
+ }
+
+ // Plus is a special case. It's a character key and shouldn't be matched
+ // against a keycode as it is only accessible via Shift/Capslock
+ if (key === "Plus") {
+ key = "+";
+ }
+
+ if (typeof key === "string" && key.length === 1) {
+ // Match any single character
+ shortcut.key = key.toLowerCase();
+ } else if (key in ElectronKeysMapping) {
+ // Maps the others manually to DOM API DOM_VK_*
+ key = ElectronKeysMapping[key];
+ shortcut.keyCode = KeyCodes[key];
+ // Used only to stringify the shortcut
+ shortcut.keyCodeString = key;
+ shortcut.key = key;
+ } else {
+ console.error("Unsupported key:", key);
+ return null;
+ }
+
+ return shortcut;
+};
+
+KeyShortcuts.stringify = function (shortcut) {
+ let list = [];
+ if (shortcut.alt) {
+ list.push("Alt");
+ }
+ if (shortcut.ctrl) {
+ list.push("Ctrl");
+ }
+ if (shortcut.meta) {
+ list.push("Cmd");
+ }
+ if (shortcut.shift) {
+ list.push("Shift");
+ }
+ let key;
+ if (shortcut.key) {
+ key = shortcut.key.toUpperCase();
+ } else {
+ key = shortcut.keyCodeString;
+ }
+ list.push(key);
+ return list.join("+");
+};
+
+KeyShortcuts.prototype = {
+ destroy() {
+ this.target.removeEventListener("keydown", this);
+ this.keys.clear();
+ },
+
+ doesEventMatchShortcut(event, shortcut) {
+ if (shortcut.meta != event.metaKey) {
+ return false;
+ }
+ if (shortcut.ctrl != event.ctrlKey) {
+ return false;
+ }
+ if (shortcut.alt != event.altKey) {
+ return false;
+ }
+ if (shortcut.shift != event.shiftKey) {
+ // Shift is a special modifier, it may implicitely be required if the expected key
+ // is a special character accessible via shift.
+ let isAlphabetical = event.key && event.key.match(/[a-zA-Z]/);
+ // OSX: distinguish cmd+[key] from cmd+shift+[key] shortcuts (Bug 1300458)
+ let cmdShortcut = shortcut.meta && !shortcut.alt && !shortcut.ctrl;
+ if (isAlphabetical || cmdShortcut) {
+ return false;
+ }
+ }
+
+ if (shortcut.keyCode) {
+ return event.keyCode == shortcut.keyCode;
+ } else if (event.key in ElectronKeysMapping) {
+ return ElectronKeysMapping[event.key] === shortcut.key;
+ }
+
+ // get the key from the keyCode if key is not provided.
+ let key = event.key || String.fromCharCode(event.keyCode);
+
+ // For character keys, we match if the final character is the expected one.
+ // But for digits we also accept indirect match to please azerty keyboard,
+ // which requires Shift to be pressed to get digits.
+ return key.toLowerCase() == shortcut.key ||
+ (shortcut.key.match(/[0-9]/) &&
+ event.keyCode == shortcut.key.charCodeAt(0));
+ },
+
+ handleEvent(event) {
+ for (let [key, shortcut] of this.keys) {
+ if (this.doesEventMatchShortcut(event, shortcut)) {
+ this.eventEmitter.emit(key, event);
+ }
+ }
+ },
+
+ on(key, listener) {
+ if (typeof listener !== "function") {
+ throw new Error("KeyShortcuts.on() expects a function as " +
+ "second argument");
+ }
+ if (!this.keys.has(key)) {
+ let shortcut = KeyShortcuts.parseElectronKey(this.window, key);
+ // The key string is wrong and we were unable to compute the key shortcut
+ if (!shortcut) {
+ return;
+ }
+ this.keys.set(key, shortcut);
+ }
+ this.eventEmitter.on(key, listener);
+ },
+
+ off(key, listener) {
+ this.eventEmitter.off(key, listener);
+ },
+};
+exports.KeyShortcuts = KeyShortcuts;