зеркало из https://github.com/mozilla/gecko-dev.git
413 строки
12 KiB
JavaScript
413 строки
12 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";
|
|
|
|
var EXPORTED_SYMBOLS = ["ShortcutUtils"];
|
|
|
|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
);
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
AppConstants: "resource://gre/modules/AppConstants.jsm",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "PlatformKeys", function() {
|
|
return Services.strings.createBundle(
|
|
"chrome://global-platform/locale/platformKeys.properties"
|
|
);
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "Keys", function() {
|
|
return Services.strings.createBundle(
|
|
"chrome://global/locale/keys.properties"
|
|
);
|
|
});
|
|
|
|
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",
|
|
|
|
MOVE_TAB_FORWARD: "MOVE_TAB_FORWARD",
|
|
MOVE_TAB_BACKWARD: "MOVE_TAB_BACKWARD",
|
|
CLOSE_TAB: "CLOSE_TAB",
|
|
CYCLE_TABS: "CYCLE_TABS",
|
|
PREVIOUS_TAB: "PREVIOUS_TAB",
|
|
NEXT_TAB: "NEXT_TAB",
|
|
|
|
/**
|
|
* Prettifies the modifier keys for an element.
|
|
*
|
|
* @param Node aElemKey
|
|
* The key element to get the modifiers from.
|
|
* @param boolean aNoCloverLeaf
|
|
* Pass true to use a descriptive string instead of the cloverleaf symbol. (OS X only)
|
|
* @return string
|
|
* A prettified and properly separated modifier keys string.
|
|
*/
|
|
prettifyShortcut(aElemKey, aNoCloverLeaf) {
|
|
let elemString = this.getModifierString(
|
|
aElemKey.getAttribute("modifiers"),
|
|
aNoCloverLeaf
|
|
);
|
|
let key = this.getKeyString(
|
|
aElemKey.getAttribute("keycode"),
|
|
aElemKey.getAttribute("key")
|
|
);
|
|
return elemString + key;
|
|
},
|
|
|
|
getModifierString(elemMod, aNoCloverLeaf) {
|
|
let elemString = "";
|
|
let haveCloverLeaf = false;
|
|
|
|
if (elemMod.match("accel")) {
|
|
if (Services.appinfo.OS == "Darwin") {
|
|
// XXX bug 779642 Use "Cmd-" literal vs. cloverleaf meta-key until
|
|
// Orion adds variable height lines.
|
|
if (aNoCloverLeaf) {
|
|
elemString += "Cmd-";
|
|
} else {
|
|
haveCloverLeaf = true;
|
|
}
|
|
} else {
|
|
elemString +=
|
|
PlatformKeys.GetStringFromName("VK_CONTROL") +
|
|
PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
|
|
}
|
|
}
|
|
if (elemMod.match("access")) {
|
|
if (Services.appinfo.OS == "Darwin") {
|
|
elemString +=
|
|
PlatformKeys.GetStringFromName("VK_CONTROL") +
|
|
PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
|
|
} else {
|
|
elemString +=
|
|
PlatformKeys.GetStringFromName("VK_ALT") +
|
|
PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
|
|
}
|
|
}
|
|
if (elemMod.match("os")) {
|
|
elemString +=
|
|
PlatformKeys.GetStringFromName("VK_WIN") +
|
|
PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
|
|
}
|
|
if (elemMod.match("shift")) {
|
|
elemString +=
|
|
PlatformKeys.GetStringFromName("VK_SHIFT") +
|
|
PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
|
|
}
|
|
if (elemMod.match("alt")) {
|
|
elemString +=
|
|
PlatformKeys.GetStringFromName("VK_ALT") +
|
|
PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
|
|
}
|
|
if (elemMod.match("ctrl") || elemMod.match("control")) {
|
|
elemString +=
|
|
PlatformKeys.GetStringFromName("VK_CONTROL") +
|
|
PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
|
|
}
|
|
if (elemMod.match("meta")) {
|
|
elemString +=
|
|
PlatformKeys.GetStringFromName("VK_META") +
|
|
PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
|
|
}
|
|
|
|
if (haveCloverLeaf) {
|
|
elemString +=
|
|
PlatformKeys.GetStringFromName("VK_META") +
|
|
PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
|
|
}
|
|
|
|
return elemString;
|
|
},
|
|
|
|
getKeyString(keyCode, keyAttribute) {
|
|
let key;
|
|
if (keyCode) {
|
|
keyCode = keyCode.toUpperCase();
|
|
try {
|
|
let bundle = keyCode == "VK_RETURN" ? PlatformKeys : Keys;
|
|
// Some keys might not exist in the locale file, which will throw.
|
|
key = bundle.GetStringFromName(keyCode);
|
|
} catch (ex) {
|
|
Cu.reportError("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.
|
|
*/
|
|
getSystemActionForEvent(event, { rtl } = {}) {
|
|
switch (event.keyCode) {
|
|
case event.DOM_VK_TAB:
|
|
if (event.ctrlKey && !event.altKey && !event.metaKey) {
|
|
return ShortcutUtils.CYCLE_TABS;
|
|
}
|
|
break;
|
|
case event.DOM_VK_PAGE_UP:
|
|
if (
|
|
event.ctrlKey &&
|
|
!event.shiftKey &&
|
|
!event.altKey &&
|
|
!event.metaKey
|
|
) {
|
|
return ShortcutUtils.PREVIOUS_TAB;
|
|
}
|
|
if (
|
|
event.ctrlKey &&
|
|
event.shiftKey &&
|
|
!event.altKey &&
|
|
!event.metaKey
|
|
) {
|
|
return ShortcutUtils.MOVE_TAB_BACKWARD;
|
|
}
|
|
break;
|
|
case event.DOM_VK_PAGE_DOWN:
|
|
if (
|
|
event.ctrlKey &&
|
|
!event.shiftKey &&
|
|
!event.altKey &&
|
|
!event.metaKey
|
|
) {
|
|
return ShortcutUtils.NEXT_TAB;
|
|
}
|
|
if (
|
|
event.ctrlKey &&
|
|
event.shiftKey &&
|
|
!event.altKey &&
|
|
!event.metaKey
|
|
) {
|
|
return ShortcutUtils.MOVE_TAB_FORWARD;
|
|
}
|
|
break;
|
|
case event.DOM_VK_LEFT:
|
|
if (
|
|
event.metaKey &&
|
|
event.altKey &&
|
|
!event.shiftKey &&
|
|
!event.ctrlKey
|
|
) {
|
|
return ShortcutUtils.PREVIOUS_TAB;
|
|
}
|
|
break;
|
|
case event.DOM_VK_RIGHT:
|
|
if (
|
|
event.metaKey &&
|
|
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.metaKey &&
|
|
event.keyCode == KeyEvent.DOM_VK_F4
|
|
) {
|
|
return ShortcutUtils.CLOSE_TAB;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
},
|
|
};
|
|
|
|
Object.freeze(ShortcutUtils);
|