// Copyright (c) 2012 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. var BASE_KEYBOARD = { top: 0, left: 0, width: 1237, height: 514 }; var BASE_INSTRUCTIONS = { top: 194, left: 370, width: 498, height: 142 }; var MODIFIER_TO_CLASS = { 'SHIFT': 'modifier-shift', 'CTRL': 'modifier-ctrl', 'ALT': 'modifier-alt', 'SEARCH': 'modifier-search' }; var IDENTIFIER_TO_CLASS = { '2A': 'is-shift', '1D': 'is-ctrl', '38': 'is-alt', 'E0 5B': 'is-search' }; var LABEL_TO_IDENTIFIER = { 'search': 'E0 5B', 'ctrl': '1D', 'alt': '38', 'caps lock': '3A', 'esc': '01', 'disabled': 'DISABLED' }; var KEYCODE_TO_LABEL = { 8: 'backspace', 9: 'tab', 13: 'enter', 27: 'esc', 32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home', 37: 'left', 38: 'up', 39: 'right', 40: 'down', 46: 'delete', 91: 'search', 92: 'search', 96: '0', 97: '1', 98: '2', 99: '3', 100: '4', 101: '5', 102: '6', 103: '7', 104: '8', 105: '9', 106: '*', 107: '+', 109: '-', 110: '.', 111: '/', 112: 'back', 113: 'forward', 114: 'reload', 115: 'full screen', 116: 'switch window', 117: 'bright down', 118: 'bright up', 119: 'mute', 120: 'vol. down', 121: 'vol. up', 186: ';', 187: '+', 188: ',', 189: '-', 190: '.', 191: '/', 192: '`', 219: '[', 220: '\\', 221: ']', 222: '\'', }; var IME_ID_PREFIX = '_comp_ime_'; var EXTENSION_ID_LEN = 32; var keyboardOverlayId = 'en_US'; var identifierMap = {}; /** * True after at least one keydown event has been received. */ var gotKeyDown = false; /** * Returns the layout name. * @return {string} layout name. */ function getLayoutName() { return getKeyboardGlyphData().layoutName; } /** * Returns layout data. * @return {Array} Keyboard layout data. */ function getLayout() { return keyboardOverlayData['layouts'][getLayoutName()]; } // Cache the shortcut data after it is constructed. var shortcutDataCache; /** * Returns shortcut data. * @return {Object} Keyboard shortcut data. */ function getShortcutData() { if (shortcutDataCache) return shortcutDataCache; shortcutDataCache = keyboardOverlayData['shortcut']; if (!isDisplayUIScalingEnabled()) { // Zoom screen in delete shortcutDataCache['+<>CTRL<>SHIFT']; // Zoom screen out delete shortcutDataCache['-<>CTRL<>SHIFT']; // Reset screen zoom delete shortcutDataCache['0<>CTRL<>SHIFT']; } return shortcutDataCache; } /** * Returns the keyboard overlay ID. * @return {string} Keyboard overlay ID. */ function getKeyboardOverlayId() { return keyboardOverlayId; } /** * Returns keyboard glyph data. * @return {Object} Keyboard glyph data. */ function getKeyboardGlyphData() { return keyboardOverlayData['keyboardGlyph'][getKeyboardOverlayId()]; } /** * Converts a single hex number to a character. * @param {string} hex Hexadecimal string. * @return {string} Unicode values of hexadecimal string. */ function hex2char(hex) { if (!hex) { return ''; } var result = ''; var n = parseInt(hex, 16); if (n <= 0xFFFF) { result += String.fromCharCode(n); } else if (n <= 0x10FFFF) { n -= 0x10000; result += (String.fromCharCode(0xD800 | (n >> 10)) + String.fromCharCode(0xDC00 | (n & 0x3FF))); } else { console.error('hex2Char error: Code point out of range :' + hex); } return result; } var searchIsPressed = false; /** * Returns a list of modifiers from the key event. * @param {Event} e The key event. * @return {Array} List of modifiers based on key event. */ function getModifiers(e) { if (!e) return []; var isKeyDown = (e.type == 'keydown'); var keyCodeToModifier = { 16: 'SHIFT', 17: 'CTRL', 18: 'ALT', 91: 'SEARCH', }; var modifierWithKeyCode = keyCodeToModifier[e.keyCode]; var isPressed = { 'SHIFT': e.shiftKey, 'CTRL': e.ctrlKey, 'ALT': e.altKey, 'SEARCH': searchIsPressed }; if (modifierWithKeyCode) isPressed[modifierWithKeyCode] = isKeyDown; searchIsPressed = isPressed['SEARCH']; // make the result array return ['SHIFT', 'CTRL', 'ALT', 'SEARCH'].filter( function(modifier) { return isPressed[modifier]; }).sort(); } /** * Returns an ID of the key. * @param {string} identifier Key identifier. * @param {number} i Key number. * @return {string} Key ID. */ function keyId(identifier, i) { return identifier + '-key-' + i; } /** * Returns an ID of the text on the key. * @param {string} identifier Key identifier. * @param {number} i Key number. * @return {string} Key text ID. */ function keyTextId(identifier, i) { return identifier + '-key-text-' + i; } /** * Returns an ID of the shortcut text. * @param {string} identifier Key identifier. * @param {number} i Key number. * @return {string} Key shortcut text ID. */ function shortcutTextId(identifier, i) { return identifier + '-shortcut-text-' + i; } /** * Returns true if |list| contains |e|. * @param {Array} list Container list. * @param {string} e Element string. * @return {boolean} Returns true if the list contains the element. */ function contains(list, e) { return list.indexOf(e) != -1; } /** * Returns a list of the class names corresponding to the identifier and * modifiers. * @param {string} identifier Key identifier. * @param {Array} modifiers List of key modifiers. * @return {Array} List of class names corresponding to specified params. */ function getKeyClasses(identifier, modifiers) { var classes = ['keyboard-overlay-key']; for (var i = 0; i < modifiers.length; ++i) { classes.push(MODIFIER_TO_CLASS[modifiers[i]]); } if ((identifier == '2A' && contains(modifiers, 'SHIFT')) || (identifier == '1D' && contains(modifiers, 'CTRL')) || (identifier == '38' && contains(modifiers, 'ALT')) || (identifier == 'E0 5B' && contains(modifiers, 'SEARCH'))) { classes.push('pressed'); classes.push(IDENTIFIER_TO_CLASS[identifier]); } return classes; } /** * Returns true if a character is a ASCII character. * @param {string} c A character to be checked. * @return {boolean} True if the character is an ASCII character. */ function isAscii(c) { var charCode = c.charCodeAt(0); return 0x00 <= charCode && charCode <= 0x7F; } /** * Returns a remapped identiifer based on the preference. * @param {string} identifier Key identifier. * @return {string} Remapped identifier. */ function remapIdentifier(identifier) { return identifierMap[identifier] || identifier; } /** * Returns a label of the key. * @param {string} keyData Key glyph data. * @param {Array} modifiers Key Modifier list. * @return {string} Label of the key. */ function getKeyLabel(keyData, modifiers) { if (!keyData) { return ''; } if (keyData.label) { return keyData.label; } var keyLabel = ''; for (var j = 1; j <= 9; j++) { var pos = keyData['p' + j]; if (!pos) { continue; } keyLabel = hex2char(pos); if (!keyLabel) { continue; } if (isAscii(keyLabel) && getShortcutData()[getAction(keyLabel, modifiers)]) { break; } } return keyLabel; } /** * Returns a normalized string used for a key of shortcutData. * * Examples: * keyCode: 'd', modifiers: ['CTRL', 'SHIFT'] => 'd<>CTRL<>SHIFT' * keyCode: 'alt', modifiers: ['ALT', 'SHIFT'] => 'ALT<>SHIFT' * * @param {string} keyCode Key code. * @param {Array} modifiers Key Modifier list. * @return {string} Normalized key shortcut data string. */ function getAction(keyCode, modifiers) { /** @const */ var separatorStr = '<>'; if (keyCode.toUpperCase() in MODIFIER_TO_CLASS) { keyCode = keyCode.toUpperCase(); if (keyCode in modifiers) { return modifiers.join(separatorStr); } else { var action = [keyCode].concat(modifiers); action.sort(); return action.join(separatorStr); } } return [keyCode].concat(modifiers).join(separatorStr); } /** * Returns a text which displayed on a key. * @param {string} keyData Key glyph data. * @return {string} Key text value. */ function getKeyTextValue(keyData) { if (keyData.label) { // Do not show text on the space key. if (keyData.label == 'space') { return ''; } return keyData.label; } var chars = []; for (var j = 1; j <= 9; ++j) { var pos = keyData['p' + j]; if (pos && pos.length > 0) { chars.push(hex2char(pos)); } } return chars.join(' '); } /** * Updates the whole keyboard. * @param {Array} modifiers Key Modifier list. */ function update(modifiers) { var instructions = $('instructions'); if (modifiers.length == 0) { instructions.style.visibility = 'visible'; } else { instructions.style.visibility = 'hidden'; } var keyboardGlyphData = getKeyboardGlyphData(); var shortcutData = getShortcutData(); var layout = getLayout(); for (var i = 0; i < layout.length; ++i) { var identifier = remapIdentifier(layout[i][0]); var keyData = keyboardGlyphData.keys[identifier]; var classes = getKeyClasses(identifier, modifiers, keyData); var keyLabel = getKeyLabel(keyData, modifiers); var shortcutId = shortcutData[getAction(keyLabel, modifiers)]; if (modifiers.length == 1 && modifiers[0] == 'SHIFT' && identifier == '2A') { // Currently there is no way to identify whether the left shift or the // right shift is preesed from the key event, so I assume the left shift // key is pressed here and do not show keyboard shortcut description for // 'Shift - Shift' (Toggle caps lock) on the left shift key, the // identifier of which is '2A'. // TODO(mazda): Remove this workaround (http://crosbug.com/18047) shortcutId = null; } if (shortcutId) { classes.push('is-shortcut'); } var key = $(keyId(identifier, i)); key.className = classes.join(' '); if (!keyData) { continue; } var keyText = $(keyTextId(identifier, i)); var keyTextValue = getKeyTextValue(keyData); if (keyTextValue) { keyText.style.visibility = 'visible'; } else { keyText.style.visibility = 'hidden'; } keyText.textContent = keyTextValue; var shortcutText = $(shortcutTextId(identifier, i)); if (shortcutId) { shortcutText.style.visibility = 'visible'; shortcutText.textContent = loadTimeData.getString(shortcutId); } else { shortcutText.style.visibility = 'hidden'; } var format = keyboardGlyphData.keys[layout[i][0]].format; if (format) { if (format == 'left' || format == 'right') { shortcutText.style.textAlign = format; keyText.style.textAlign = format; } } } } /** * A callback function for onkeydown and onkeyup events. * @param {Event} e Key event. */ function handleKeyEvent(e) { if (!getKeyboardOverlayId()) { return; } // To avoid flickering as the user releases the modifier keys that were held // to trigger the overlay, avoid updating in response to keyup events until at // least one keydown event has been received. if (!gotKeyDown) { if (e.type == 'keyup') { return; } else if (e.type == 'keydown') { gotKeyDown = true; } } var modifiers = getModifiers(e); update(modifiers); KeyboardOverlayAccessibilityHelper.maybeSpeakAllShortcuts(modifiers); e.preventDefault(); } /** * Initializes the layout of the keys. */ function initLayout() { // Add data for the caps lock key var keys = getKeyboardGlyphData().keys; if (!('3A' in keys)) { keys['3A'] = {label: 'caps lock', format: 'left'}; } // Add data for the special key representing a disabled key keys['DISABLED'] = {label: 'disabled', format: 'left'}; var layout = getLayout(); var keyboard = document.body; var minX = window.innerWidth; var maxX = 0; var minY = window.innerHeight; var maxY = 0; var multiplier = 1.38 * window.innerWidth / BASE_KEYBOARD.width; var keyMargin = 7; var offsetX = 10; var offsetY = 7; for (var i = 0; i < layout.length; i++) { var array = layout[i]; var identifier = remapIdentifier(array[0]); var x = Math.round((array[1] + offsetX) * multiplier); var y = Math.round((array[2] + offsetY) * multiplier); var w = Math.round((array[3] - keyMargin) * multiplier); var h = Math.round((array[4] - keyMargin) * multiplier); var key = document.createElement('div'); key.id = keyId(identifier, i); key.className = 'keyboard-overlay-key'; key.style.left = x + 'px'; key.style.top = y + 'px'; key.style.width = w + 'px'; key.style.height = h + 'px'; var keyText = document.createElement('div'); keyText.id = keyTextId(identifier, i); keyText.className = 'keyboard-overlay-key-text'; keyText.style.visibility = 'hidden'; key.appendChild(keyText); var shortcutText = document.createElement('div'); shortcutText.id = shortcutTextId(identifier, i); shortcutText.className = 'keyboard-overlay-shortcut-text'; shortcutText.style.visilibity = 'hidden'; key.appendChild(shortcutText); keyboard.appendChild(key); minX = Math.min(minX, x); maxX = Math.max(maxX, x + w); minY = Math.min(minY, y); maxY = Math.max(maxY, y + h); } var width = maxX - minX + 1; var height = maxY - minY + 1; keyboard.style.width = (width + 2 * (minX + 1)) + 'px'; keyboard.style.height = (height + 2 * (minY + 1)) + 'px'; var instructions = document.createElement('div'); instructions.id = 'instructions'; instructions.className = 'keyboard-overlay-instructions'; instructions.style.left = ((BASE_INSTRUCTIONS.left - BASE_KEYBOARD.left) * width / BASE_KEYBOARD.width + minX) + 'px'; instructions.style.top = ((BASE_INSTRUCTIONS.top - BASE_KEYBOARD.top) * height / BASE_KEYBOARD.height + minY) + 'px'; instructions.style.width = (width * BASE_INSTRUCTIONS.width / BASE_KEYBOARD.width) + 'px'; instructions.style.height = (height * BASE_INSTRUCTIONS.height / BASE_KEYBOARD.height) + 'px'; var instructionsText = document.createElement('div'); instructionsText.id = 'instructions-text'; instructionsText.className = 'keyboard-overlay-instructions-text'; instructionsText.innerHTML = loadTimeData.getString('keyboardOverlayInstructions'); instructions.appendChild(instructionsText); var instructionsHideText = document.createElement('div'); instructionsHideText.id = 'instructions-hide-text'; instructionsHideText.className = 'keyboard-overlay-instructions-hide-text'; instructionsHideText.innerHTML = loadTimeData.getString('keyboardOverlayInstructionsHide'); instructions.appendChild(instructionsHideText); var learnMoreLinkText = document.createElement('div'); learnMoreLinkText.id = 'learn-more-text'; learnMoreLinkText.className = 'keyboard-overlay-learn-more-text'; learnMoreLinkText.addEventListener('click', learnMoreClicked); var learnMoreLinkAnchor = document.createElement('a'); learnMoreLinkAnchor.href = loadTimeData.getString('keyboardOverlayLearnMoreURL'); learnMoreLinkAnchor.textContent = loadTimeData.getString('keyboardOverlayLearnMore'); learnMoreLinkText.appendChild(learnMoreLinkAnchor); instructions.appendChild(learnMoreLinkText); keyboard.appendChild(instructions); } /** * Returns true if the device has a diamond key. * @return {boolean} Returns true if the device has a diamond key. */ function hasDiamondKey() { return loadTimeData.getBoolean('keyboardOverlayHasChromeOSDiamondKey'); } /** * Returns true if display scaling feature is enabled. * @return {boolean} True if display scaling feature is enabled. */ function isDisplayUIScalingEnabled() { return loadTimeData.getBoolean('keyboardOverlayIsDisplayUIScalingEnabled'); } /** * Initializes the layout and the key labels for the keyboard that has a diamond * key. */ function initDiamondKey() { var newLayoutData = { '1D': [65.0, 287.0, 60.0, 60.0], // left Ctrl '38': [185.0, 287.0, 60.0, 60.0], // left Alt 'E0 5B': [125.0, 287.0, 60.0, 60.0], // search '3A': [5.0, 167.0, 105.0, 60.0], // caps lock '5B': [803.0, 6.0, 72.0, 35.0], // lock key '5D': [5.0, 287.0, 60.0, 60.0] // diamond key }; var layout = getLayout(); var powerKeyIndex = -1; var powerKeyId = '00'; for (var i = 0; i < layout.length; i++) { var keyId = layout[i][0]; if (keyId in newLayoutData) { layout[i] = [keyId].concat(newLayoutData[keyId]); delete newLayoutData[keyId]; } if (keyId == powerKeyId) powerKeyIndex = i; } for (var keyId in newLayoutData) layout.push([keyId].concat(newLayoutData[keyId])); // Remove the power key. if (powerKeyIndex != -1) layout.splice(powerKeyIndex, 1); var keyData = getKeyboardGlyphData()['keys']; var newKeyData = { '3A': {'label': 'caps lock', 'format': 'left'}, '5B': {'label': 'lock'}, '5D': {'label': 'diamond', 'format': 'left'} }; for (var keyId in newKeyData) keyData[keyId] = newKeyData[keyId]; } /** * A callback function for the onload event of the body element. */ function init() { document.addEventListener('keydown', handleKeyEvent); document.addEventListener('keyup', handleKeyEvent); chrome.send('getLabelMap'); } /** * Initializes the global map for remapping identifiers of modifier keys based * on the preference. * Called after sending the 'getLabelMap' message. * @param {Object} remap Identifier map. */ function initIdentifierMap(remap) { for (var key in remap) { var val = remap[key]; if ((key in LABEL_TO_IDENTIFIER) && (val in LABEL_TO_IDENTIFIER)) { identifierMap[LABEL_TO_IDENTIFIER[key]] = LABEL_TO_IDENTIFIER[val]; } else { console.error('Invalid label map element: ' + key + ', ' + val); } } chrome.send('getInputMethodId'); } /** * Initializes the global keyboad overlay ID and the layout of keys. * Called after sending the 'getInputMethodId' message. * @param {inputMethodId} inputMethodId Input Method Identifier. */ function initKeyboardOverlayId(inputMethodId) { // Libcros returns an empty string when it cannot find the keyboard overlay ID // corresponding to the current input method. // In such a case, fallback to the default ID (en_US). var inputMethodIdToOverlayId = keyboardOverlayData['inputMethodIdToOverlayId']; if (inputMethodId) { if (inputMethodId.indexOf(IME_ID_PREFIX) == 0) { // If the input method is a component extension IME, remove the prefix: // _comp_ime_ // The extension id is a hash value with 32 characters. inputMethodId = inputMethodId.slice( IME_ID_PREFIX.length + EXTENSION_ID_LEN); } keyboardOverlayId = inputMethodIdToOverlayId[inputMethodId]; } if (!keyboardOverlayId) { console.error('No keyboard overlay ID for ' + inputMethodId); keyboardOverlayId = 'en_US'; } while (document.body.firstChild) { document.body.removeChild(document.body.firstChild); } // We show Japanese layout as-is because the user has chosen the layout // that is quite diffrent from the physical layout that has a diamond key. if (hasDiamondKey() && getLayoutName() != 'J') initDiamondKey(); initLayout(); update([]); window.webkitRequestAnimationFrame(function() { chrome.send('didPaint'); }); } /** * Handles click events of the learn more link. * @param {Event} e Mouse click event. */ function learnMoreClicked(e) { chrome.send('openLearnMorePage'); chrome.send('dialogClose'); e.preventDefault(); } document.addEventListener('DOMContentLoaded', init);