Mypal/devtools/client/shared/key-shortcuts.js

252 lines
7.0 KiB
JavaScript

/* 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;