| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview A collection of JavaScript utilities used to simplify working |
| * with keyboard events. |
| */ |
| import {AsyncUtil} from '/common/async_util.js'; |
| import {KeyCode} from '/common/key_code.js'; |
| |
| import {KeySequence} from './key_sequence.js'; |
| import {Msgs} from './msgs.js'; |
| |
| export namespace KeyUtil { |
| /** |
| * Convert a key event into a Key Sequence representation. |
| * |
| * @param keyEvent The keyEvent to convert. |
| * @return A key sequence representation of the key event. |
| */ |
| export function keyEventToKeySequence(keyEvent: KeyboardEvent): KeySequence { |
| if (KeyUtil.prevKeySequence && |
| (KeyUtil.maxSeqLength === KeyUtil.prevKeySequence.length())) { |
| // Reset the sequence buffer if max sequence length is reached. |
| KeyUtil.sequencing = false; |
| KeyUtil.prevKeySequence = null; |
| } |
| const hasKeyPrefix = |
| (keyEvent as unknown as {keyPrefix: boolean}).keyPrefix; |
| const stickyMode = |
| (keyEvent as unknown as {stickyMode: boolean}).stickyMode; |
| // Either we are in the middle of a key sequence (N > H), or the key prefix |
| // was pressed before (Ctrl+Z), or sticky mode is enabled |
| const keyIsPrefixed = KeyUtil.sequencing || hasKeyPrefix || stickyMode; |
| |
| // Create key sequence. |
| let keySequence = new KeySequence(keyEvent); |
| |
| // Check if the Cvox key should be considered as pressed because the |
| // modifier key combination is active. |
| const keyWasCvox = keySequence.cvoxModifier; |
| |
| if (keyIsPrefixed || keyWasCvox) { |
| if (!KeyUtil.sequencing && KeyUtil.isSequenceSwitchKeyCode(keySequence)) { |
| // If this is the beginning of a sequence. |
| KeyUtil.sequencing = true; |
| KeyUtil.prevKeySequence = keySequence; |
| return keySequence; |
| } else if (KeyUtil.sequencing) { |
| // TODO(b/314203187): Not nulls asserted, check that this is correct. |
| if (KeyUtil.prevKeySequence!.addKeyEvent(keyEvent)) { |
| keySequence = KeyUtil.prevKeySequence!; |
| KeyUtil.prevKeySequence = null; |
| KeyUtil.sequencing = false; |
| return keySequence; |
| } else { |
| throw 'Think sequencing is enabled, yet KeyUtil.prevKeySequence' + |
| 'already has two key codes (' + KeyUtil.prevKeySequence + ')'; |
| } |
| } |
| } else { |
| KeyUtil.sequencing = false; |
| } |
| |
| // Repeated keys pressed. |
| const currTime = new Date().getTime(); |
| if (KeyUtil.isDoubleTapKey(keySequence) && KeyUtil.prevKeySequence && |
| keySequence.equals(KeyUtil.prevKeySequence)) { |
| const prevTime = KeyUtil.modeKeyPressTime; |
| const delta = currTime - prevTime; |
| if (!keyEvent.repeat && prevTime > 0 && delta < 300) /* Double tap */ { |
| keySequence = KeyUtil.prevKeySequence; |
| keySequence.doubleTap = true; |
| KeyUtil.prevKeySequence = null; |
| KeyUtil.sequencing = false; |
| return keySequence; |
| } |
| // The user double tapped the sticky key but didn't do it within the |
| // required time. It's possible they will try again, so keep track of the |
| // time the sticky key was pressed and keep track of the corresponding |
| // key sequence. |
| } |
| KeyUtil.prevKeySequence = keySequence; |
| KeyUtil.modeKeyPressTime = currTime; |
| return keySequence; |
| } |
| |
| /** |
| * Returns the string representation of the specified key code. |
| * @return A string representation of the key event. |
| */ |
| export function keyCodeToString(keyCode: number): string { |
| if (keyCode === KeyCode.CONTROL) { |
| return 'Ctrl'; |
| } |
| if (KeyCode.name(keyCode)) { |
| return KeyCode.name(keyCode); |
| } |
| |
| // Anything else |
| return '#' + keyCode; |
| } |
| |
| /** |
| * Returns the keycode of a string representation of the specified modifier. |
| * |
| * @param keyString Modifier key. |
| * @return Key code. |
| */ |
| export function modStringToKeyCode(keyString: string): number { |
| switch (keyString) { |
| case 'Ctrl': |
| return KeyCode.CONTROL; |
| case 'Alt': |
| return KeyCode.ALT; |
| case 'Shift': |
| return KeyCode.SHIFT; |
| case 'Cmd': |
| case 'Win': |
| return KeyCode.SEARCH; |
| } |
| return -1; |
| } |
| |
| /** |
| * Returns the key codes of a string representation of the ChromeVox |
| * modifiers. |
| * |
| * @return Array of key codes. |
| */ |
| export function cvoxModKeyCodes(): number[] { |
| const modKeyCombo = KeySequence.modKeyStr.split(/\+/g); |
| const modKeyCodes = |
| modKeyCombo.map(keyString => KeyUtil.modStringToKeyCode(keyString)); |
| return modKeyCodes; |
| } |
| |
| /** |
| * Checks if the specified key code is a key used for switching into a |
| * sequence mode. Sequence switch keys are specified in |
| * KeySequence.sequenceSwitchKeyCodes |
| * |
| * @param rhKeySeq The key sequence to check. |
| * @return true if it is a sequence switch keycode, false otherwise. |
| */ |
| export function isSequenceSwitchKeyCode(rhKeySeq: KeySequence): boolean { |
| for (let i = 0; i < KeySequence.sequenceSwitchKeyCodes.length; i++) { |
| const lhKeySeq = KeySequence.sequenceSwitchKeyCodes[i]; |
| if (lhKeySeq.equals(rhKeySeq)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Get readable string description of the specified keycode. |
| * |
| * @param keyCode The key code. |
| * @return Returns a string description. |
| */ |
| export function getReadableNameForKeyCode(keyCode: number): string { |
| const msg = Msgs.getMsg.bind(Msgs); |
| switch (keyCode) { |
| case KeyCode.BROWSER_BACK: |
| return msg('back_key'); |
| case KeyCode.BROWSER_FORWARD: |
| return msg('forward_key'); |
| case KeyCode.BROWSER_REFRESH: |
| return msg('refresh_key'); |
| case KeyCode.ZOOM: |
| return msg('toggle_full_screen_key'); |
| case KeyCode.MEDIA_LAUNCH_APP1: |
| return msg('window_overview_key'); |
| case KeyCode.BRIGHTNESS_DOWN: |
| return msg('brightness_down_key'); |
| case KeyCode.BRIGHTNESS_UP: |
| return msg('brightness_up_key'); |
| case KeyCode.VOLUME_MUTE: |
| return msg('volume_mute_key'); |
| case KeyCode.VOLUME_DOWN: |
| return msg('volume_down_key'); |
| case KeyCode.VOLUME_UP: |
| return msg('volume_up_key'); |
| case KeyCode.ASSISTANT: |
| return msg('assistant_key'); |
| case KeyCode.MEDIA_PLAY_PAUSE: |
| return msg('media_play_pause'); |
| } |
| return KeyCode.name(keyCode); |
| } |
| |
| /** |
| * Get the platform specific sticky key keycode. |
| * @return The platform specific sticky key keycode. |
| */ |
| export function getStickyKeyCode(): number { |
| return KeyCode.SEARCH; |
| } |
| |
| // TODO (clchen): Refactor this function away since it is no longer used. |
| export function getReadableNameForStr(_keyStr: string): null { |
| return null; |
| } |
| |
| /** |
| * Creates a string representation of a KeySequence. |
| * A KeySequence with a keyCode of 76 ('L') and the control and alt keys down |
| * would return the string 'Ctrl+Alt+L', for example. A key code that doesn't |
| * correspond to a letter or number will typically return a string with a |
| * pound and then its keyCode, like '#39' for Right Arrow. However, |
| * if the opt_readableKeyCode option is specified, the key code will return a |
| * readable string description like 'Right Arrow' instead of '#39'. |
| * |
| * The modifiers always come in this order: |
| * |
| * Ctrl |
| * Alt |
| * Shift |
| * Meta |
| * |
| * @param keySequence The KeySequence object. |
| * @param readableKeyCode Whether or not to return a readable |
| * string description instead of a string with a pound symbol and a keycode. |
| * Default is false. |
| * @param modifiers Restrict printout to only modifiers. Defaults to false. |
| */ |
| export async function keySequenceToString( |
| keySequence: KeySequence, readableKeyCode?: boolean, |
| modifiers?: boolean): Promise<string> { |
| // TODO(rshearer): Move this method and the getReadableNameForKeyCode and |
| // the method to KeySequence after we refactor isModifierActive (when the |
| // modifie key becomes customizable and isn't stored as a string). We can't |
| // do it earlier because isModifierActive uses |
| // KeyUtil.getReadableNameForKeyCode, and I don't want KeySequence to depend |
| // on KeyUtil. |
| let str = ''; |
| |
| const numKeys = keySequence.length(); |
| |
| for (let index = 0; index < numKeys; index++) { |
| if (str !== '' && !modifiers) { |
| str += ', then '; |
| } else if (str !== '') { |
| str += '+'; |
| } |
| |
| // This iterates through the sequence. Either we're on the first key |
| // pressed or the second |
| let tempStr = ''; |
| for (const keyPressed in keySequence.keys) { |
| // This iterates through the actual key, taking into account any |
| // modifiers. |
| //@ts-expect-error Indexing with string not allowed |
| if (!keySequence.keys[keyPressed][index as number]) { |
| continue; |
| } |
| let modifier = ''; |
| switch (keyPressed) { |
| case 'ctrlKey': |
| // TODO(rshearer): This is a hack to work around the special casing |
| // of the Ctrl key that used to happen in keyEventToString. We won't |
| // need it once we move away from strings completely. |
| modifier = 'Ctrl'; |
| break; |
| case 'searchKeyHeld': |
| const searchKey = KeyUtil.getReadableNameForKeyCode(KeyCode.SEARCH); |
| modifier = searchKey; |
| break; |
| case 'altKey': |
| modifier = 'Alt'; |
| break; |
| case 'altGraphKey': |
| modifier = 'AltGraph'; |
| break; |
| case 'shiftKey': |
| modifier = 'Shift'; |
| break; |
| case 'metaKey': |
| const metaKey = KeyUtil.getReadableNameForKeyCode(KeyCode.SEARCH); |
| modifier = metaKey; |
| break; |
| case 'keyCode': |
| const keyCode = keySequence.keys[keyPressed][index]; |
| // We make sure the keyCode isn't for a modifier key. If it is, then |
| // we've already added that into the string above. |
| if (keySequence.isModifierKey(keyCode) || modifiers) { |
| break; |
| } |
| |
| if (!readableKeyCode) { |
| tempStr += KeyUtil.keyCodeToString(keyCode); |
| break; |
| } |
| |
| // First, try using Chrome OS's localized DOM key string conversion. |
| let domKeyString = |
| await AsyncUtil.getLocalizedDomKeyStringForKeyCode(keyCode); |
| if (!domKeyString) { |
| tempStr += KeyUtil.getReadableNameForKeyCode(keyCode); |
| break; |
| } |
| |
| // Upper case single-lettered key strings for better tts. |
| if (domKeyString.length === 1) { |
| domKeyString = domKeyString.toUpperCase(); |
| } |
| |
| tempStr += domKeyString; |
| break; |
| } |
| if (str.indexOf(modifier) === -1) { |
| tempStr += modifier + '+'; |
| } |
| } |
| str += tempStr; |
| |
| // Strip trailing +. |
| if (str[str.length - 1] === '+') { |
| str = str.slice(0, -1); |
| } |
| } |
| |
| if (keySequence.cvoxModifier || keySequence.prefixKey) { |
| if (str !== '') { |
| str = 'Search+' + str; |
| } else { |
| str = 'Search+Search'; |
| } |
| } else if (keySequence.stickyMode) { |
| // Strip trailing ', then '. |
| const cut = str.slice(str.length - ', then '.length); |
| if (cut === ', then ') { |
| str = str.slice(0, str.length - cut.length); |
| } |
| str = str + '+' + str; |
| } |
| return str; |
| } |
| |
| /** |
| * Looks up if the given key sequence is triggered via double tap. |
| * @return True if key is triggered via double tap. |
| */ |
| export function isDoubleTapKey(key: KeySequence): boolean { |
| let isSet = false; |
| const originalState = key.doubleTap; |
| key.doubleTap = true; |
| for (let i = 0, keySeq; keySeq = KeySequence.doubleTapCache[i]; i++) { |
| if (keySeq.equals(key)) { |
| isSet = true; |
| break; |
| } |
| } |
| key.doubleTap = originalState; |
| return isSet; |
| } |
| |
| /** The time in ms at which the ChromeVox Sticky Mode key was pressed. */ |
| export let modeKeyPressTime: number; |
| modeKeyPressTime = 0; |
| |
| /** Indicates if sequencing is currently building a keyboard shortcut. */ |
| export let sequencing: boolean; |
| sequencing = false; |
| |
| /** The previous KeySequence when sequencing is ON. */ |
| export let prevKeySequence: KeySequence | null; |
| prevKeySequence = null; |
| |
| /** The sticky key sequence. */ |
| export let stickyKeySequence: KeySequence | null; |
| stickyKeySequence = null; |
| |
| /** |
| * Maximum number of key codes the sequence buffer may hold. This is the max |
| * length of a sequential keyboard shortcut, i.e. the number of key that can |
| * be pressed one after the other while modifier keys (Cros+Shift) are held |
| * down. |
| */ |
| export const maxSeqLength = 2; |
| } |