mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 12:51:06 +00:00
4954865f6e
It became unused in bug 1290227 Differential Revision: https://phabricator.services.mozilla.com/D219717
307 lines
8.3 KiB
JavaScript
307 lines
8.3 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 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;
|