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 = {
104 * Some key labels define actions (like for example 'vol. up' or 'mute').
105 * These labels should be localized. (crbug.com/471025).
107 var LABEL_TO_LOCALIZED_LABEL_ID = {
108 'esc' : 'keyboardOverlayEscKeyLabel',
109 'back' : 'keyboardOverlayBackKeyLabel',
110 'forward' : 'keyboardOverlayForwardKeyLabel',
111 'reload' : 'keyboardOverlayReloadKeyLabel',
112 'full screen' : 'keyboardOverlayFullScreenKeyLabel',
113 'switch window' : 'keyboardOverlaySwitchWinKeyLabel',
114 'bright down' : 'keyboardOverlayBrightDownKeyLabel',
115 'bright up' : 'keyboardOverlayBrightUpKeyLabel',
116 'mute' : 'keyboardOverlayMuteKeyLabel',
117 'vol. down' : 'keyboardOverlayVolDownKeyLabel',
118 'vol. up' : 'keyboardOverlayVolUpKeyLabel',
119 'power' : 'keyboardOverlayPowerKeyLabel',
120 'backspace' : 'keyboardOverlayBackspaceKeyLabel',
121 'tab' : 'keyboardOverlayTabKeyLabel',
122 'search' : 'keyboardOverlaySearchKeyLabel',
123 'enter' : 'keyboardOverlayEnterKeyLabel',
124 'shift' : 'keyboardOverlayShiftKeyLabel',
125 'ctrl' : 'keyboardOverlayCtrlKeyLabel',
126 'alt' : 'keyboardOverlayAltKeyLabel',
127 'left' : 'keyboardOverlayLeftKeyLabel',
128 'right' : 'keyboardOverlayRightKeyLabel',
129 'up' : 'keyboardOverlayUpKeyLabel',
130 'down' : 'keyboardOverlayDownKeyLabel',
133 var IME_ID_PREFIX = '_comp_ime_';
134 var EXTENSION_ID_LEN = 32;
136 var keyboardOverlayId = 'en_US';
137 var identifierMap = {};
140 * True after at least one keydown event has been received.
142 var gotKeyDown = false;
145 * Returns the layout name.
146 * @return {string} layout name.
148 function getLayoutName() {
149 return getKeyboardGlyphData().layoutName;
153 * Returns layout data.
154 * @return {Array} Keyboard layout data.
156 function getLayout() {
157 return keyboardOverlayData['layouts'][getLayoutName()];
160 // Cache the shortcut data after it is constructed.
161 var shortcutDataCache;
164 * Returns shortcut data.
165 * @return {Object} Keyboard shortcut data.
167 function getShortcutData() {
168 if (shortcutDataCache)
169 return shortcutDataCache;
171 shortcutDataCache = keyboardOverlayData['shortcut'];
173 if (!isDisplayUIScalingEnabled()) {
175 delete shortcutDataCache['+<>CTRL<>SHIFT'];
177 delete shortcutDataCache['-<>CTRL<>SHIFT'];
179 delete shortcutDataCache['0<>CTRL<>SHIFT'];
182 return shortcutDataCache;
186 * Returns the keyboard overlay ID.
187 * @return {string} Keyboard overlay ID.
189 function getKeyboardOverlayId() {
190 return keyboardOverlayId;
194 * Returns keyboard glyph data.
195 * @return {Object} Keyboard glyph data.
197 function getKeyboardGlyphData() {
198 return keyboardOverlayData['keyboardGlyph'][getKeyboardOverlayId()];
202 * Converts a single hex number to a character.
203 * @param {string} hex Hexadecimal string.
204 * @return {string} Unicode values of hexadecimal string.
206 function hex2char(hex) {
211 var n = parseInt(hex, 16);
213 result += String.fromCharCode(n);
214 } else if (n <= 0x10FFFF) {
216 result += (String.fromCharCode(0xD800 | (n >> 10)) +
217 String.fromCharCode(0xDC00 | (n & 0x3FF)));
219 console.error('hex2Char error: Code point out of range :' + hex);
225 * Returns a list of modifiers normalized to ignore the distinction between
226 * right or left keys.
227 * @param {Array} modifiers List of modifiers with distinction between right
229 * @return {Array} List of normalized modifiers ignoring the difference between
230 * right or left keys.
232 function normalizeModifiers(modifiers) {
234 if (contains(modifiers, 'L_SHIFT') || contains(modifiers, 'R_SHIFT')) {
235 result.push('SHIFT');
237 if (contains(modifiers, 'L_CTRL') || contains(modifiers, 'R_CTRL')) {
240 if (contains(modifiers, 'L_ALT') || contains(modifiers, 'R_ALT')) {
243 if (contains(modifiers, 'SEARCH')) {
244 result.push('SEARCH');
246 return result.sort();
250 * This table will contain the status of the modifiers.
263 * Returns a list of modifiers from the key event distinguishing right and left
265 * @param {Event} e The key event.
266 * @return {Array} List of modifiers based on key event.
268 function getModifiers(e) {
272 var keyCodeToModifier = {
278 var modifierWithKeyCode = keyCodeToModifier[e.keyCode];
279 /** @const */ var DOM_KEY_LOCATION_LEFT = 1;
280 var side = (e.location == DOM_KEY_LOCATION_LEFT) ? 'L_' : 'R_';
281 var isKeyDown = (e.type == 'keydown');
283 if (modifierWithKeyCode == 'SEARCH') {
284 isPressed['SEARCH'] = isKeyDown;
286 isPressed[side + modifierWithKeyCode] = isKeyDown;
289 // make the result array
290 return result = ['L_SHIFT', 'R_SHIFT', 'L_CTRL', 'R_CTRL', 'L_ALT', 'R_ALT',
293 return isPressed[modifier];
298 * Returns an ID of the key.
299 * @param {string} identifier Key identifier.
300 * @param {number} i Key number.
301 * @return {string} Key ID.
303 function keyId(identifier, i) {
304 return identifier + '-key-' + i;
308 * Returns an ID of the text on the key.
309 * @param {string} identifier Key identifier.
310 * @param {number} i Key number.
311 * @return {string} Key text ID.
313 function keyTextId(identifier, i) {
314 return identifier + '-key-text-' + i;
318 * Returns an ID of the shortcut text.
319 * @param {string} identifier Key identifier.
320 * @param {number} i Key number.
321 * @return {string} Key shortcut text ID.
323 function shortcutTextId(identifier, i) {
324 return identifier + '-shortcut-text-' + i;
328 * Returns true if |list| contains |e|.
329 * @param {Array} list Container list.
330 * @param {string} e Element string.
331 * @return {boolean} Returns true if the list contains the element.
333 function contains(list, e) {
334 return list.indexOf(e) != -1;
338 * Returns a list of the class names corresponding to the identifier and
340 * @param {string} identifier Key identifier.
341 * @param {Array} modifiers List of key modifiers (with distinction between
342 * right and left keys).
343 * @param {Array} normalizedModifiers List of key modifiers (without distinction
344 * between right or left keys).
345 * @return {Array} List of class names corresponding to specified params.
347 function getKeyClasses(identifier, modifiers, normalizedModifiers) {
348 var classes = ['keyboard-overlay-key'];
349 for (var i = 0; i < normalizedModifiers.length; ++i) {
350 classes.push(MODIFIER_TO_CLASS[normalizedModifiers[i]]);
353 if ((identifier == '2A' && contains(modifiers, 'L_SHIFT')) ||
354 (identifier == '36' && contains(modifiers, 'R_SHIFT')) ||
355 (identifier == '1D' && contains(modifiers, 'L_CTRL')) ||
356 (identifier == 'E0 1D' && contains(modifiers, 'R_CTRL')) ||
357 (identifier == '38' && contains(modifiers, 'L_ALT')) ||
358 (identifier == 'E0 38' && contains(modifiers, 'R_ALT')) ||
359 (identifier == 'E0 5B' && contains(modifiers, 'SEARCH'))) {
360 classes.push('pressed');
361 classes.push(IDENTIFIER_TO_CLASS[identifier]);
367 * Returns true if a character is a ASCII character.
368 * @param {string} c A character to be checked.
369 * @return {boolean} True if the character is an ASCII character.
371 function isAscii(c) {
372 var charCode = c.charCodeAt(0);
373 return 0x00 <= charCode && charCode <= 0x7F;
377 * Returns a remapped identiifer based on the preference.
378 * @param {string} identifier Key identifier.
379 * @return {string} Remapped identifier.
381 function remapIdentifier(identifier) {
382 return identifierMap[identifier] || identifier;
386 * Returns a label of the key.
387 * @param {string} keyData Key glyph data.
388 * @param {Array} modifiers Key Modifier list.
389 * @return {string} Label of the key.
391 function getKeyLabel(keyData, modifiers) {
396 return keyData.label;
399 for (var j = 1; j <= 9; j++) {
400 var pos = keyData['p' + j];
404 keyLabel = hex2char(pos);
408 if (isAscii(keyLabel) &&
409 getShortcutData()[getAction(keyLabel, modifiers)]) {
417 * Returns a normalized string used for a key of shortcutData.
420 * keyCode: 'd', modifiers: ['CTRL', 'SHIFT'] => 'd<>CTRL<>SHIFT'
421 * keyCode: 'alt', modifiers: ['ALT', 'SHIFT'] => 'ALT<>SHIFT'
423 * @param {string} keyCode Key code.
424 * @param {Array} modifiers Key Modifier list.
425 * @return {string} Normalized key shortcut data string.
427 function getAction(keyCode, modifiers) {
428 /** @const */ var separatorStr = '<>';
429 if (keyCode.toUpperCase() in MODIFIER_TO_CLASS) {
430 keyCode = keyCode.toUpperCase();
431 if (keyCode in modifiers) {
432 return modifiers.join(separatorStr);
434 var action = [keyCode].concat(modifiers);
436 return action.join(separatorStr);
439 return [keyCode].concat(modifiers).join(separatorStr);
443 * Returns a text which displayed on a key.
444 * @param {string} keyData Key glyph data.
445 * @return {string} Key text value.
447 function getKeyTextValue(keyData) {
449 // Do not show text on the space key.
450 if (keyData.label == 'space') {
453 // some key labels define actions such as 'mute' or 'vol. up'. Those actions
454 // should be localized (crbug.com/471025).
455 var localizedLabel = LABEL_TO_LOCALIZED_LABEL_ID[keyData.label];
457 return loadTimeData.getString(localizedLabel);
459 return keyData.label;
463 for (var j = 1; j <= 9; ++j) {
464 var pos = keyData['p' + j];
465 if (pos && pos.length > 0) {
466 chars.push(hex2char(pos));
469 return chars.join(' ');
473 * Updates the whole keyboard.
474 * @param {Array} modifiers Key Modifier list.
475 * @param {Array} normModifiers Key Modifier list ignoring the distinction
476 * between right and left keys.
478 function update(modifiers, normModifiers) {
479 var instructions = $('instructions');
480 if (modifiers.length == 0) {
481 instructions.style.visibility = 'visible';
483 instructions.style.visibility = 'hidden';
486 var keyboardGlyphData = getKeyboardGlyphData();
487 var shortcutData = getShortcutData();
488 var layout = getLayout();
489 for (var i = 0; i < layout.length; ++i) {
490 var identifier = remapIdentifier(layout[i][0]);
491 var keyData = keyboardGlyphData.keys[identifier];
492 var classes = getKeyClasses(identifier, modifiers, normModifiers);
493 var keyLabel = getKeyLabel(keyData, normModifiers);
494 var shortcutId = shortcutData[getAction(keyLabel, normModifiers)];
495 if (modifiers.length == 0 &&
496 (identifier == '2A' || identifier == '36')) {
497 // Either the right or left shift keys are used to disable the caps lock
498 // if it was enabled. To fix crbug.com/453623.
499 shortcutId = 'keyboardOverlayDisableCapsLock';
502 classes.push('is-shortcut');
505 var key = $(keyId(identifier, i));
506 key.className = classes.join(' ');
512 var keyText = $(keyTextId(identifier, i));
513 var keyTextValue = getKeyTextValue(keyData);
515 keyText.style.visibility = 'visible';
517 keyText.style.visibility = 'hidden';
519 keyText.textContent = keyTextValue;
521 var shortcutText = $(shortcutTextId(identifier, i));
523 shortcutText.style.visibility = 'visible';
524 shortcutText.textContent = loadTimeData.getString(shortcutId);
526 shortcutText.style.visibility = 'hidden';
529 var format = keyboardGlyphData.keys[layout[i][0]].format;
531 if (format == 'left' || format == 'right') {
532 shortcutText.style.textAlign = format;
533 keyText.style.textAlign = format;
540 * A callback function for onkeydown and onkeyup events.
541 * @param {Event} e Key event.
543 function handleKeyEvent(e) {
544 if (!getKeyboardOverlayId()) {
548 var modifiers = getModifiers(e);
550 // To avoid flickering as the user releases the modifier keys that were held
551 // to trigger the overlay, avoid updating in response to keyup events until at
552 // least one keydown event has been received.
554 if (e.type == 'keyup') {
556 } else if (e.type == 'keydown') {
561 var normModifiers = normalizeModifiers(modifiers);
562 update(modifiers, normModifiers);
563 KeyboardOverlayAccessibilityHelper.maybeSpeakAllShortcuts(normModifiers);
568 * Initializes the layout of the keys.
570 function initLayout() {
571 // Add data for the caps lock key
572 var keys = getKeyboardGlyphData().keys;
573 if (!('3A' in keys)) {
574 keys['3A'] = {label: 'caps lock', format: 'left'};
576 // Add data for the special key representing a disabled key
577 keys['DISABLED'] = {label: 'disabled', format: 'left'};
579 var layout = getLayout();
580 var keyboard = document.body;
581 var minX = window.innerWidth;
583 var minY = window.innerHeight;
585 var multiplier = 1.38 * window.innerWidth / BASE_KEYBOARD.width;
589 for (var i = 0; i < layout.length; i++) {
590 var array = layout[i];
591 var identifier = remapIdentifier(array[0]);
592 var x = Math.round((array[1] + offsetX) * multiplier);
593 var y = Math.round((array[2] + offsetY) * multiplier);
594 var w = Math.round((array[3] - keyMargin) * multiplier);
595 var h = Math.round((array[4] - keyMargin) * multiplier);
597 var key = document.createElement('div');
598 key.id = keyId(identifier, i);
599 key.className = 'keyboard-overlay-key';
600 key.style.left = x + 'px';
601 key.style.top = y + 'px';
602 key.style.width = w + 'px';
603 key.style.height = h + 'px';
605 var keyText = document.createElement('div');
606 keyText.id = keyTextId(identifier, i);
607 keyText.className = 'keyboard-overlay-key-text';
608 keyText.style.visibility = 'hidden';
609 key.appendChild(keyText);
611 var shortcutText = document.createElement('div');
612 shortcutText.id = shortcutTextId(identifier, i);
613 shortcutText.className = 'keyboard-overlay-shortcut-text';
614 shortcutText.style.visilibity = 'hidden';
615 key.appendChild(shortcutText);
616 keyboard.appendChild(key);
618 minX = Math.min(minX, x);
619 maxX = Math.max(maxX, x + w);
620 minY = Math.min(minY, y);
621 maxY = Math.max(maxY, y + h);
624 var width = maxX - minX + 1;
625 var height = maxY - minY + 1;
626 keyboard.style.width = (width + 2 * (minX + 1)) + 'px';
627 keyboard.style.height = (height + 2 * (minY + 1)) + 'px';
629 var instructions = document.createElement('div');
630 instructions.id = 'instructions';
631 instructions.className = 'keyboard-overlay-instructions';
632 instructions.style.left = ((BASE_INSTRUCTIONS.left - BASE_KEYBOARD.left) *
633 width / BASE_KEYBOARD.width + minX) + 'px';
634 instructions.style.top = ((BASE_INSTRUCTIONS.top - BASE_KEYBOARD.top) *
635 height / BASE_KEYBOARD.height + minY) + 'px';
636 instructions.style.width = (width * BASE_INSTRUCTIONS.width /
637 BASE_KEYBOARD.width) + 'px';
638 instructions.style.height = (height * BASE_INSTRUCTIONS.height /
639 BASE_KEYBOARD.height) + 'px';
641 var instructionsText = document.createElement('div');
642 instructionsText.id = 'instructions-text';
643 instructionsText.className = 'keyboard-overlay-instructions-text';
644 instructionsText.innerHTML =
645 loadTimeData.getString('keyboardOverlayInstructions');
646 instructions.appendChild(instructionsText);
647 var instructionsHideText = document.createElement('div');
648 instructionsHideText.id = 'instructions-hide-text';
649 instructionsHideText.className = 'keyboard-overlay-instructions-hide-text';
650 instructionsHideText.innerHTML =
651 loadTimeData.getString('keyboardOverlayInstructionsHide');
652 instructions.appendChild(instructionsHideText);
653 var learnMoreLinkText = document.createElement('div');
654 learnMoreLinkText.id = 'learn-more-text';
655 learnMoreLinkText.className = 'keyboard-overlay-learn-more-text';
656 learnMoreLinkText.addEventListener('click', learnMoreClicked);
657 var learnMoreLinkAnchor = document.createElement('a');
658 learnMoreLinkAnchor.href =
659 loadTimeData.getString('keyboardOverlayLearnMoreURL');
660 learnMoreLinkAnchor.textContent =
661 loadTimeData.getString('keyboardOverlayLearnMore');
662 learnMoreLinkText.appendChild(learnMoreLinkAnchor);
663 instructions.appendChild(learnMoreLinkText);
664 keyboard.appendChild(instructions);
668 * Returns true if the device has a diamond key.
669 * @return {boolean} Returns true if the device has a diamond key.
671 function hasDiamondKey() {
672 return loadTimeData.getBoolean('keyboardOverlayHasChromeOSDiamondKey');
676 * Returns true if display scaling feature is enabled.
677 * @return {boolean} True if display scaling feature is enabled.
679 function isDisplayUIScalingEnabled() {
680 return loadTimeData.getBoolean('keyboardOverlayIsDisplayUIScalingEnabled');
684 * Initializes the layout and the key labels for the keyboard that has a diamond
687 function initDiamondKey() {
688 var newLayoutData = {
689 '1D': [65.0, 287.0, 60.0, 60.0], // left Ctrl
690 '38': [185.0, 287.0, 60.0, 60.0], // left Alt
691 'E0 5B': [125.0, 287.0, 60.0, 60.0], // search
692 '3A': [5.0, 167.0, 105.0, 60.0], // caps lock
693 '5B': [803.0, 6.0, 72.0, 35.0], // lock key
694 '5D': [5.0, 287.0, 60.0, 60.0] // diamond key
697 var layout = getLayout();
698 var powerKeyIndex = -1;
699 var powerKeyId = '00';
700 for (var i = 0; i < layout.length; i++) {
701 var keyId = layout[i][0];
702 if (keyId in newLayoutData) {
703 layout[i] = [keyId].concat(newLayoutData[keyId]);
704 delete newLayoutData[keyId];
706 if (keyId == powerKeyId)
709 for (var keyId in newLayoutData)
710 layout.push([keyId].concat(newLayoutData[keyId]));
712 // Remove the power key.
713 if (powerKeyIndex != -1)
714 layout.splice(powerKeyIndex, 1);
716 var keyData = getKeyboardGlyphData()['keys'];
718 '3A': {'label': 'caps lock', 'format': 'left'},
719 '5B': {'label': 'lock'},
720 '5D': {'label': 'diamond', 'format': 'left'}
722 for (var keyId in newKeyData)
723 keyData[keyId] = newKeyData[keyId];
727 * A callback function for the onload event of the body element.
730 document.addEventListener('keydown', handleKeyEvent);
731 document.addEventListener('keyup', handleKeyEvent);
732 chrome.send('getLabelMap');
736 * Initializes the global map for remapping identifiers of modifier keys based
738 * Called after sending the 'getLabelMap' message.
739 * @param {Object} remap Identifier map.
741 function initIdentifierMap(remap) {
742 for (var key in remap) {
743 var val = remap[key];
744 if ((key in LABEL_TO_IDENTIFIER) &&
745 (val in LABEL_TO_IDENTIFIER)) {
746 identifierMap[LABEL_TO_IDENTIFIER[key]] =
747 LABEL_TO_IDENTIFIER[val];
749 console.error('Invalid label map element: ' + key + ', ' + val);
752 chrome.send('getInputMethodId');
756 * Initializes the global keyboad overlay ID and the layout of keys.
757 * Called after sending the 'getInputMethodId' message.
758 * @param {inputMethodId} inputMethodId Input Method Identifier.
760 function initKeyboardOverlayId(inputMethodId) {
761 // Libcros returns an empty string when it cannot find the keyboard overlay ID
762 // corresponding to the current input method.
763 // In such a case, fallback to the default ID (en_US).
764 var inputMethodIdToOverlayId =
765 keyboardOverlayData['inputMethodIdToOverlayId'];
767 if (inputMethodId.indexOf(IME_ID_PREFIX) == 0) {
768 // If the input method is a component extension IME, remove the prefix:
769 // _comp_ime_<ext_id>
770 // The extension id is a hash value with 32 characters.
771 inputMethodId = inputMethodId.slice(
772 IME_ID_PREFIX.length + EXTENSION_ID_LEN);
774 keyboardOverlayId = inputMethodIdToOverlayId[inputMethodId];
776 if (!keyboardOverlayId) {
777 console.error('No keyboard overlay ID for ' + inputMethodId);
778 keyboardOverlayId = 'en_US';
780 while (document.body.firstChild) {
781 document.body.removeChild(document.body.firstChild);
783 // We show Japanese layout as-is because the user has chosen the layout
784 // that is quite diffrent from the physical layout that has a diamond key.
785 if (hasDiamondKey() && getLayoutName() != 'J')
789 window.webkitRequestAnimationFrame(function() {
790 chrome.send('didPaint');
795 * Handles click events of the learn more link.
796 * @param {Event} e Mouse click event.
798 function learnMoreClicked(e) {
799 chrome.send('openLearnMorePage');
800 chrome.send('dialogClose');
804 document.addEventListener('DOMContentLoaded', init);