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
);