2016-05-10 11:40:51 +03:00
|
|
|
/* 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");
|
2018-03-07 12:17:03 +03:00
|
|
|
const EventEmitter = require("devtools/shared/event-emitter");
|
2016-05-10 11:40:51 +03:00
|
|
|
const isOSX = Services.appinfo.OS === "Darwin";
|
2016-08-09 00:41:37 +03:00
|
|
|
const {KeyCodes} = require("devtools/client/shared/keycodes");
|
2016-05-10 11:40:51 +03:00
|
|
|
|
|
|
|
// 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",
|
2016-06-02 13:04:14 +03:00
|
|
|
"Tab": "DOM_VK_TAB",
|
2016-05-10 11:40:51 +03:00
|
|
|
"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.
|
2016-06-03 12:24:31 +03:00
|
|
|
* @param DOMElement target
|
|
|
|
* Optional DOM Element on which we should listen events from.
|
|
|
|
* If omitted, we listen for all events fired on `window`.
|
2016-05-10 11:40:51 +03:00
|
|
|
*/
|
2016-06-03 12:24:31 +03:00
|
|
|
function KeyShortcuts({ window, target }) {
|
2016-05-10 11:40:51 +03:00
|
|
|
this.window = window;
|
2016-06-03 12:24:31 +03:00
|
|
|
this.target = target || window;
|
2016-05-10 11:40:51 +03:00
|
|
|
this.keys = new Map();
|
|
|
|
this.eventEmitter = new EventEmitter();
|
2016-06-03 12:24:31 +03:00
|
|
|
this.target.addEventListener("keydown", this);
|
2016-05-10 11:40:51 +03:00
|
|
|
}
|
|
|
|
|
2016-05-12 13:07:56 +03:00
|
|
|
/*
|
|
|
|
* 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
|
|
|
|
*/
|
2018-03-12 21:24:38 +03:00
|
|
|
KeyShortcuts.parseElectronKey = function(window, str) {
|
2016-05-12 13:07:56 +03:00
|
|
|
let modifiers = str.split("+");
|
|
|
|
let key = modifiers.pop();
|
2016-05-10 11:40:51 +03:00
|
|
|
|
2016-05-12 13:07:56 +03:00
|
|
|
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) {
|
2016-05-10 11:40:51 +03:00
|
|
|
shortcut.meta = true;
|
|
|
|
} else {
|
2016-05-12 13:07:56 +03:00
|
|
|
shortcut.ctrl = true;
|
2016-05-10 11:40:51 +03:00
|
|
|
}
|
2016-05-12 13:07:56 +03:00
|
|
|
} else if (["Control", "Ctrl"].includes(mod)) {
|
|
|
|
shortcut.ctrl = true;
|
|
|
|
} else if (mod === "Shift") {
|
|
|
|
shortcut.shift = true;
|
2016-05-10 11:40:51 +03:00
|
|
|
} else {
|
2016-06-30 11:29:56 +03:00
|
|
|
console.error("Unsupported modifier:", mod, "from key:", str);
|
|
|
|
return null;
|
2016-05-10 11:40:51 +03:00
|
|
|
}
|
2016-05-12 13:07:56 +03:00
|
|
|
}
|
2016-05-10 11:40:51 +03:00
|
|
|
|
2016-05-25 18:31:06 +03:00
|
|
|
// 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) {
|
2018-03-15 01:02:07 +03:00
|
|
|
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();
|
|
|
|
}
|
2016-05-12 13:07:56 +03:00
|
|
|
} else if (key in ElectronKeysMapping) {
|
|
|
|
// Maps the others manually to DOM API DOM_VK_*
|
|
|
|
key = ElectronKeysMapping[key];
|
2016-08-09 00:41:37 +03:00
|
|
|
shortcut.keyCode = KeyCodes[key];
|
2016-05-25 18:31:05 +03:00
|
|
|
// Used only to stringify the shortcut
|
|
|
|
shortcut.keyCodeString = key;
|
2016-08-18 21:36:47 +03:00
|
|
|
shortcut.key = key;
|
2016-05-12 13:07:56 +03:00
|
|
|
} else {
|
2016-06-30 11:29:56 +03:00
|
|
|
console.error("Unsupported key:", key);
|
|
|
|
return null;
|
2016-05-12 13:07:56 +03:00
|
|
|
}
|
2016-05-10 11:40:51 +03:00
|
|
|
|
2016-05-12 13:07:56 +03:00
|
|
|
return shortcut;
|
2016-05-17 21:25:54 +03:00
|
|
|
};
|
2016-05-12 13:07:56 +03:00
|
|
|
|
2018-03-12 21:24:38 +03:00
|
|
|
KeyShortcuts.stringify = function(shortcut) {
|
2016-05-25 18:31:05 +03:00
|
|
|
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("+");
|
|
|
|
};
|
|
|
|
|
2016-05-12 13:07:56 +03:00
|
|
|
KeyShortcuts.prototype = {
|
2016-05-10 11:40:51 +03:00
|
|
|
destroy() {
|
2016-06-03 12:24:31 +03:00
|
|
|
this.target.removeEventListener("keydown", this);
|
2016-05-10 11:40:51 +03:00
|
|
|
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;
|
|
|
|
}
|
2016-09-06 12:43:11 +03:00
|
|
|
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;
|
|
|
|
}
|
2016-05-10 11:40:51 +03:00
|
|
|
}
|
2016-09-06 12:43:11 +03:00
|
|
|
|
2016-05-10 11:40:51 +03:00
|
|
|
if (shortcut.keyCode) {
|
|
|
|
return event.keyCode == shortcut.keyCode;
|
2016-08-18 21:36:47 +03:00
|
|
|
} else if (event.key in ElectronKeysMapping) {
|
|
|
|
return ElectronKeysMapping[event.key] === shortcut.key;
|
2016-05-10 11:40:51 +03:00
|
|
|
}
|
2016-08-18 21:36:47 +03:00
|
|
|
|
|
|
|
// get the key from the keyCode if key is not provided.
|
|
|
|
let key = event.key || String.fromCharCode(event.keyCode);
|
|
|
|
|
2016-05-25 18:31:06 +03:00
|
|
|
// 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.
|
2016-08-18 21:36:47 +03:00
|
|
|
return key.toLowerCase() == shortcut.key ||
|
2016-05-25 18:31:06 +03:00
|
|
|
(shortcut.key.match(/[0-9]/) &&
|
|
|
|
event.keyCode == shortcut.key.charCodeAt(0));
|
2016-05-10 11:40:51 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
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") {
|
2016-05-19 17:35:45 +03:00
|
|
|
throw new Error("KeyShortcuts.on() expects a function as " +
|
|
|
|
"second argument");
|
2016-05-10 11:40:51 +03:00
|
|
|
}
|
|
|
|
if (!this.keys.has(key)) {
|
2016-05-12 13:07:56 +03:00
|
|
|
let shortcut = KeyShortcuts.parseElectronKey(this.window, key);
|
2016-06-30 11:29:56 +03:00
|
|
|
// The key string is wrong and we were unable to compute the key shortcut
|
|
|
|
if (!shortcut) {
|
|
|
|
return;
|
|
|
|
}
|
2016-05-10 11:40:51 +03:00
|
|
|
this.keys.set(key, shortcut);
|
|
|
|
}
|
|
|
|
this.eventEmitter.on(key, listener);
|
|
|
|
},
|
|
|
|
|
|
|
|
off(key, listener) {
|
|
|
|
this.eventEmitter.off(key, listener);
|
|
|
|
},
|
|
|
|
};
|
2016-12-17 06:44:56 +03:00
|
|
|
|
|
|
|
module.exports = KeyShortcuts;
|