summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/ShortcutUtils.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/ShortcutUtils.sys.mjs')
-rw-r--r--toolkit/modules/ShortcutUtils.sys.mjs409
1 files changed, 409 insertions, 0 deletions
diff --git a/toolkit/modules/ShortcutUtils.sys.mjs b/toolkit/modules/ShortcutUtils.sys.mjs
new file mode 100644
index 0000000000..e45855602b
--- /dev/null
+++ b/toolkit/modules/ShortcutUtils.sys.mjs
@@ -0,0 +1,409 @@
+/* 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/. */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineLazyGetter(lazy, "PlatformKeys", function () {
+ return Services.strings.createBundle(
+ "chrome://global-platform/locale/platformKeys.properties"
+ );
+});
+
+ChromeUtils.defineLazyGetter(lazy, "Keys", function () {
+ return Services.strings.createBundle(
+ "chrome://global/locale/keys.properties"
+ );
+});
+
+export var ShortcutUtils = {
+ IS_VALID: "valid",
+ INVALID_KEY: "invalid_key",
+ INVALID_MODIFIER: "invalid_modifier",
+ INVALID_COMBINATION: "invalid_combination",
+ DUPLICATE_MODIFIER: "duplicate_modifier",
+ MODIFIER_REQUIRED: "modifier_required",
+
+ CLOSE_TAB: "CLOSE_TAB",
+ CYCLE_TABS: "CYCLE_TABS",
+ TOGGLE_CARET_BROWSING: "TOGGLE_CARET_BROWSING",
+ MOVE_TAB_BACKWARD: "MOVE_TAB_BACKWARD",
+ MOVE_TAB_FORWARD: "MOVE_TAB_FORWARD",
+ NEXT_TAB: "NEXT_TAB",
+ PREVIOUS_TAB: "PREVIOUS_TAB",
+
+ /**
+ * Prettifies the modifier keys for an element.
+ *
+ * @param Node aElemKey
+ * The key element to get the modifiers from.
+ * @return string
+ * A prettified and properly separated modifier keys string.
+ */
+ prettifyShortcut(aElemKey) {
+ let elemString = this.getModifierString(aElemKey.getAttribute("modifiers"));
+ let key = this.getKeyString(
+ aElemKey.getAttribute("keycode"),
+ aElemKey.getAttribute("key")
+ );
+ return elemString + key;
+ },
+
+ metaKeyIsCommandKey() {
+ return AppConstants.platform == "macosx";
+ },
+
+ getModifierString(elemMod) {
+ let elemString = "";
+ let haveCloverLeaf = false;
+
+ if (elemMod.match("accel")) {
+ if (Services.appinfo.OS == "Darwin") {
+ haveCloverLeaf = true;
+ } else {
+ elemString +=
+ lazy.PlatformKeys.GetStringFromName("VK_CONTROL") +
+ lazy.PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ }
+ if (elemMod.match("access")) {
+ if (Services.appinfo.OS == "Darwin") {
+ elemString +=
+ lazy.PlatformKeys.GetStringFromName("VK_CONTROL") +
+ lazy.PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ } else {
+ elemString +=
+ lazy.PlatformKeys.GetStringFromName("VK_ALT") +
+ lazy.PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ }
+ if (elemMod.match("meta") && !this.metaKeyIsCommandKey()) {
+ elemString +=
+ lazy.PlatformKeys.GetStringFromName("VK_COMMAND_OR_WIN") +
+ lazy.PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ if (elemMod.match("shift")) {
+ elemString +=
+ lazy.PlatformKeys.GetStringFromName("VK_SHIFT") +
+ lazy.PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ if (elemMod.match("alt")) {
+ elemString +=
+ lazy.PlatformKeys.GetStringFromName("VK_ALT") +
+ lazy.PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ if (elemMod.match("ctrl") || elemMod.match("control")) {
+ elemString +=
+ lazy.PlatformKeys.GetStringFromName("VK_CONTROL") +
+ lazy.PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+ if (elemMod.match("meta") && this.metaKeyIsCommandKey()) {
+ elemString +=
+ lazy.PlatformKeys.GetStringFromName("VK_COMMAND_OR_WIN") +
+ lazy.PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+
+ if (haveCloverLeaf) {
+ elemString +=
+ lazy.PlatformKeys.GetStringFromName("VK_COMMAND_OR_WIN") +
+ lazy.PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+ }
+
+ return elemString;
+ },
+
+ getKeyString(keyCode, keyAttribute) {
+ let key;
+ if (keyCode) {
+ keyCode = keyCode.toUpperCase();
+ if (AppConstants.platform == "macosx") {
+ // Return fancy Unicode symbols for some keys.
+ switch (keyCode) {
+ case "VK_LEFT":
+ return "\u2190"; // U+2190 LEFTWARDS ARROW
+ case "VK_RIGHT":
+ return "\u2192"; // U+2192 RIGHTWARDS ARROW
+ }
+ }
+ try {
+ let bundle = keyCode == "VK_RETURN" ? lazy.PlatformKeys : lazy.Keys;
+ // Some keys might not exist in the locale file, which will throw.
+ key = bundle.GetStringFromName(keyCode);
+ } catch (ex) {
+ console.error("Error finding ", keyCode, ": ", ex);
+ key = keyCode.replace(/^VK_/, "");
+ }
+ } else {
+ key = keyAttribute.toUpperCase();
+ }
+
+ return key;
+ },
+
+ getKeyAttribute(chromeKey) {
+ if (/^[A-Z]$/.test(chromeKey)) {
+ // We use the key attribute for single characters.
+ return ["key", chromeKey];
+ }
+ return ["keycode", this.getKeycodeAttribute(chromeKey)];
+ },
+
+ /**
+ * Determines the corresponding XUL keycode from the given chrome key.
+ *
+ * For example:
+ *
+ * input | output
+ * ---------------------------------------
+ * "PageUp" | "VK_PAGE_UP"
+ * "Delete" | "VK_DELETE"
+ *
+ * @param {string} chromeKey The chrome key (e.g. "PageUp", "Space", ...)
+ * @returns {string} The constructed value for the Key's 'keycode' attribute.
+ */
+ getKeycodeAttribute(chromeKey) {
+ if (/^[0-9]/.test(chromeKey)) {
+ return `VK_${chromeKey}`;
+ }
+ return `VK${chromeKey.replace(/([A-Z])/g, "_$&").toUpperCase()}`;
+ },
+
+ findShortcut(aElemCommand) {
+ let document = aElemCommand.ownerDocument;
+ return document.querySelector(
+ 'key[command="' + aElemCommand.getAttribute("id") + '"]'
+ );
+ },
+
+ chromeModifierKeyMap: {
+ Alt: "alt",
+ Command: "accel",
+ Ctrl: "accel",
+ MacCtrl: "control",
+ Shift: "shift",
+ },
+
+ /**
+ * Determines the corresponding XUL modifiers from the chrome modifiers.
+ *
+ * For example:
+ *
+ * input | output
+ * ---------------------------------------
+ * ["Ctrl", "Shift"] | "accel,shift"
+ * ["MacCtrl"] | "control"
+ *
+ * @param {Array} chromeModifiers The array of chrome modifiers.
+ * @returns {string} The constructed value for the Key's 'modifiers' attribute.
+ */
+ getModifiersAttribute(chromeModifiers) {
+ return Array.from(chromeModifiers, modifier => {
+ return ShortcutUtils.chromeModifierKeyMap[modifier];
+ })
+ .sort()
+ .join(",");
+ },
+
+ /**
+ * Validate if a shortcut string is valid and return an error code if it
+ * isn't valid.
+ *
+ * For example:
+ *
+ * input | output
+ * ---------------------------------------
+ * "Ctrl+Shift+A" | IS_VALID
+ * "Shift+F" | MODIFIER_REQUIRED
+ * "Command+>" | INVALID_KEY
+ *
+ * @param {string} string The shortcut string.
+ * @returns {string} The code for the validation result.
+ */
+ validate(string) {
+ // A valid shortcut key for a webextension manifest
+ const MEDIA_KEYS =
+ /^(MediaNextTrack|MediaPlayPause|MediaPrevTrack|MediaStop)$/;
+ const BASIC_KEYS =
+ /^([A-Z0-9]|Comma|Period|Home|End|PageUp|PageDown|Space|Insert|Delete|Up|Down|Left|Right)$/;
+ const FUNCTION_KEYS = /^(F[1-9]|F1[0-2])$/;
+
+ if (MEDIA_KEYS.test(string.trim())) {
+ return this.IS_VALID;
+ }
+
+ let modifiers = string.split("+").map(s => s.trim());
+ let key = modifiers.pop();
+
+ let chromeModifiers = modifiers.map(
+ m => ShortcutUtils.chromeModifierKeyMap[m]
+ );
+ // If the modifier wasn't found it will be undefined.
+ if (chromeModifiers.some(modifier => !modifier)) {
+ return this.INVALID_MODIFIER;
+ }
+
+ switch (modifiers.length) {
+ case 0:
+ // A lack of modifiers is only allowed with function keys.
+ if (!FUNCTION_KEYS.test(key)) {
+ return this.MODIFIER_REQUIRED;
+ }
+ break;
+ case 1:
+ // Shift is only allowed on its own with function keys.
+ if (chromeModifiers[0] == "shift" && !FUNCTION_KEYS.test(key)) {
+ return this.MODIFIER_REQUIRED;
+ }
+ break;
+ case 2:
+ if (chromeModifiers[0] == chromeModifiers[1]) {
+ return this.DUPLICATE_MODIFIER;
+ }
+ break;
+ default:
+ return this.INVALID_COMBINATION;
+ }
+
+ if (!BASIC_KEYS.test(key) && !FUNCTION_KEYS.test(key)) {
+ return this.INVALID_KEY;
+ }
+
+ return this.IS_VALID;
+ },
+
+ /**
+ * Attempt to find a key for a given shortcut string, such as
+ * "Ctrl+Shift+A" and determine if it is a system shortcut.
+ *
+ * @param {Object} win The window to look for key elements in.
+ * @param {string} value The shortcut string.
+ * @returns {boolean} Whether a system shortcut was found or not.
+ */
+ isSystem(win, value) {
+ let modifiers = value.split("+");
+ let chromeKey = modifiers.pop();
+ let modifiersString = this.getModifiersAttribute(modifiers);
+ let keycode = this.getKeycodeAttribute(chromeKey);
+
+ let baseSelector = "key";
+ if (modifiers.length) {
+ baseSelector += `[modifiers="${modifiersString}"]`;
+ }
+
+ let keyEl = win.document.querySelector(
+ [
+ `${baseSelector}[key="${chromeKey}"]`,
+ `${baseSelector}[key="${chromeKey.toLowerCase()}"]`,
+ `${baseSelector}[keycode="${keycode}"]`,
+ ].join(",")
+ );
+ return keyEl && !keyEl.closest("keyset").id.startsWith("ext-keyset-id");
+ },
+
+ /**
+ * Determine what action a KeyboardEvent should perform, if any.
+ *
+ * @param {KeyboardEvent} event The event to check for a related system action.
+ * @returns {string} A string identifying the action, or null if no action is found.
+ */
+ // eslint-disable-next-line complexity
+ getSystemActionForEvent(event, { rtl } = {}) {
+ // On Windows, Win key state is not strictly checked so that we can ignore
+ // Win key state to check the other modifier state.
+ const meaningfulMetaKey = event.metaKey && AppConstants.platform != "win";
+ // This is set to true only when the Meta key is accel key on the platform.
+ const accelMetaKey = event.metaKey && this.metaKeyIsCommandKey();
+ switch (event.keyCode) {
+ case event.DOM_VK_TAB:
+ if (event.ctrlKey && !event.altKey && !meaningfulMetaKey) {
+ return ShortcutUtils.CYCLE_TABS;
+ }
+ break;
+ case event.DOM_VK_F7:
+ // shift + F7 is the default DevTools shortcut for the Style Editor.
+ if (!event.shiftKey) {
+ return ShortcutUtils.TOGGLE_CARET_BROWSING;
+ }
+ break;
+ case event.DOM_VK_PAGE_UP:
+ if (
+ event.ctrlKey &&
+ !event.shiftKey &&
+ !event.altKey &&
+ !meaningfulMetaKey
+ ) {
+ return ShortcutUtils.PREVIOUS_TAB;
+ }
+ if (
+ event.ctrlKey &&
+ event.shiftKey &&
+ !event.altKey &&
+ !meaningfulMetaKey
+ ) {
+ return ShortcutUtils.MOVE_TAB_BACKWARD;
+ }
+ break;
+ case event.DOM_VK_PAGE_DOWN:
+ if (
+ event.ctrlKey &&
+ !event.shiftKey &&
+ !event.altKey &&
+ !meaningfulMetaKey
+ ) {
+ return ShortcutUtils.NEXT_TAB;
+ }
+ if (
+ event.ctrlKey &&
+ event.shiftKey &&
+ !event.altKey &&
+ !meaningfulMetaKey
+ ) {
+ return ShortcutUtils.MOVE_TAB_FORWARD;
+ }
+ break;
+ case event.DOM_VK_LEFT:
+ if (accelMetaKey && event.altKey && !event.shiftKey && !event.ctrlKey) {
+ return ShortcutUtils.PREVIOUS_TAB;
+ }
+ break;
+ case event.DOM_VK_RIGHT:
+ if (accelMetaKey && event.altKey && !event.shiftKey && !event.ctrlKey) {
+ return ShortcutUtils.NEXT_TAB;
+ }
+ break;
+ }
+
+ if (AppConstants.platform == "macosx") {
+ if (!event.altKey && event.metaKey) {
+ switch (event.charCode) {
+ case "}".charCodeAt(0):
+ if (rtl) {
+ return ShortcutUtils.PREVIOUS_TAB;
+ }
+ return ShortcutUtils.NEXT_TAB;
+ case "{".charCodeAt(0):
+ if (rtl) {
+ return ShortcutUtils.NEXT_TAB;
+ }
+ return ShortcutUtils.PREVIOUS_TAB;
+ }
+ }
+ }
+ // Not on Mac from now on.
+ if (AppConstants.platform != "macosx") {
+ if (
+ event.ctrlKey &&
+ !event.shiftKey &&
+ event.keyCode == KeyEvent.DOM_VK_F4
+ ) {
+ return ShortcutUtils.CLOSE_TAB;
+ }
+ }
+
+ return null;
+ },
+};
+
+Object.freeze(ShortcutUtils);