1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 var EXPORTED_SYMBOLS = ["ShortcutUtils"];
9 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
10 const { XPCOMUtils } = ChromeUtils.import(
11 "resource://gre/modules/XPCOMUtils.jsm"
14 XPCOMUtils.defineLazyModuleGetters(this, {
15 AppConstants: "resource://gre/modules/AppConstants.jsm",
18 XPCOMUtils.defineLazyGetter(this, "PlatformKeys", function() {
19 return Services.strings.createBundle(
20 "chrome://global-platform/locale/platformKeys.properties"
24 XPCOMUtils.defineLazyGetter(this, "Keys", function() {
25 return Services.strings.createBundle(
26 "chrome://global/locale/keys.properties"
32 INVALID_KEY: "invalid_key",
33 INVALID_MODIFIER: "invalid_modifier",
34 INVALID_COMBINATION: "invalid_combination",
35 DUPLICATE_MODIFIER: "duplicate_modifier",
36 MODIFIER_REQUIRED: "modifier_required",
38 MOVE_TAB_FORWARD: "MOVE_TAB_FORWARD",
39 MOVE_TAB_BACKWARD: "MOVE_TAB_BACKWARD",
40 CLOSE_TAB: "CLOSE_TAB",
41 CYCLE_TABS: "CYCLE_TABS",
42 PREVIOUS_TAB: "PREVIOUS_TAB",
46 * Prettifies the modifier keys for an element.
48 * @param Node aElemKey
49 * The key element to get the modifiers from.
50 * @param boolean aNoCloverLeaf
51 * Pass true to use a descriptive string instead of the cloverleaf symbol. (OS X only)
53 * A prettified and properly separated modifier keys string.
55 prettifyShortcut(aElemKey, aNoCloverLeaf) {
56 let elemString = this.getModifierString(
57 aElemKey.getAttribute("modifiers"),
60 let key = this.getKeyString(
61 aElemKey.getAttribute("keycode"),
62 aElemKey.getAttribute("key")
64 return elemString + key;
67 getModifierString(elemMod, aNoCloverLeaf) {
69 let haveCloverLeaf = false;
71 if (elemMod.match("accel")) {
72 if (Services.appinfo.OS == "Darwin") {
73 // XXX bug 779642 Use "Cmd-" literal vs. cloverleaf meta-key until
74 // Orion adds variable height lines.
78 haveCloverLeaf = true;
82 PlatformKeys.GetStringFromName("VK_CONTROL") +
83 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
86 if (elemMod.match("access")) {
87 if (Services.appinfo.OS == "Darwin") {
89 PlatformKeys.GetStringFromName("VK_CONTROL") +
90 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
93 PlatformKeys.GetStringFromName("VK_ALT") +
94 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
97 if (elemMod.match("os")) {
99 PlatformKeys.GetStringFromName("VK_WIN") +
100 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
102 if (elemMod.match("shift")) {
104 PlatformKeys.GetStringFromName("VK_SHIFT") +
105 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
107 if (elemMod.match("alt")) {
109 PlatformKeys.GetStringFromName("VK_ALT") +
110 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
112 if (elemMod.match("ctrl") || elemMod.match("control")) {
114 PlatformKeys.GetStringFromName("VK_CONTROL") +
115 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
117 if (elemMod.match("meta")) {
119 PlatformKeys.GetStringFromName("VK_META") +
120 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
123 if (haveCloverLeaf) {
125 PlatformKeys.GetStringFromName("VK_META") +
126 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
132 getKeyString(keyCode, keyAttribute) {
135 keyCode = keyCode.toUpperCase();
137 let bundle = keyCode == "VK_RETURN" ? PlatformKeys : Keys;
138 // Some keys might not exist in the locale file, which will throw.
139 key = bundle.GetStringFromName(keyCode);
141 Cu.reportError("Error finding " + keyCode + ": " + ex);
142 key = keyCode.replace(/^VK_/, "");
145 key = keyAttribute.toUpperCase();
151 getKeyAttribute(chromeKey) {
152 if (/^[A-Z]$/.test(chromeKey)) {
153 // We use the key attribute for single characters.
154 return ["key", chromeKey];
156 return ["keycode", this.getKeycodeAttribute(chromeKey)];
160 * Determines the corresponding XUL keycode from the given chrome key.
165 * ---------------------------------------
166 * "PageUp" | "VK_PAGE_UP"
167 * "Delete" | "VK_DELETE"
169 * @param {string} chromeKey The chrome key (e.g. "PageUp", "Space", ...)
170 * @returns {string} The constructed value for the Key's 'keycode' attribute.
172 getKeycodeAttribute(chromeKey) {
173 if (/^[0-9]/.test(chromeKey)) {
174 return `VK_${chromeKey}`;
176 return `VK${chromeKey.replace(/([A-Z])/g, "_$&").toUpperCase()}`;
179 findShortcut(aElemCommand) {
180 let document = aElemCommand.ownerDocument;
181 return document.querySelector(
182 'key[command="' + aElemCommand.getAttribute("id") + '"]'
186 chromeModifierKeyMap: {
195 * Determines the corresponding XUL modifiers from the chrome modifiers.
200 * ---------------------------------------
201 * ["Ctrl", "Shift"] | "accel,shift"
202 * ["MacCtrl"] | "control"
204 * @param {Array} chromeModifiers The array of chrome modifiers.
205 * @returns {string} The constructed value for the Key's 'modifiers' attribute.
207 getModifiersAttribute(chromeModifiers) {
208 return Array.from(chromeModifiers, modifier => {
209 return ShortcutUtils.chromeModifierKeyMap[modifier];
216 * Validate if a shortcut string is valid and return an error code if it
222 * ---------------------------------------
223 * "Ctrl+Shift+A" | IS_VALID
224 * "Shift+F" | MODIFIER_REQUIRED
225 * "Command+>" | INVALID_KEY
227 * @param {string} string The shortcut string.
228 * @returns {string} The code for the validation result.
231 // A valid shortcut key for a webextension manifest
232 const MEDIA_KEYS = /^(MediaNextTrack|MediaPlayPause|MediaPrevTrack|MediaStop)$/;
233 const BASIC_KEYS = /^([A-Z0-9]|Comma|Period|Home|End|PageUp|PageDown|Space|Insert|Delete|Up|Down|Left|Right)$/;
234 const FUNCTION_KEYS = /^(F[1-9]|F1[0-2])$/;
236 if (MEDIA_KEYS.test(string.trim())) {
237 return this.IS_VALID;
240 let modifiers = string.split("+").map(s => s.trim());
241 let key = modifiers.pop();
243 let chromeModifiers = modifiers.map(
244 m => ShortcutUtils.chromeModifierKeyMap[m]
246 // If the modifier wasn't found it will be undefined.
247 if (chromeModifiers.some(modifier => !modifier)) {
248 return this.INVALID_MODIFIER;
251 switch (modifiers.length) {
253 // A lack of modifiers is only allowed with function keys.
254 if (!FUNCTION_KEYS.test(key)) {
255 return this.MODIFIER_REQUIRED;
259 // Shift is only allowed on its own with function keys.
260 if (chromeModifiers[0] == "shift" && !FUNCTION_KEYS.test(key)) {
261 return this.MODIFIER_REQUIRED;
265 if (chromeModifiers[0] == chromeModifiers[1]) {
266 return this.DUPLICATE_MODIFIER;
270 return this.INVALID_COMBINATION;
273 if (!BASIC_KEYS.test(key) && !FUNCTION_KEYS.test(key)) {
274 return this.INVALID_KEY;
277 return this.IS_VALID;
281 * Attempt to find a key for a given shortcut string, such as
282 * "Ctrl+Shift+A" and determine if it is a system shortcut.
284 * @param {Object} win The window to look for key elements in.
285 * @param {string} value The shortcut string.
286 * @returns {boolean} Whether a system shortcut was found or not.
288 isSystem(win, value) {
289 let modifiers = value.split("+");
290 let chromeKey = modifiers.pop();
291 let modifiersString = this.getModifiersAttribute(modifiers);
292 let keycode = this.getKeycodeAttribute(chromeKey);
294 let baseSelector = "key";
295 if (modifiers.length) {
296 baseSelector += `[modifiers="${modifiersString}"]`;
299 let keyEl = win.document.querySelector(
301 `${baseSelector}[key="${chromeKey}"]`,
302 `${baseSelector}[key="${chromeKey.toLowerCase()}"]`,
303 `${baseSelector}[keycode="${keycode}"]`,
306 return keyEl && !keyEl.closest("keyset").id.startsWith("ext-keyset-id");
310 * Determine what action a KeyboardEvent should perform, if any.
312 * @param {KeyboardEvent} event The event to check for a related system action.
313 * @returns {string} A string identifying the action, or null if no action is found.
315 getSystemActionForEvent(event, { rtl } = {}) {
316 switch (event.keyCode) {
317 case event.DOM_VK_TAB:
318 if (event.ctrlKey && !event.altKey && !event.metaKey) {
319 return ShortcutUtils.CYCLE_TABS;
322 case event.DOM_VK_PAGE_UP:
329 return ShortcutUtils.PREVIOUS_TAB;
337 return ShortcutUtils.MOVE_TAB_BACKWARD;
340 case event.DOM_VK_PAGE_DOWN:
347 return ShortcutUtils.NEXT_TAB;
355 return ShortcutUtils.MOVE_TAB_FORWARD;
358 case event.DOM_VK_LEFT:
365 return ShortcutUtils.PREVIOUS_TAB;
368 case event.DOM_VK_RIGHT:
375 return ShortcutUtils.NEXT_TAB;
380 if (AppConstants.platform == "macosx") {
381 if (!event.altKey && event.metaKey) {
382 switch (event.charCode) {
383 case "}".charCodeAt(0):
385 return ShortcutUtils.PREVIOUS_TAB;
387 return ShortcutUtils.NEXT_TAB;
388 case "{".charCodeAt(0):
390 return ShortcutUtils.NEXT_TAB;
392 return ShortcutUtils.PREVIOUS_TAB;
396 // Not on Mac from now on.
397 if (AppConstants.platform != "macosx") {
402 event.keyCode == KeyEvent.DOM_VK_F4
404 return ShortcutUtils.CLOSE_TAB;
412 Object.freeze(ShortcutUtils);