blob: e823efe5a99bb8f7590846e60b98f56520019e42 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// 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.
*/
goog.provide('cvox.KeyUtil');
goog.provide('cvox.SimpleKeyEvent');
goog.require('Msgs');
goog.require('cvox.ChromeVox');
goog.require('cvox.KeySequence');
/**
* @typedef {{ctrlKey: (boolean|undefined),
* altKey: (boolean|undefined),
* shiftKey: (boolean|undefined),
* keyCode: (number|undefined)}}
*/
cvox.SimpleKeyEvent;
/**
* Create the namespace
* @constructor
*/
cvox.KeyUtil = function() {
};
/**
* The time in ms at which the ChromeVox Sticky Mode key was pressed.
* @type {number}
*/
cvox.KeyUtil.modeKeyPressTime = 0;
/**
* Indicates if sequencing is currently active for building a keyboard shortcut.
* @type {boolean}
*/
cvox.KeyUtil.sequencing = false;
/**
* The previous KeySequence when sequencing is ON.
* @type {cvox.KeySequence}
*/
cvox.KeyUtil.prevKeySequence = null;
/**
* The sticky key sequence.
* @type {cvox.KeySequence}
*/
cvox.KeyUtil.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.
* @const
* @type {number}
*/
cvox.KeyUtil.maxSeqLength = 2;
/**
* Convert a key event into a Key Sequence representation.
*
* @param {Event|cvox.SimpleKeyEvent} keyEvent The keyEvent to convert.
* @return {cvox.KeySequence} A key sequence representation of the key event.
*/
cvox.KeyUtil.keyEventToKeySequence = function(keyEvent) {
var util = cvox.KeyUtil;
if (util.prevKeySequence &&
(util.maxSeqLength == util.prevKeySequence.length())) {
// Reset the sequence buffer if max sequence length is reached.
util.sequencing = false;
util.prevKeySequence = null;
}
// 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
var keyIsPrefixed = util.sequencing || keyEvent['keyPrefix'] ||
keyEvent['stickyMode'];
// Create key sequence.
var keySequence = new cvox.KeySequence(keyEvent);
// Check if the Cvox key should be considered as pressed because the
// modifier key combination is active.
var keyWasCvox = keySequence.cvoxModifier;
if (keyIsPrefixed || keyWasCvox) {
if (!util.sequencing && util.isSequenceSwitchKeyCode(keySequence)) {
// If this is the beginning of a sequence.
util.sequencing = true;
util.prevKeySequence = keySequence;
return keySequence;
} else if (util.sequencing) {
if (util.prevKeySequence.addKeyEvent(keyEvent)) {
keySequence = util.prevKeySequence;
util.prevKeySequence = null;
util.sequencing = false;
return keySequence;
} else {
throw 'Think sequencing is enabled, yet util.prevKeySequence already' +
'has two key codes' + util.prevKeySequence;
}
}
} else {
util.sequencing = false;
}
// Repeated keys pressed.
var currTime = new Date().getTime();
if (cvox.KeyUtil.isDoubleTapKey(keySequence) &&
util.prevKeySequence &&
keySequence.equals(util.prevKeySequence)) {
var prevTime = util.modeKeyPressTime;
if (prevTime > 0 && currTime - prevTime < 300) { // Double tap
keySequence = util.prevKeySequence;
keySequence.doubleTap = true;
util.prevKeySequence = null;
util.sequencing = false;
// Resets the search key state tracked for ChromeOS because in OOBE,
// we never get a key up for the key down (keyCode 91).
if (cvox.ChromeVox.isChromeOS &&
keyEvent.keyCode == cvox.KeyUtil.getStickyKeyCode()) {
cvox.ChromeVox.searchKeyHeld = 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.
}
util.prevKeySequence = keySequence;
util.modeKeyPressTime = currTime;
return keySequence;
};
/**
* Returns the string representation of the specified key code.
*
* @param {number} keyCode key code.
* @return {string} A string representation of the key event.
*/
cvox.KeyUtil.keyCodeToString = function(keyCode) {
if (keyCode == 17) {
return 'Ctrl';
}
if (keyCode == 18) {
return 'Alt';
}
if (keyCode == 16) {
return 'Shift';
}
if ((keyCode == 91) || (keyCode == 93)) {
if (cvox.ChromeVox.isChromeOS) {
return 'Search';
} else if (cvox.ChromeVox.isMac) {
return 'Cmd';
} else {
return 'Win';
}
}
// TODO(rshearer): This is a hack to work around the special casing of the
// sticky mode string that used to happen in keyEventToString. We won't need
// it once we move away from strings completely.
if (keyCode == 45) {
return 'Insert';
}
if (keyCode >= 65 && keyCode <= 90) {
// A - Z
return String.fromCharCode(keyCode);
} else if (keyCode >= 48 && keyCode <= 57) {
// 0 - 9
return String.fromCharCode(keyCode);
} else {
// Anything else
return '#' + keyCode;
}
};
/**
* Returns the keycode of a string representation of the specified modifier.
*
* @param {string} keyString Modifier key.
* @return {number} Key code.
*/
cvox.KeyUtil.modStringToKeyCode = function(keyString) {
switch (keyString) {
case 'Ctrl':
return 17;
case 'Alt':
return 18;
case 'Shift':
return 16;
case 'Cmd':
case 'Win':
return 91;
}
return -1;
};
/**
* Returns the key codes of a string respresentation of the ChromeVox modifiers.
*
* @return {Array<number>} Array of key codes.
*/
cvox.KeyUtil.cvoxModKeyCodes = function() {
var modKeyCombo = cvox.ChromeVox.modKeyStr.split(/\+/g);
var modKeyCodes = modKeyCombo.map(function(keyString) {
return cvox.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
* cvox.KeyUtil.sequenceSwitchKeyCodes
*
* @param {!cvox.KeySequence} rhKeySeq The key sequence to check.
* @return {boolean} true if it is a sequence switch keycode, false otherwise.
*/
cvox.KeyUtil.isSequenceSwitchKeyCode = function(rhKeySeq) {
for (var i = 0; i < cvox.ChromeVox.sequenceSwitchKeyCodes.length; i++) {
var lhKeySeq = cvox.ChromeVox.sequenceSwitchKeyCodes[i];
if (lhKeySeq.equals(rhKeySeq)) {
return true;
}
}
return false;
};
/**
* Get readable string description of the specified keycode.
*
* @param {number} keyCode The key code.
* @return {string} Returns a string description.
*/
cvox.KeyUtil.getReadableNameForKeyCode = function(keyCode) {
var msg = Msgs.getMsg.bind(Msgs);
var cros = cvox.ChromeVox.isChromeOS;
if (keyCode == 0) {
return 'Power button';
} else if (keyCode == 17) {
return 'Control';
} else if (keyCode == 18) {
return 'Alt';
} else if (keyCode == 16) {
return 'Shift';
} else if (keyCode == 9) {
return 'Tab';
} else if ((keyCode == 91) || (keyCode == 93)) {
if (cros) {
return 'Search';
} else if (cvox.ChromeVox.isMac) {
return 'Cmd';
} else {
return 'Win';
}
} else if (keyCode == 8) {
return 'Backspace';
} else if (keyCode == 32) {
return 'Space';
} else if (keyCode == 35) {
return'end';
} else if (keyCode == 36) {
return 'home';
} else if (keyCode == 37) {
return 'Left arrow';
} else if (keyCode == 38) {
return 'Up arrow';
} else if (keyCode == 39) {
return 'Right arrow';
} else if (keyCode == 40) {
return 'Down arrow';
} else if (keyCode == 45) {
return 'Insert';
} else if (keyCode == 13) {
return 'Enter';
} else if (keyCode == 27) {
return 'Escape';
} else if (keyCode == 112) {
return cros ? msg('back_key') : 'F1';
} else if (keyCode == 113) {
return cros ? msg('forward_key') : 'F2';
} else if (keyCode == 114) {
return cros ? msg('refresh_key') : 'F3';
} else if (keyCode == 115) {
return cros ? msg('toggle_full_screen_key') : 'F4';
} else if (keyCode == 116) {
return cros ? msg('window_overview_key') : 'F5';
} else if (keyCode == 117) {
return cros ? msg('brightness_down_key') : 'F6';
} else if (keyCode == 118) {
return cros ? msg('brightness_up_key') : 'F7';
} else if (keyCode == 119) {
return cros ? msg('volume_mute_key') : 'F8';
} else if (keyCode == 120) {
return cros ? msg('volume_down_key') : 'F9';
} else if (keyCode == 121) {
return cros ? msg('volume_up_key') : 'F10';
} else if (keyCode == 122) {
return 'F11';
} else if (keyCode == 123) {
return 'F12';
} else if (keyCode == 186) {
return 'Semicolon';
} else if (keyCode == 187) {
return 'Equal sign';
} else if (keyCode == 188) {
return 'Comma';
} else if (keyCode == 189) {
return 'Dash';
} else if (keyCode == 190) {
return 'Period';
} else if (keyCode == 191) {
return 'Forward slash';
} else if (keyCode == 192) {
return 'Grave accent';
} else if (keyCode == 219) {
return 'Open bracket';
} else if (keyCode == 220) {
return 'Back slash';
} else if (keyCode == 221) {
return 'Close bracket';
} else if (keyCode == 222) {
return 'Single quote';
} else if (keyCode == 115) {
return 'Toggle full screen';
} else if (keyCode >= 48 && keyCode <= 90) {
return String.fromCharCode(keyCode);
}
return '';
};
/**
* Get the platform specific sticky key keycode.
*
* @return {number} The platform specific sticky key keycode.
*/
cvox.KeyUtil.getStickyKeyCode = function() {
// TODO (rshearer): This should not be hard-coded here.
var stickyKeyCode = 45; // Insert for Linux and Windows
if (cvox.ChromeVox.isChromeOS || cvox.ChromeVox.isMac) {
stickyKeyCode = 91; // GUI key (Search/Cmd) for ChromeOs and Mac
}
return stickyKeyCode;
};
/**
* Get readable string description for an internal string representation of a
* key or a keyboard shortcut.
*
* @param {string} keyStr The internal string repsentation of a key or
* a keyboard shortcut.
* @return {?string} Readable string representation of the input.
*/
cvox.KeyUtil.getReadableNameForStr = function(keyStr) {
// TODO (clchen): Refactor this function away since it is no longer used.
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 {cvox.KeySequence} keySequence The KeySequence object.
* @param {boolean=} opt_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 {boolean=} opt_modifiers Restrict printout to only modifiers. Defaults
* to false.
* @return {string} Readable string representation of the KeySequence object.
*/
cvox.KeyUtil.keySequenceToString = function(
keySequence, opt_readableKeyCode, opt_modifiers) {
// 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.
var str = '';
var numKeys = keySequence.length();
for (var index = 0; index < numKeys; index++) {
if (str != '' && !opt_modifiers) {
str += '>';
} else if (str != '') {
str += '+';
}
// This iterates through the sequence. Either we're on the first key
// pressed or the second
var tempStr = '';
for (var keyPressed in keySequence.keys) {
// This iterates through the actual key, taking into account any
// modifiers.
if (!keySequence.keys[keyPressed][index]) {
continue;
}
var 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':
var searchKey = cvox.KeyUtil.getReadableNameForKeyCode(91);
modifier = searchKey;
break;
case 'altKey':
modifier = 'Alt';
break;
case 'altGraphKey':
modifier = 'AltGraph';
break;
case 'shiftKey':
modifier = 'Shift';
break;
case 'metaKey':
var metaKey = cvox.KeyUtil.getReadableNameForKeyCode(91);
modifier = metaKey;
break;
case 'keyCode':
var 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) && !opt_modifiers) {
if (opt_readableKeyCode) {
tempStr += cvox.KeyUtil.getReadableNameForKeyCode(keyCode);
} else {
tempStr += cvox.KeyUtil.keyCodeToString(keyCode);
}
}
}
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 = 'ChromeVox+' + str;
} else {
str = 'Cvox';
}
} else if (keySequence.stickyMode) {
if (str[str.length - 1] == '>') {
str = str.slice(0, -1);
}
str = str + '+' + str;
}
return str;
};
/**
* Looks up if the given key sequence is triggered via double tap.
* @param {cvox.KeySequence} key The key.
* @return {boolean} True if key is triggered via double tap.
*/
cvox.KeyUtil.isDoubleTapKey = function(key) {
var isSet = false;
var originalState = key.doubleTap;
key.doubleTap = true;
for (var i = 0, keySeq; keySeq = cvox.KeySequence.doubleTapCache[i]; i++) {
if (keySeq.equals(key)) {
isSet = true;
break;
}
}
key.doubleTap = originalState;
return isSet;
};