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/. */ 4 5"use strict"; 6 7var EXPORTED_SYMBOLS = ["ShortcutUtils"]; 8 9const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 10const { XPCOMUtils } = ChromeUtils.import( 11 "resource://gre/modules/XPCOMUtils.jsm" 12); 13 14XPCOMUtils.defineLazyModuleGetters(this, { 15 AppConstants: "resource://gre/modules/AppConstants.jsm", 16}); 17 18XPCOMUtils.defineLazyGetter(this, "PlatformKeys", function() { 19 return Services.strings.createBundle( 20 "chrome://global-platform/locale/platformKeys.properties" 21 ); 22}); 23 24XPCOMUtils.defineLazyGetter(this, "Keys", function() { 25 return Services.strings.createBundle( 26 "chrome://global/locale/keys.properties" 27 ); 28}); 29 30var 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", 37 38 CLOSE_TAB: "CLOSE_TAB", 39 CYCLE_TABS: "CYCLE_TABS", 40 TOGGLE_CARET_BROWSING: "TOGGLE_CARET_BROWSING", 41 MOVE_TAB_BACKWARD: "MOVE_TAB_BACKWARD", 42 MOVE_TAB_FORWARD: "MOVE_TAB_FORWARD", 43 NEXT_TAB: "NEXT_TAB", 44 PREVIOUS_TAB: "PREVIOUS_TAB", 45 46 /** 47 * Prettifies the modifier keys for an element. 48 * 49 * @param Node aElemKey 50 * The key element to get the modifiers from. 51 * @return string 52 * A prettified and properly separated modifier keys string. 53 */ 54 prettifyShortcut(aElemKey) { 55 let elemString = this.getModifierString(aElemKey.getAttribute("modifiers")); 56 let key = this.getKeyString( 57 aElemKey.getAttribute("keycode"), 58 aElemKey.getAttribute("key") 59 ); 60 return elemString + key; 61 }, 62 63 getModifierString(elemMod) { 64 let elemString = ""; 65 let haveCloverLeaf = false; 66 67 if (elemMod.match("accel")) { 68 if (Services.appinfo.OS == "Darwin") { 69 haveCloverLeaf = true; 70 } else { 71 elemString += 72 PlatformKeys.GetStringFromName("VK_CONTROL") + 73 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR"); 74 } 75 } 76 if (elemMod.match("access")) { 77 if (Services.appinfo.OS == "Darwin") { 78 elemString += 79 PlatformKeys.GetStringFromName("VK_CONTROL") + 80 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR"); 81 } else { 82 elemString += 83 PlatformKeys.GetStringFromName("VK_ALT") + 84 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR"); 85 } 86 } 87 if (elemMod.match("os")) { 88 elemString += 89 PlatformKeys.GetStringFromName("VK_WIN") + 90 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR"); 91 } 92 if (elemMod.match("shift")) { 93 elemString += 94 PlatformKeys.GetStringFromName("VK_SHIFT") + 95 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR"); 96 } 97 if (elemMod.match("alt")) { 98 elemString += 99 PlatformKeys.GetStringFromName("VK_ALT") + 100 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR"); 101 } 102 if (elemMod.match("ctrl") || elemMod.match("control")) { 103 elemString += 104 PlatformKeys.GetStringFromName("VK_CONTROL") + 105 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR"); 106 } 107 if (elemMod.match("meta")) { 108 elemString += 109 PlatformKeys.GetStringFromName("VK_META") + 110 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR"); 111 } 112 113 if (haveCloverLeaf) { 114 elemString += 115 PlatformKeys.GetStringFromName("VK_META") + 116 PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR"); 117 } 118 119 return elemString; 120 }, 121 122 getKeyString(keyCode, keyAttribute) { 123 let key; 124 if (keyCode) { 125 keyCode = keyCode.toUpperCase(); 126 if (AppConstants.platform == "macosx") { 127 // Return fancy Unicode symbols for some keys. 128 switch (keyCode) { 129 case "VK_LEFT": 130 return "\u2190"; // U+2190 LEFTWARDS ARROW 131 case "VK_RIGHT": 132 return "\u2192"; // U+2192 RIGHTWARDS ARROW 133 } 134 } 135 try { 136 let bundle = keyCode == "VK_RETURN" ? PlatformKeys : Keys; 137 // Some keys might not exist in the locale file, which will throw. 138 key = bundle.GetStringFromName(keyCode); 139 } catch (ex) { 140 Cu.reportError("Error finding " + keyCode + ": " + ex); 141 key = keyCode.replace(/^VK_/, ""); 142 } 143 } else { 144 key = keyAttribute.toUpperCase(); 145 } 146 147 return key; 148 }, 149 150 getKeyAttribute(chromeKey) { 151 if (/^[A-Z]$/.test(chromeKey)) { 152 // We use the key attribute for single characters. 153 return ["key", chromeKey]; 154 } 155 return ["keycode", this.getKeycodeAttribute(chromeKey)]; 156 }, 157 158 /** 159 * Determines the corresponding XUL keycode from the given chrome key. 160 * 161 * For example: 162 * 163 * input | output 164 * --------------------------------------- 165 * "PageUp" | "VK_PAGE_UP" 166 * "Delete" | "VK_DELETE" 167 * 168 * @param {string} chromeKey The chrome key (e.g. "PageUp", "Space", ...) 169 * @returns {string} The constructed value for the Key's 'keycode' attribute. 170 */ 171 getKeycodeAttribute(chromeKey) { 172 if (/^[0-9]/.test(chromeKey)) { 173 return `VK_${chromeKey}`; 174 } 175 return `VK${chromeKey.replace(/([A-Z])/g, "_$&").toUpperCase()}`; 176 }, 177 178 findShortcut(aElemCommand) { 179 let document = aElemCommand.ownerDocument; 180 return document.querySelector( 181 'key[command="' + aElemCommand.getAttribute("id") + '"]' 182 ); 183 }, 184 185 chromeModifierKeyMap: { 186 Alt: "alt", 187 Command: "accel", 188 Ctrl: "accel", 189 MacCtrl: "control", 190 Shift: "shift", 191 }, 192 193 /** 194 * Determines the corresponding XUL modifiers from the chrome modifiers. 195 * 196 * For example: 197 * 198 * input | output 199 * --------------------------------------- 200 * ["Ctrl", "Shift"] | "accel,shift" 201 * ["MacCtrl"] | "control" 202 * 203 * @param {Array} chromeModifiers The array of chrome modifiers. 204 * @returns {string} The constructed value for the Key's 'modifiers' attribute. 205 */ 206 getModifiersAttribute(chromeModifiers) { 207 return Array.from(chromeModifiers, modifier => { 208 return ShortcutUtils.chromeModifierKeyMap[modifier]; 209 }) 210 .sort() 211 .join(","); 212 }, 213 214 /** 215 * Validate if a shortcut string is valid and return an error code if it 216 * isn't valid. 217 * 218 * For example: 219 * 220 * input | output 221 * --------------------------------------- 222 * "Ctrl+Shift+A" | IS_VALID 223 * "Shift+F" | MODIFIER_REQUIRED 224 * "Command+>" | INVALID_KEY 225 * 226 * @param {string} string The shortcut string. 227 * @returns {string} The code for the validation result. 228 */ 229 validate(string) { 230 // A valid shortcut key for a webextension manifest 231 const MEDIA_KEYS = /^(MediaNextTrack|MediaPlayPause|MediaPrevTrack|MediaStop)$/; 232 const BASIC_KEYS = /^([A-Z0-9]|Comma|Period|Home|End|PageUp|PageDown|Space|Insert|Delete|Up|Down|Left|Right)$/; 233 const FUNCTION_KEYS = /^(F[1-9]|F1[0-2])$/; 234 235 if (MEDIA_KEYS.test(string.trim())) { 236 return this.IS_VALID; 237 } 238 239 let modifiers = string.split("+").map(s => s.trim()); 240 let key = modifiers.pop(); 241 242 let chromeModifiers = modifiers.map( 243 m => ShortcutUtils.chromeModifierKeyMap[m] 244 ); 245 // If the modifier wasn't found it will be undefined. 246 if (chromeModifiers.some(modifier => !modifier)) { 247 return this.INVALID_MODIFIER; 248 } 249 250 switch (modifiers.length) { 251 case 0: 252 // A lack of modifiers is only allowed with function keys. 253 if (!FUNCTION_KEYS.test(key)) { 254 return this.MODIFIER_REQUIRED; 255 } 256 break; 257 case 1: 258 // Shift is only allowed on its own with function keys. 259 if (chromeModifiers[0] == "shift" && !FUNCTION_KEYS.test(key)) { 260 return this.MODIFIER_REQUIRED; 261 } 262 break; 263 case 2: 264 if (chromeModifiers[0] == chromeModifiers[1]) { 265 return this.DUPLICATE_MODIFIER; 266 } 267 break; 268 default: 269 return this.INVALID_COMBINATION; 270 } 271 272 if (!BASIC_KEYS.test(key) && !FUNCTION_KEYS.test(key)) { 273 return this.INVALID_KEY; 274 } 275 276 return this.IS_VALID; 277 }, 278 279 /** 280 * Attempt to find a key for a given shortcut string, such as 281 * "Ctrl+Shift+A" and determine if it is a system shortcut. 282 * 283 * @param {Object} win The window to look for key elements in. 284 * @param {string} value The shortcut string. 285 * @returns {boolean} Whether a system shortcut was found or not. 286 */ 287 isSystem(win, value) { 288 let modifiers = value.split("+"); 289 let chromeKey = modifiers.pop(); 290 let modifiersString = this.getModifiersAttribute(modifiers); 291 let keycode = this.getKeycodeAttribute(chromeKey); 292 293 let baseSelector = "key"; 294 if (modifiers.length) { 295 baseSelector += `[modifiers="${modifiersString}"]`; 296 } 297 298 let keyEl = win.document.querySelector( 299 [ 300 `${baseSelector}[key="${chromeKey}"]`, 301 `${baseSelector}[key="${chromeKey.toLowerCase()}"]`, 302 `${baseSelector}[keycode="${keycode}"]`, 303 ].join(",") 304 ); 305 return keyEl && !keyEl.closest("keyset").id.startsWith("ext-keyset-id"); 306 }, 307 308 /** 309 * Determine what action a KeyboardEvent should perform, if any. 310 * 311 * @param {KeyboardEvent} event The event to check for a related system action. 312 * @returns {string} A string identifying the action, or null if no action is found. 313 */ 314 // eslint-disable-next-line complexity 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_F7: 323 // shift + F7 is the default DevTools shortcut for the Style Editor. 324 if (!event.shiftKey) { 325 return ShortcutUtils.TOGGLE_CARET_BROWSING; 326 } 327 break; 328 case event.DOM_VK_PAGE_UP: 329 if ( 330 event.ctrlKey && 331 !event.shiftKey && 332 !event.altKey && 333 !event.metaKey 334 ) { 335 return ShortcutUtils.PREVIOUS_TAB; 336 } 337 if ( 338 event.ctrlKey && 339 event.shiftKey && 340 !event.altKey && 341 !event.metaKey 342 ) { 343 return ShortcutUtils.MOVE_TAB_BACKWARD; 344 } 345 break; 346 case event.DOM_VK_PAGE_DOWN: 347 if ( 348 event.ctrlKey && 349 !event.shiftKey && 350 !event.altKey && 351 !event.metaKey 352 ) { 353 return ShortcutUtils.NEXT_TAB; 354 } 355 if ( 356 event.ctrlKey && 357 event.shiftKey && 358 !event.altKey && 359 !event.metaKey 360 ) { 361 return ShortcutUtils.MOVE_TAB_FORWARD; 362 } 363 break; 364 case event.DOM_VK_LEFT: 365 if ( 366 event.metaKey && 367 event.altKey && 368 !event.shiftKey && 369 !event.ctrlKey 370 ) { 371 return ShortcutUtils.PREVIOUS_TAB; 372 } 373 break; 374 case event.DOM_VK_RIGHT: 375 if ( 376 event.metaKey && 377 event.altKey && 378 !event.shiftKey && 379 !event.ctrlKey 380 ) { 381 return ShortcutUtils.NEXT_TAB; 382 } 383 break; 384 } 385 386 if (AppConstants.platform == "macosx") { 387 if (!event.altKey && event.metaKey) { 388 switch (event.charCode) { 389 case "}".charCodeAt(0): 390 if (rtl) { 391 return ShortcutUtils.PREVIOUS_TAB; 392 } 393 return ShortcutUtils.NEXT_TAB; 394 case "{".charCodeAt(0): 395 if (rtl) { 396 return ShortcutUtils.NEXT_TAB; 397 } 398 return ShortcutUtils.PREVIOUS_TAB; 399 } 400 } 401 } 402 // Not on Mac from now on. 403 if (AppConstants.platform != "macosx") { 404 if ( 405 event.ctrlKey && 406 !event.shiftKey && 407 !event.metaKey && 408 event.keyCode == KeyEvent.DOM_VK_F4 409 ) { 410 return ShortcutUtils.CLOSE_TAB; 411 } 412 } 413 414 return null; 415 }, 416}; 417 418Object.freeze(ShortcutUtils); 419