1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 <include src="keyboard_overlay_data.js">
6 <include src="keyboard_overlay_accessibility_helper.js">
15 var BASE_INSTRUCTIONS = {
22 var MODIFIER_TO_CLASS = {
23 'SHIFT': 'modifier-shift',
24 'CTRL': 'modifier-ctrl',
25 'ALT': 'modifier-alt',
26 'SEARCH': 'modifier-search'
29 var IDENTIFIER_TO_CLASS = {
39 var LABEL_TO_IDENTIFIER = {
45 'disabled': 'DISABLED'
48 var KEYCODE_TO_LABEL = {
103 var IME_ID_PREFIX = '_comp_ime_';
104 var EXTENSION_ID_LEN = 32;
106 var keyboardOverlayId = 'en_US';
107 var identifierMap = {};
110 * True after at least one keydown event has been received.
112 var gotKeyDown = false;
115 * Returns the layout name.
116 * @return {string} layout name.
118 function getLayoutName() {
119 return getKeyboardGlyphData().layoutName;
123 * Returns layout data.
124 * @return {Array} Keyboard layout data.
126 function getLayout() {
127 return keyboardOverlayData['layouts'][getLayoutName()];
130 // Cache the shortcut data after it is constructed.
131 var shortcutDataCache;
134 * Returns shortcut data.
135 * @return {Object} Keyboard shortcut data.
137 function getShortcutData() {
138 if (shortcutDataCache)
139 return shortcutDataCache;
141 shortcutDataCache = keyboardOverlayData['shortcut'];
143 if (!isDisplayUIScalingEnabled()) {
145 delete shortcutDataCache['+<>CTRL<>SHIFT'];
147 delete shortcutDataCache['-<>CTRL<>SHIFT'];
149 delete shortcutDataCache['0<>CTRL<>SHIFT'];
152 return shortcutDataCache;
156 * Returns the keyboard overlay ID.
157 * @return {string} Keyboard overlay ID.
159 function getKeyboardOverlayId() {
160 return keyboardOverlayId;
164 * Returns keyboard glyph data.
165 * @return {Object} Keyboard glyph data.
167 function getKeyboardGlyphData() {
168 return keyboardOverlayData['keyboardGlyph'][getKeyboardOverlayId()];
172 * Converts a single hex number to a character.
173 * @param {string} hex Hexadecimal string.
174 * @return {string} Unicode values of hexadecimal string.
176 function hex2char(hex) {
181 var n = parseInt(hex, 16);
183 result += String.fromCharCode(n);
184 } else if (n <= 0x10FFFF) {
186 result += (String.fromCharCode(0xD800 | (n >> 10)) +
187 String.fromCharCode(0xDC00 | (n & 0x3FF)));
189 console.error('hex2Char error: Code point out of range :' + hex);
195 * Returns a list of modifiers normalized to ignore the distinction between
196 * right or left keys.
197 * @param {Array} modifiers List of modifiers with distinction between right
199 * @return {Array} List of normalized modifiers ignoring the difference between
200 * right or left keys.
202 function normalizeModifiers(modifiers) {
204 if (contains(modifiers, 'L_SHIFT') || contains(modifiers, 'R_SHIFT')) {
205 result.push('SHIFT');
207 if (contains(modifiers, 'L_CTRL') || contains(modifiers, 'R_CTRL')) {
210 if (contains(modifiers, 'L_ALT') || contains(modifiers, 'R_ALT')) {
213 if (contains(modifiers, 'SEARCH')) {
214 result.push('SEARCH');
216 return result.sort();
220 * This table will contain the status of the modifiers.
233 * Returns a list of modifiers from the key event distinguishing right and left
235 * @param {Event} e The key event.
236 * @return {Array} List of modifiers based on key event.
238 function getModifiers(e) {
242 var keyCodeToModifier = {
248 var modifierWithKeyCode = keyCodeToModifier[e.keyCode];
249 /** @const */ var DOM_KEY_LOCATION_LEFT = 1;
250 var side = (e.location == DOM_KEY_LOCATION_LEFT) ? 'L_' : 'R_';
251 var isKeyDown = (e.type == 'keydown');
253 if (modifierWithKeyCode == 'SEARCH') {
254 isPressed['SEARCH'] = isKeyDown;
256 isPressed[side + modifierWithKeyCode] = isKeyDown;
259 // make the result array
260 return result = ['L_SHIFT', 'R_SHIFT', 'L_CTRL', 'R_CTRL', 'L_ALT', 'R_ALT',
263 return isPressed[modifier];
268 * Returns an ID of the key.
269 * @param {string} identifier Key identifier.
270 * @param {number} i Key number.
271 * @return {string} Key ID.
273 function keyId(identifier, i) {
274 return identifier + '-key-' + i;
278 * Returns an ID of the text on the key.
279 * @param {string} identifier Key identifier.
280 * @param {number} i Key number.
281 * @return {string} Key text ID.
283 function keyTextId(identifier, i) {
284 return identifier + '-key-text-' + i;
288 * Returns an ID of the shortcut text.
289 * @param {string} identifier Key identifier.
290 * @param {number} i Key number.
291 * @return {string} Key shortcut text ID.
293 function shortcutTextId(identifier, i) {
294 return identifier + '-shortcut-text-' + i;
298 * Returns true if |list| contains |e|.
299 * @param {Array} list Container list.
300 * @param {string} e Element string.
301 * @return {boolean} Returns true if the list contains the element.
303 function contains(list, e) {
304 return list.indexOf(e) != -1;
308 * Returns a list of the class names corresponding to the identifier and
310 * @param {string} identifier Key identifier.
311 * @param {Array} modifiers List of key modifiers (with distinction between
312 * right and left keys).
313 * @param {Array} normalizedModifiers List of key modifiers (without distinction
314 * between right or left keys).
315 * @return {Array} List of class names corresponding to specified params.
317 function getKeyClasses(identifier, modifiers, normalizedModifiers) {
318 var classes = ['keyboard-overlay-key'];
319 for (var i = 0; i < normalizedModifiers.length; ++i) {
320 classes.push(MODIFIER_TO_CLASS[normalizedModifiers[i]]);
323 if ((identifier == '2A' && contains(modifiers, 'L_SHIFT')) ||
324 (identifier == '36' && contains(modifiers, 'R_SHIFT')) ||
325 (identifier == '1D' && contains(modifiers, 'L_CTRL')) ||
326 (identifier == 'E0 1D' && contains(modifiers, 'R_CTRL')) ||
327 (identifier == '38' && contains(modifiers, 'L_ALT')) ||
328 (identifier == 'E0 38' && contains(modifiers, 'R_ALT')) ||
329 (identifier == 'E0 5B' && contains(modifiers, 'SEARCH'))) {
330 classes.push('pressed');
331 classes.push(IDENTIFIER_TO_CLASS[identifier]);
337 * Returns true if a character is a ASCII character.
338 * @param {string} c A character to be checked.
339 * @return {boolean} True if the character is an ASCII character.
341 function isAscii(c) {
342 var charCode = c.charCodeAt(0);
343 return 0x00 <= charCode && charCode <= 0x7F;
347 * Returns a remapped identiifer based on the preference.
348 * @param {string} identifier Key identifier.
349 * @return {string} Remapped identifier.
351 function remapIdentifier(identifier) {
352 return identifierMap[identifier] || identifier;
356 * Returns a label of the key.
357 * @param {string} keyData Key glyph data.
358 * @param {Array} modifiers Key Modifier list.
359 * @return {string} Label of the key.
361 function getKeyLabel(keyData, modifiers) {
366 return keyData.label;
369 for (var j = 1; j <= 9; j++) {
370 var pos = keyData['p' + j];
374 keyLabel = hex2char(pos);
378 if (isAscii(keyLabel) &&
379 getShortcutData()[getAction(keyLabel, modifiers)]) {
387 * Returns a normalized string used for a key of shortcutData.
390 * keyCode: 'd', modifiers: ['CTRL', 'SHIFT'] => 'd<>CTRL<>SHIFT'
391 * keyCode: 'alt', modifiers: ['ALT', 'SHIFT'] => 'ALT<>SHIFT'
393 * @param {string} keyCode Key code.
394 * @param {Array} modifiers Key Modifier list.
395 * @return {string} Normalized key shortcut data string.
397 function getAction(keyCode, modifiers) {
398 /** @const */ var separatorStr = '<>';
399 if (keyCode.toUpperCase() in MODIFIER_TO_CLASS) {
400 keyCode = keyCode.toUpperCase();
401 if (keyCode in modifiers) {
402 return modifiers.join(separatorStr);
404 var action = [keyCode].concat(modifiers);
406 return action.join(separatorStr);
409 return [keyCode].concat(modifiers).join(separatorStr);
413 * Returns a text which displayed on a key.
414 * @param {string} keyData Key glyph data.
415 * @return {string} Key text value.
417 function getKeyTextValue(keyData) {
419 // Do not show text on the space key.
420 if (keyData.label == 'space') {
423 return keyData.label;
427 for (var j = 1; j <= 9; ++j) {
428 var pos = keyData['p' + j];
429 if (pos && pos.length > 0) {
430 chars.push(hex2char(pos));
433 return chars.join(' ');
437 * Updates the whole keyboard.
438 * @param {Array} modifiers Key Modifier list.
439 * @param {Array} normModifiers Key Modifier list ignoring the distinction
440 * between right and left keys.
442 function update(modifiers, normModifiers) {
443 var instructions = $('instructions');
444 if (modifiers.length == 0) {
445 instructions.style.visibility = 'visible';
447 instructions.style.visibility = 'hidden';
450 var keyboardGlyphData = getKeyboardGlyphData();
451 var shortcutData = getShortcutData();
452 var layout = getLayout();
453 for (var i = 0; i < layout.length; ++i) {
454 var identifier = remapIdentifier(layout[i][0]);
455 var keyData = keyboardGlyphData.keys[identifier];
456 var classes = getKeyClasses(identifier, modifiers, normModifiers);
457 var keyLabel = getKeyLabel(keyData, normModifiers);
458 var shortcutId = shortcutData[getAction(keyLabel, normModifiers)];
459 if (modifiers.length == 0 &&
460 (identifier == '2A' || identifier == '36')) {
461 // Either the right or left shift keys are used to disable the caps lock
462 // if it was enabled. To fix crbug.com/453623.
463 shortcutId = 'keyboardOverlayDisableCapsLock';
466 classes.push('is-shortcut');
469 var key = $(keyId(identifier, i));
470 key.className = classes.join(' ');
476 var keyText = $(keyTextId(identifier, i));
477 var keyTextValue = getKeyTextValue(keyData);
479 keyText.style.visibility = 'visible';
481 keyText.style.visibility = 'hidden';
483 keyText.textContent = keyTextValue;
485 var shortcutText = $(shortcutTextId(identifier, i));
487 shortcutText.style.visibility = 'visible';
488 shortcutText.textContent = loadTimeData.getString(shortcutId);
490 shortcutText.style.visibility = 'hidden';
493 var format = keyboardGlyphData.keys[layout[i][0]].format;
495 if (format == 'left' || format == 'right') {
496 shortcutText.style.textAlign = format;
497 keyText.style.textAlign = format;
504 * A callback function for onkeydown and onkeyup events.
505 * @param {Event} e Key event.
507 function handleKeyEvent(e) {
508 if (!getKeyboardOverlayId()) {
512 var modifiers = getModifiers(e);
514 // To avoid flickering as the user releases the modifier keys that were held
515 // to trigger the overlay, avoid updating in response to keyup events until at
516 // least one keydown event has been received.
518 if (e.type == 'keyup') {
520 } else if (e.type == 'keydown') {
525 var normModifiers = normalizeModifiers(modifiers);
526 update(modifiers, normModifiers);
527 KeyboardOverlayAccessibilityHelper.maybeSpeakAllShortcuts(normModifiers);
532 * Initializes the layout of the keys.
534 function initLayout() {
535 // Add data for the caps lock key
536 var keys = getKeyboardGlyphData().keys;
537 if (!('3A' in keys)) {
538 keys['3A'] = {label: 'caps lock', format: 'left'};
540 // Add data for the special key representing a disabled key
541 keys['DISABLED'] = {label: 'disabled', format: 'left'};
543 var layout = getLayout();
544 var keyboard = document.body;
545 var minX = window.innerWidth;
547 var minY = window.innerHeight;
549 var multiplier = 1.38 * window.innerWidth / BASE_KEYBOARD.width;
553 for (var i = 0; i < layout.length; i++) {
554 var array = layout[i];
555 var identifier = remapIdentifier(array[0]);
556 var x = Math.round((array[1] + offsetX) * multiplier);
557 var y = Math.round((array[2] + offsetY) * multiplier);
558 var w = Math.round((array[3] - keyMargin) * multiplier);
559 var h = Math.round((array[4] - keyMargin) * multiplier);
561 var key = document.createElement('div');
562 key.id = keyId(identifier, i);
563 key.className = 'keyboard-overlay-key';
564 key.style.left = x + 'px';
565 key.style.top = y + 'px';
566 key.style.width = w + 'px';
567 key.style.height = h + 'px';
569 var keyText = document.createElement('div');
570 keyText.id = keyTextId(identifier, i);
571 keyText.className = 'keyboard-overlay-key-text';
572 keyText.style.visibility = 'hidden';
573 key.appendChild(keyText);
575 var shortcutText = document.createElement('div');
576 shortcutText.id = shortcutTextId(identifier, i);
577 shortcutText.className = 'keyboard-overlay-shortcut-text';
578 shortcutText.style.visilibity = 'hidden';
579 key.appendChild(shortcutText);
580 keyboard.appendChild(key);
582 minX = Math.min(minX, x);
583 maxX = Math.max(maxX, x + w);
584 minY = Math.min(minY, y);
585 maxY = Math.max(maxY, y + h);
588 var width = maxX - minX + 1;
589 var height = maxY - minY + 1;
590 keyboard.style.width = (width + 2 * (minX + 1)) + 'px';
591 keyboard.style.height = (height + 2 * (minY + 1)) + 'px';
593 var instructions = document.createElement('div');
594 instructions.id = 'instructions';
595 instructions.className = 'keyboard-overlay-instructions';
596 instructions.style.left = ((BASE_INSTRUCTIONS.left - BASE_KEYBOARD.left) *
597 width / BASE_KEYBOARD.width + minX) + 'px';
598 instructions.style.top = ((BASE_INSTRUCTIONS.top - BASE_KEYBOARD.top) *
599 height / BASE_KEYBOARD.height + minY) + 'px';
600 instructions.style.width = (width * BASE_INSTRUCTIONS.width /
601 BASE_KEYBOARD.width) + 'px';
602 instructions.style.height = (height * BASE_INSTRUCTIONS.height /
603 BASE_KEYBOARD.height) + 'px';
605 var instructionsText = document.createElement('div');
606 instructionsText.id = 'instructions-text';
607 instructionsText.className = 'keyboard-overlay-instructions-text';
608 instructionsText.innerHTML =
609 loadTimeData.getString('keyboardOverlayInstructions');
610 instructions.appendChild(instructionsText);
611 var instructionsHideText = document.createElement('div');
612 instructionsHideText.id = 'instructions-hide-text';
613 instructionsHideText.className = 'keyboard-overlay-instructions-hide-text';
614 instructionsHideText.innerHTML =
615 loadTimeData.getString('keyboardOverlayInstructionsHide');
616 instructions.appendChild(instructionsHideText);
617 var learnMoreLinkText = document.createElement('div');
618 learnMoreLinkText.id = 'learn-more-text';
619 learnMoreLinkText.className = 'keyboard-overlay-learn-more-text';
620 learnMoreLinkText.addEventListener('click', learnMoreClicked);
621 var learnMoreLinkAnchor = document.createElement('a');
622 learnMoreLinkAnchor.href =
623 loadTimeData.getString('keyboardOverlayLearnMoreURL');
624 learnMoreLinkAnchor.textContent =
625 loadTimeData.getString('keyboardOverlayLearnMore');
626 learnMoreLinkText.appendChild(learnMoreLinkAnchor);
627 instructions.appendChild(learnMoreLinkText);
628 keyboard.appendChild(instructions);
632 * Returns true if the device has a diamond key.
633 * @return {boolean} Returns true if the device has a diamond key.
635 function hasDiamondKey() {
636 return loadTimeData.getBoolean('keyboardOverlayHasChromeOSDiamondKey');
640 * Returns true if display scaling feature is enabled.
641 * @return {boolean} True if display scaling feature is enabled.
643 function isDisplayUIScalingEnabled() {
644 return loadTimeData.getBoolean('keyboardOverlayIsDisplayUIScalingEnabled');
648 * Initializes the layout and the key labels for the keyboard that has a diamond
651 function initDiamondKey() {
652 var newLayoutData = {
653 '1D': [65.0, 287.0, 60.0, 60.0], // left Ctrl
654 '38': [185.0, 287.0, 60.0, 60.0], // left Alt
655 'E0 5B': [125.0, 287.0, 60.0, 60.0], // search
656 '3A': [5.0, 167.0, 105.0, 60.0], // caps lock
657 '5B': [803.0, 6.0, 72.0, 35.0], // lock key
658 '5D': [5.0, 287.0, 60.0, 60.0] // diamond key
661 var layout = getLayout();
662 var powerKeyIndex = -1;
663 var powerKeyId = '00';
664 for (var i = 0; i < layout.length; i++) {
665 var keyId = layout[i][0];
666 if (keyId in newLayoutData) {
667 layout[i] = [keyId].concat(newLayoutData[keyId]);
668 delete newLayoutData[keyId];
670 if (keyId == powerKeyId)
673 for (var keyId in newLayoutData)
674 layout.push([keyId].concat(newLayoutData[keyId]));
676 // Remove the power key.
677 if (powerKeyIndex != -1)
678 layout.splice(powerKeyIndex, 1);
680 var keyData = getKeyboardGlyphData()['keys'];
682 '3A': {'label': 'caps lock', 'format': 'left'},
683 '5B': {'label': 'lock'},
684 '5D': {'label': 'diamond', 'format': 'left'}
686 for (var keyId in newKeyData)
687 keyData[keyId] = newKeyData[keyId];
691 * A callback function for the onload event of the body element.
694 document.addEventListener('keydown', handleKeyEvent);
695 document.addEventListener('keyup', handleKeyEvent);
696 chrome.send('getLabelMap');
700 * Initializes the global map for remapping identifiers of modifier keys based
702 * Called after sending the 'getLabelMap' message.
703 * @param {Object} remap Identifier map.
705 function initIdentifierMap(remap) {
706 for (var key in remap) {
707 var val = remap[key];
708 if ((key in LABEL_TO_IDENTIFIER) &&
709 (val in LABEL_TO_IDENTIFIER)) {
710 identifierMap[LABEL_TO_IDENTIFIER[key]] =
711 LABEL_TO_IDENTIFIER[val];
713 console.error('Invalid label map element: ' + key + ', ' + val);
716 chrome.send('getInputMethodId');
720 * Initializes the global keyboad overlay ID and the layout of keys.
721 * Called after sending the 'getInputMethodId' message.
722 * @param {inputMethodId} inputMethodId Input Method Identifier.
724 function initKeyboardOverlayId(inputMethodId) {
725 // Libcros returns an empty string when it cannot find the keyboard overlay ID
726 // corresponding to the current input method.
727 // In such a case, fallback to the default ID (en_US).
728 var inputMethodIdToOverlayId =
729 keyboardOverlayData['inputMethodIdToOverlayId'];
731 if (inputMethodId.indexOf(IME_ID_PREFIX) == 0) {
732 // If the input method is a component extension IME, remove the prefix:
733 // _comp_ime_<ext_id>
734 // The extension id is a hash value with 32 characters.
735 inputMethodId = inputMethodId.slice(
736 IME_ID_PREFIX.length + EXTENSION_ID_LEN);
738 keyboardOverlayId = inputMethodIdToOverlayId[inputMethodId];
740 if (!keyboardOverlayId) {
741 console.error('No keyboard overlay ID for ' + inputMethodId);
742 keyboardOverlayId = 'en_US';
744 while (document.body.firstChild) {
745 document.body.removeChild(document.body.firstChild);
747 // We show Japanese layout as-is because the user has chosen the layout
748 // that is quite diffrent from the physical layout that has a diamond key.
749 if (hasDiamondKey() && getLayoutName() != 'J')
753 window.webkitRequestAnimationFrame(function() {
754 chrome.send('didPaint');
759 * Handles click events of the learn more link.
760 * @param {Event} e Mouse click event.
762 function learnMoreClicked(e) {
763 chrome.send('openLearnMorePage');
764 chrome.send('dialogClose');
768 document.addEventListener('DOMContentLoaded', init);