Bug 1608150 [wpt PR 21112] - Add missing space in `./wpt lint` command line docs...
[gecko.git] / toolkit / modules / ShortcutUtils.jsm
blob549075be0149753c40cbc227848daf2d91bb178c
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/. */
5 "use strict";
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",
16 });
18 XPCOMUtils.defineLazyGetter(this, "PlatformKeys", function() {
19   return Services.strings.createBundle(
20     "chrome://global-platform/locale/platformKeys.properties"
21   );
22 });
24 XPCOMUtils.defineLazyGetter(this, "Keys", function() {
25   return Services.strings.createBundle(
26     "chrome://global/locale/keys.properties"
27   );
28 });
30 var ShortcutUtils = {
31   IS_VALID: "valid",
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",
43   NEXT_TAB: "NEXT_TAB",
45   /**
46    * Prettifies the modifier keys for an element.
47    *
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)
52    * @return string
53    *         A prettified and properly separated modifier keys string.
54    */
55   prettifyShortcut(aElemKey, aNoCloverLeaf) {
56     let elemString = this.getModifierString(
57       aElemKey.getAttribute("modifiers"),
58       aNoCloverLeaf
59     );
60     let key = this.getKeyString(
61       aElemKey.getAttribute("keycode"),
62       aElemKey.getAttribute("key")
63     );
64     return elemString + key;
65   },
67   getModifierString(elemMod, aNoCloverLeaf) {
68     let elemString = "";
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.
75         if (aNoCloverLeaf) {
76           elemString += "Cmd-";
77         } else {
78           haveCloverLeaf = true;
79         }
80       } else {
81         elemString +=
82           PlatformKeys.GetStringFromName("VK_CONTROL") +
83           PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
84       }
85     }
86     if (elemMod.match("access")) {
87       if (Services.appinfo.OS == "Darwin") {
88         elemString +=
89           PlatformKeys.GetStringFromName("VK_CONTROL") +
90           PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
91       } else {
92         elemString +=
93           PlatformKeys.GetStringFromName("VK_ALT") +
94           PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
95       }
96     }
97     if (elemMod.match("os")) {
98       elemString +=
99         PlatformKeys.GetStringFromName("VK_WIN") +
100         PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
101     }
102     if (elemMod.match("shift")) {
103       elemString +=
104         PlatformKeys.GetStringFromName("VK_SHIFT") +
105         PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
106     }
107     if (elemMod.match("alt")) {
108       elemString +=
109         PlatformKeys.GetStringFromName("VK_ALT") +
110         PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
111     }
112     if (elemMod.match("ctrl") || elemMod.match("control")) {
113       elemString +=
114         PlatformKeys.GetStringFromName("VK_CONTROL") +
115         PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
116     }
117     if (elemMod.match("meta")) {
118       elemString +=
119         PlatformKeys.GetStringFromName("VK_META") +
120         PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
121     }
123     if (haveCloverLeaf) {
124       elemString +=
125         PlatformKeys.GetStringFromName("VK_META") +
126         PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
127     }
129     return elemString;
130   },
132   getKeyString(keyCode, keyAttribute) {
133     let key;
134     if (keyCode) {
135       keyCode = keyCode.toUpperCase();
136       try {
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);
140       } catch (ex) {
141         Cu.reportError("Error finding " + keyCode + ": " + ex);
142         key = keyCode.replace(/^VK_/, "");
143       }
144     } else {
145       key = keyAttribute.toUpperCase();
146     }
148     return key;
149   },
151   getKeyAttribute(chromeKey) {
152     if (/^[A-Z]$/.test(chromeKey)) {
153       // We use the key attribute for single characters.
154       return ["key", chromeKey];
155     }
156     return ["keycode", this.getKeycodeAttribute(chromeKey)];
157   },
159   /**
160    * Determines the corresponding XUL keycode from the given chrome key.
161    *
162    * For example:
163    *
164    *    input     |  output
165    *    ---------------------------------------
166    *    "PageUp"  |  "VK_PAGE_UP"
167    *    "Delete"  |  "VK_DELETE"
168    *
169    * @param {string} chromeKey The chrome key (e.g. "PageUp", "Space", ...)
170    * @returns {string} The constructed value for the Key's 'keycode' attribute.
171    */
172   getKeycodeAttribute(chromeKey) {
173     if (/^[0-9]/.test(chromeKey)) {
174       return `VK_${chromeKey}`;
175     }
176     return `VK${chromeKey.replace(/([A-Z])/g, "_$&").toUpperCase()}`;
177   },
179   findShortcut(aElemCommand) {
180     let document = aElemCommand.ownerDocument;
181     return document.querySelector(
182       'key[command="' + aElemCommand.getAttribute("id") + '"]'
183     );
184   },
186   chromeModifierKeyMap: {
187     Alt: "alt",
188     Command: "accel",
189     Ctrl: "accel",
190     MacCtrl: "control",
191     Shift: "shift",
192   },
194   /**
195    * Determines the corresponding XUL modifiers from the chrome modifiers.
196    *
197    * For example:
198    *
199    *    input             |   output
200    *    ---------------------------------------
201    *    ["Ctrl", "Shift"] |   "accel,shift"
202    *    ["MacCtrl"]       |   "control"
203    *
204    * @param {Array} chromeModifiers The array of chrome modifiers.
205    * @returns {string} The constructed value for the Key's 'modifiers' attribute.
206    */
207   getModifiersAttribute(chromeModifiers) {
208     return Array.from(chromeModifiers, modifier => {
209       return ShortcutUtils.chromeModifierKeyMap[modifier];
210     })
211       .sort()
212       .join(",");
213   },
215   /**
216    * Validate if a shortcut string is valid and return an error code if it
217    * isn't valid.
218    *
219    * For example:
220    *
221    *    input            |   output
222    *    ---------------------------------------
223    *    "Ctrl+Shift+A"   |   IS_VALID
224    *    "Shift+F"        |   MODIFIER_REQUIRED
225    *    "Command+>"      |   INVALID_KEY
226    *
227    * @param {string} string The shortcut string.
228    * @returns {string} The code for the validation result.
229    */
230   validate(string) {
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;
238     }
240     let modifiers = string.split("+").map(s => s.trim());
241     let key = modifiers.pop();
243     let chromeModifiers = modifiers.map(
244       m => ShortcutUtils.chromeModifierKeyMap[m]
245     );
246     // If the modifier wasn't found it will be undefined.
247     if (chromeModifiers.some(modifier => !modifier)) {
248       return this.INVALID_MODIFIER;
249     }
251     switch (modifiers.length) {
252       case 0:
253         // A lack of modifiers is only allowed with function keys.
254         if (!FUNCTION_KEYS.test(key)) {
255           return this.MODIFIER_REQUIRED;
256         }
257         break;
258       case 1:
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;
262         }
263         break;
264       case 2:
265         if (chromeModifiers[0] == chromeModifiers[1]) {
266           return this.DUPLICATE_MODIFIER;
267         }
268         break;
269       default:
270         return this.INVALID_COMBINATION;
271     }
273     if (!BASIC_KEYS.test(key) && !FUNCTION_KEYS.test(key)) {
274       return this.INVALID_KEY;
275     }
277     return this.IS_VALID;
278   },
280   /**
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.
283    *
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.
287    */
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}"]`;
297     }
299     let keyEl = win.document.querySelector(
300       [
301         `${baseSelector}[key="${chromeKey}"]`,
302         `${baseSelector}[key="${chromeKey.toLowerCase()}"]`,
303         `${baseSelector}[keycode="${keycode}"]`,
304       ].join(",")
305     );
306     return keyEl && !keyEl.closest("keyset").id.startsWith("ext-keyset-id");
307   },
309   /**
310    * Determine what action a KeyboardEvent should perform, if any.
311    *
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.
314    */
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;
320         }
321         break;
322       case event.DOM_VK_PAGE_UP:
323         if (
324           event.ctrlKey &&
325           !event.shiftKey &&
326           !event.altKey &&
327           !event.metaKey
328         ) {
329           return ShortcutUtils.PREVIOUS_TAB;
330         }
331         if (
332           event.ctrlKey &&
333           event.shiftKey &&
334           !event.altKey &&
335           !event.metaKey
336         ) {
337           return ShortcutUtils.MOVE_TAB_BACKWARD;
338         }
339         break;
340       case event.DOM_VK_PAGE_DOWN:
341         if (
342           event.ctrlKey &&
343           !event.shiftKey &&
344           !event.altKey &&
345           !event.metaKey
346         ) {
347           return ShortcutUtils.NEXT_TAB;
348         }
349         if (
350           event.ctrlKey &&
351           event.shiftKey &&
352           !event.altKey &&
353           !event.metaKey
354         ) {
355           return ShortcutUtils.MOVE_TAB_FORWARD;
356         }
357         break;
358       case event.DOM_VK_LEFT:
359         if (
360           event.metaKey &&
361           event.altKey &&
362           !event.shiftKey &&
363           !event.ctrlKey
364         ) {
365           return ShortcutUtils.PREVIOUS_TAB;
366         }
367         break;
368       case event.DOM_VK_RIGHT:
369         if (
370           event.metaKey &&
371           event.altKey &&
372           !event.shiftKey &&
373           !event.ctrlKey
374         ) {
375           return ShortcutUtils.NEXT_TAB;
376         }
377         break;
378     }
380     if (AppConstants.platform == "macosx") {
381       if (!event.altKey && event.metaKey) {
382         switch (event.charCode) {
383           case "}".charCodeAt(0):
384             if (rtl) {
385               return ShortcutUtils.PREVIOUS_TAB;
386             }
387             return ShortcutUtils.NEXT_TAB;
388           case "{".charCodeAt(0):
389             if (rtl) {
390               return ShortcutUtils.NEXT_TAB;
391             }
392             return ShortcutUtils.PREVIOUS_TAB;
393         }
394       }
395     }
396     // Not on Mac from now on.
397     if (AppConstants.platform != "macosx") {
398       if (
399         event.ctrlKey &&
400         !event.shiftKey &&
401         !event.metaKey &&
402         event.keyCode == KeyEvent.DOM_VK_F4
403       ) {
404         return ShortcutUtils.CLOSE_TAB;
405       }
406     }
408     return null;
409   },
412 Object.freeze(ShortcutUtils);