/* 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 EventEmitter = require(
"resource://devtools/shared/event-emitter.js");
const isOSX = Services.appinfo.OS ===
"Darwin";
const { KeyCodes } = require(
"resource://devtools/client/shared/keycodes.js");
// 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 described 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 String str
* The shortcut string to parse, following this document:
* https://github.com/electron/electron/blob/master/docs/api/accelerator.md
*/
KeyShortcuts.parseElectronKey =
function (str) {
// If a localized string is found but has no value in the properties file,
// getStr will return `null`. See Bug 1569572.
if (
typeof str !==
"string") {
console.error(
"Invalid key passed to parseElectronKey, stacktrace below");
console.trace();
return null;
}
const modifiers = str.split(
"+");
let key = modifiers.pop();
const shortcut = {
ctrl:
false,
meta:
false,
alt:
false,
shift:
false,
// Set for character keys
key: undefined,
// Set for non-character keys
keyCode: undefined,
};
for (
const 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) {
if (shortcut.alt) {
// When Alt is involved, some platforms (macOS) give different printable characters
// for the `key` value, like `®` for the key `R`. In this case, prefer matching by
// `keyCode` instead.
shortcut.keyCode = KeyCodes[`DOM_VK_${key.toUpperCase()}`];
shortcut.keyCodeString = key;
}
else {
// 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) {
if (shortcut ===
null) {
// parseElectronKey might return null in several situations.
return "";
}
const 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(
"+");
};
/*
* Parse an xul-like key string and return an electron-like string.
*/
KeyShortcuts.parseXulKey =
function (modifiers, shortcut) {
modifiers = modifiers
.split(
",")
.map(mod => {
if (mod ==
"alt") {
return "Alt";
}
else if (mod ==
"shift") {
return "Shift";
}
else if (mod ==
"accel") {
return "CmdOrCtrl";
}
return mod;
})
.join(
"+");
if (shortcut.startsWith(
"VK_")) {
shortcut = shortcut.substr(3);
}
return modifiers +
"+" + shortcut;
};
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) {
// Check the `keyCode` to see whether it's a character (see also Bug 1493646)
const char = String.fromCharCode(event.keyCode);
let isAlphabetical =
char.length == 1 &&
char.match(/[a-zA-Z]/);
// Shift is a special modifier, it may implicitly be required if the expected key
// is a special character accessible via shift.
if (!isAlphabetical) {
isAlphabetical = event.key && event.key.match(/[a-zA-Z]/);
}
// OSX: distinguish cmd+[key] from cmd+shift+[key] shortcuts (Bug 1300458)
const 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.
const 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 (
const [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)) {
const shortcut = KeyShortcuts.parseElectronKey(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);
},
};
module.exports = KeyShortcuts;