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. 4 5<include src="keyboard_overlay_data.js"> 6<include src="keyboard_overlay_accessibility_helper.js"> 7 8var BASE_KEYBOARD = { 9 top: 0, 10 left: 0, 11 width: 1237, 12 height: 514 13}; 14 15var BASE_INSTRUCTIONS = { 16 top: 194, 17 left: 370, 18 width: 498, 19 height: 142 20}; 21 22var MODIFIER_TO_CLASS = { 23 'SHIFT': 'modifier-shift', 24 'CTRL': 'modifier-ctrl', 25 'ALT': 'modifier-alt', 26 'SEARCH': 'modifier-search' 27}; 28 29var IDENTIFIER_TO_CLASS = { 30 '2A': 'is-shift', 31 '1D': 'is-ctrl', 32 '38': 'is-alt', 33 'E0 5B': 'is-search' 34}; 35 36var LABEL_TO_IDENTIFIER = { 37 'search': 'E0 5B', 38 'ctrl': '1D', 39 'alt': '38', 40 'caps lock': '3A', 41 'esc': '01', 42 'disabled': 'DISABLED' 43}; 44 45var KEYCODE_TO_LABEL = { 46 8: 'backspace', 47 9: 'tab', 48 13: 'enter', 49 27: 'esc', 50 32: 'space', 51 33: 'pageup', 52 34: 'pagedown', 53 35: 'end', 54 36: 'home', 55 37: 'left', 56 38: 'up', 57 39: 'right', 58 40: 'down', 59 46: 'delete', 60 91: 'search', 61 92: 'search', 62 96: '0', 63 97: '1', 64 98: '2', 65 99: '3', 66 100: '4', 67 101: '5', 68 102: '6', 69 103: '7', 70 104: '8', 71 105: '9', 72 106: '*', 73 107: '+', 74 109: '-', 75 110: '.', 76 111: '/', 77 112: 'back', 78 113: 'forward', 79 114: 'reload', 80 115: 'full screen', 81 116: 'switch window', 82 117: 'bright down', 83 118: 'bright up', 84 119: 'mute', 85 120: 'vol. down', 86 121: 'vol. up', 87 186: ';', 88 187: '+', 89 188: ',', 90 189: '-', 91 190: '.', 92 191: '/', 93 192: '`', 94 219: '[', 95 220: '\\', 96 221: ']', 97 222: '\'', 98}; 99 100var IME_ID_PREFIX = '_comp_ime_'; 101var EXTENSION_ID_LEN = 32; 102 103var keyboardOverlayId = 'en_US'; 104var identifierMap = {}; 105 106/** 107 * True after at least one keydown event has been received. 108 */ 109var gotKeyDown = false; 110 111/** 112 * Returns the layout name. 113 * @return {string} layout name. 114 */ 115function getLayoutName() { 116 return getKeyboardGlyphData().layoutName; 117} 118 119/** 120 * Returns layout data. 121 * @return {Array} Keyboard layout data. 122 */ 123function getLayout() { 124 return keyboardOverlayData['layouts'][getLayoutName()]; 125} 126 127// Cache the shortcut data after it is constructed. 128var shortcutDataCache; 129 130/** 131 * Returns shortcut data. 132 * @return {Object} Keyboard shortcut data. 133 */ 134function getShortcutData() { 135 if (shortcutDataCache) 136 return shortcutDataCache; 137 138 shortcutDataCache = keyboardOverlayData['shortcut']; 139 140 if (!isDisplayUIScalingEnabled()) { 141 // Zoom screen in 142 delete shortcutDataCache['+<>CTRL<>SHIFT']; 143 // Zoom screen out 144 delete shortcutDataCache['-<>CTRL<>SHIFT']; 145 // Reset screen zoom 146 delete shortcutDataCache['0<>CTRL<>SHIFT']; 147 } 148 149 return shortcutDataCache; 150} 151 152/** 153 * Returns the keyboard overlay ID. 154 * @return {string} Keyboard overlay ID. 155 */ 156function getKeyboardOverlayId() { 157 return keyboardOverlayId; 158} 159 160/** 161 * Returns keyboard glyph data. 162 * @return {Object} Keyboard glyph data. 163 */ 164function getKeyboardGlyphData() { 165 return keyboardOverlayData['keyboardGlyph'][getKeyboardOverlayId()]; 166} 167 168/** 169 * Converts a single hex number to a character. 170 * @param {string} hex Hexadecimal string. 171 * @return {string} Unicode values of hexadecimal string. 172 */ 173function hex2char(hex) { 174 if (!hex) { 175 return ''; 176 } 177 var result = ''; 178 var n = parseInt(hex, 16); 179 if (n <= 0xFFFF) { 180 result += String.fromCharCode(n); 181 } else if (n <= 0x10FFFF) { 182 n -= 0x10000; 183 result += (String.fromCharCode(0xD800 | (n >> 10)) + 184 String.fromCharCode(0xDC00 | (n & 0x3FF))); 185 } else { 186 console.error('hex2Char error: Code point out of range :' + hex); 187 } 188 return result; 189} 190 191var searchIsPressed = false; 192 193/** 194 * Returns a list of modifiers from the key event. 195 * @param {Event} e The key event. 196 * @return {Array} List of modifiers based on key event. 197 */ 198function getModifiers(e) { 199 if (!e) 200 return []; 201 202 var isKeyDown = (e.type == 'keydown'); 203 var keyCodeToModifier = { 204 16: 'SHIFT', 205 17: 'CTRL', 206 18: 'ALT', 207 91: 'SEARCH', 208 }; 209 var modifierWithKeyCode = keyCodeToModifier[e.keyCode]; 210 var isPressed = { 211 'SHIFT': e.shiftKey, 212 'CTRL': e.ctrlKey, 213 'ALT': e.altKey, 214 'SEARCH': searchIsPressed 215 }; 216 if (modifierWithKeyCode) 217 isPressed[modifierWithKeyCode] = isKeyDown; 218 219 searchIsPressed = isPressed['SEARCH']; 220 221 // make the result array 222 return ['SHIFT', 'CTRL', 'ALT', 'SEARCH'].filter( 223 function(modifier) { 224 return isPressed[modifier]; 225 }).sort(); 226} 227 228/** 229 * Returns an ID of the key. 230 * @param {string} identifier Key identifier. 231 * @param {number} i Key number. 232 * @return {string} Key ID. 233 */ 234function keyId(identifier, i) { 235 return identifier + '-key-' + i; 236} 237 238/** 239 * Returns an ID of the text on the key. 240 * @param {string} identifier Key identifier. 241 * @param {number} i Key number. 242 * @return {string} Key text ID. 243 */ 244function keyTextId(identifier, i) { 245 return identifier + '-key-text-' + i; 246} 247 248/** 249 * Returns an ID of the shortcut text. 250 * @param {string} identifier Key identifier. 251 * @param {number} i Key number. 252 * @return {string} Key shortcut text ID. 253 */ 254function shortcutTextId(identifier, i) { 255 return identifier + '-shortcut-text-' + i; 256} 257 258/** 259 * Returns true if |list| contains |e|. 260 * @param {Array} list Container list. 261 * @param {string} e Element string. 262 * @return {boolean} Returns true if the list contains the element. 263 */ 264function contains(list, e) { 265 return list.indexOf(e) != -1; 266} 267 268/** 269 * Returns a list of the class names corresponding to the identifier and 270 * modifiers. 271 * @param {string} identifier Key identifier. 272 * @param {Array} modifiers List of key modifiers. 273 * @return {Array} List of class names corresponding to specified params. 274 */ 275function getKeyClasses(identifier, modifiers) { 276 var classes = ['keyboard-overlay-key']; 277 for (var i = 0; i < modifiers.length; ++i) { 278 classes.push(MODIFIER_TO_CLASS[modifiers[i]]); 279 } 280 281 if ((identifier == '2A' && contains(modifiers, 'SHIFT')) || 282 (identifier == '1D' && contains(modifiers, 'CTRL')) || 283 (identifier == '38' && contains(modifiers, 'ALT')) || 284 (identifier == 'E0 5B' && contains(modifiers, 'SEARCH'))) { 285 classes.push('pressed'); 286 classes.push(IDENTIFIER_TO_CLASS[identifier]); 287 } 288 return classes; 289} 290 291/** 292 * Returns true if a character is a ASCII character. 293 * @param {string} c A character to be checked. 294 * @return {boolean} True if the character is an ASCII character. 295 */ 296function isAscii(c) { 297 var charCode = c.charCodeAt(0); 298 return 0x00 <= charCode && charCode <= 0x7F; 299} 300 301/** 302 * Returns a remapped identiifer based on the preference. 303 * @param {string} identifier Key identifier. 304 * @return {string} Remapped identifier. 305 */ 306function remapIdentifier(identifier) { 307 return identifierMap[identifier] || identifier; 308} 309 310/** 311 * Returns a label of the key. 312 * @param {string} keyData Key glyph data. 313 * @param {Array} modifiers Key Modifier list. 314 * @return {string} Label of the key. 315 */ 316function getKeyLabel(keyData, modifiers) { 317 if (!keyData) { 318 return ''; 319 } 320 if (keyData.label) { 321 return keyData.label; 322 } 323 var keyLabel = ''; 324 for (var j = 1; j <= 9; j++) { 325 var pos = keyData['p' + j]; 326 if (!pos) { 327 continue; 328 } 329 keyLabel = hex2char(pos); 330 if (!keyLabel) { 331 continue; 332 } 333 if (isAscii(keyLabel) && 334 getShortcutData()[getAction(keyLabel, modifiers)]) { 335 break; 336 } 337 } 338 return keyLabel; 339} 340 341/** 342 * Returns a normalized string used for a key of shortcutData. 343 * 344 * Examples: 345 * keyCode: 'd', modifiers: ['CTRL', 'SHIFT'] => 'd<>CTRL<>SHIFT' 346 * keyCode: 'alt', modifiers: ['ALT', 'SHIFT'] => 'ALT<>SHIFT' 347 * 348 * @param {string} keyCode Key code. 349 * @param {Array} modifiers Key Modifier list. 350 * @return {string} Normalized key shortcut data string. 351 */ 352function getAction(keyCode, modifiers) { 353 /** @const */ var separatorStr = '<>'; 354 if (keyCode.toUpperCase() in MODIFIER_TO_CLASS) { 355 keyCode = keyCode.toUpperCase(); 356 if (keyCode in modifiers) { 357 return modifiers.join(separatorStr); 358 } else { 359 var action = [keyCode].concat(modifiers); 360 action.sort(); 361 return action.join(separatorStr); 362 } 363 } 364 return [keyCode].concat(modifiers).join(separatorStr); 365} 366 367/** 368 * Returns a text which displayed on a key. 369 * @param {string} keyData Key glyph data. 370 * @return {string} Key text value. 371 */ 372function getKeyTextValue(keyData) { 373 if (keyData.label) { 374 // Do not show text on the space key. 375 if (keyData.label == 'space') { 376 return ''; 377 } 378 return keyData.label; 379 } 380 381 var chars = []; 382 for (var j = 1; j <= 9; ++j) { 383 var pos = keyData['p' + j]; 384 if (pos && pos.length > 0) { 385 chars.push(hex2char(pos)); 386 } 387 } 388 return chars.join(' '); 389} 390 391/** 392 * Updates the whole keyboard. 393 * @param {Array} modifiers Key Modifier list. 394 */ 395function update(modifiers) { 396 var instructions = $('instructions'); 397 if (modifiers.length == 0) { 398 instructions.style.visibility = 'visible'; 399 } else { 400 instructions.style.visibility = 'hidden'; 401 } 402 403 var keyboardGlyphData = getKeyboardGlyphData(); 404 var shortcutData = getShortcutData(); 405 var layout = getLayout(); 406 for (var i = 0; i < layout.length; ++i) { 407 var identifier = remapIdentifier(layout[i][0]); 408 var keyData = keyboardGlyphData.keys[identifier]; 409 var classes = getKeyClasses(identifier, modifiers, keyData); 410 var keyLabel = getKeyLabel(keyData, modifiers); 411 var shortcutId = shortcutData[getAction(keyLabel, modifiers)]; 412 if (modifiers.length == 1 && modifiers[0] == 'SHIFT' && 413 identifier == '2A') { 414 // Currently there is no way to identify whether the left shift or the 415 // right shift is preesed from the key event, so I assume the left shift 416 // key is pressed here and do not show keyboard shortcut description for 417 // 'Shift - Shift' (Toggle caps lock) on the left shift key, the 418 // identifier of which is '2A'. 419 // TODO(mazda): Remove this workaround (http://crosbug.com/18047) 420 shortcutId = null; 421 } 422 if (shortcutId) { 423 classes.push('is-shortcut'); 424 } 425 426 var key = $(keyId(identifier, i)); 427 key.className = classes.join(' '); 428 429 if (!keyData) { 430 continue; 431 } 432 433 var keyText = $(keyTextId(identifier, i)); 434 var keyTextValue = getKeyTextValue(keyData); 435 if (keyTextValue) { 436 keyText.style.visibility = 'visible'; 437 } else { 438 keyText.style.visibility = 'hidden'; 439 } 440 keyText.textContent = keyTextValue; 441 442 var shortcutText = $(shortcutTextId(identifier, i)); 443 if (shortcutId) { 444 shortcutText.style.visibility = 'visible'; 445 shortcutText.textContent = loadTimeData.getString(shortcutId); 446 } else { 447 shortcutText.style.visibility = 'hidden'; 448 } 449 450 var format = keyboardGlyphData.keys[layout[i][0]].format; 451 if (format) { 452 if (format == 'left' || format == 'right') { 453 shortcutText.style.textAlign = format; 454 keyText.style.textAlign = format; 455 } 456 } 457 } 458} 459 460/** 461 * A callback function for onkeydown and onkeyup events. 462 * @param {Event} e Key event. 463 */ 464function handleKeyEvent(e) { 465 if (!getKeyboardOverlayId()) { 466 return; 467 } 468 469 // To avoid flickering as the user releases the modifier keys that were held 470 // to trigger the overlay, avoid updating in response to keyup events until at 471 // least one keydown event has been received. 472 if (!gotKeyDown) { 473 if (e.type == 'keyup') { 474 return; 475 } else if (e.type == 'keydown') { 476 gotKeyDown = true; 477 } 478 } 479 480 var modifiers = getModifiers(e); 481 update(modifiers); 482 KeyboardOverlayAccessibilityHelper.maybeSpeakAllShortcuts(modifiers); 483 e.preventDefault(); 484} 485 486/** 487 * Initializes the layout of the keys. 488 */ 489function initLayout() { 490 // Add data for the caps lock key 491 var keys = getKeyboardGlyphData().keys; 492 if (!('3A' in keys)) { 493 keys['3A'] = {label: 'caps lock', format: 'left'}; 494 } 495 // Add data for the special key representing a disabled key 496 keys['DISABLED'] = {label: 'disabled', format: 'left'}; 497 498 var layout = getLayout(); 499 var keyboard = document.body; 500 var minX = window.innerWidth; 501 var maxX = 0; 502 var minY = window.innerHeight; 503 var maxY = 0; 504 var multiplier = 1.38 * window.innerWidth / BASE_KEYBOARD.width; 505 var keyMargin = 7; 506 var offsetX = 10; 507 var offsetY = 7; 508 for (var i = 0; i < layout.length; i++) { 509 var array = layout[i]; 510 var identifier = remapIdentifier(array[0]); 511 var x = Math.round((array[1] + offsetX) * multiplier); 512 var y = Math.round((array[2] + offsetY) * multiplier); 513 var w = Math.round((array[3] - keyMargin) * multiplier); 514 var h = Math.round((array[4] - keyMargin) * multiplier); 515 516 var key = document.createElement('div'); 517 key.id = keyId(identifier, i); 518 key.className = 'keyboard-overlay-key'; 519 key.style.left = x + 'px'; 520 key.style.top = y + 'px'; 521 key.style.width = w + 'px'; 522 key.style.height = h + 'px'; 523 524 var keyText = document.createElement('div'); 525 keyText.id = keyTextId(identifier, i); 526 keyText.className = 'keyboard-overlay-key-text'; 527 keyText.style.visibility = 'hidden'; 528 key.appendChild(keyText); 529 530 var shortcutText = document.createElement('div'); 531 shortcutText.id = shortcutTextId(identifier, i); 532 shortcutText.className = 'keyboard-overlay-shortcut-text'; 533 shortcutText.style.visilibity = 'hidden'; 534 key.appendChild(shortcutText); 535 keyboard.appendChild(key); 536 537 minX = Math.min(minX, x); 538 maxX = Math.max(maxX, x + w); 539 minY = Math.min(minY, y); 540 maxY = Math.max(maxY, y + h); 541 } 542 543 var width = maxX - minX + 1; 544 var height = maxY - minY + 1; 545 keyboard.style.width = (width + 2 * (minX + 1)) + 'px'; 546 keyboard.style.height = (height + 2 * (minY + 1)) + 'px'; 547 548 var instructions = document.createElement('div'); 549 instructions.id = 'instructions'; 550 instructions.className = 'keyboard-overlay-instructions'; 551 instructions.style.left = ((BASE_INSTRUCTIONS.left - BASE_KEYBOARD.left) * 552 width / BASE_KEYBOARD.width + minX) + 'px'; 553 instructions.style.top = ((BASE_INSTRUCTIONS.top - BASE_KEYBOARD.top) * 554 height / BASE_KEYBOARD.height + minY) + 'px'; 555 instructions.style.width = (width * BASE_INSTRUCTIONS.width / 556 BASE_KEYBOARD.width) + 'px'; 557 instructions.style.height = (height * BASE_INSTRUCTIONS.height / 558 BASE_KEYBOARD.height) + 'px'; 559 560 var instructionsText = document.createElement('div'); 561 instructionsText.id = 'instructions-text'; 562 instructionsText.className = 'keyboard-overlay-instructions-text'; 563 instructionsText.innerHTML = 564 loadTimeData.getString('keyboardOverlayInstructions'); 565 instructions.appendChild(instructionsText); 566 var instructionsHideText = document.createElement('div'); 567 instructionsHideText.id = 'instructions-hide-text'; 568 instructionsHideText.className = 'keyboard-overlay-instructions-hide-text'; 569 instructionsHideText.innerHTML = 570 loadTimeData.getString('keyboardOverlayInstructionsHide'); 571 instructions.appendChild(instructionsHideText); 572 var learnMoreLinkText = document.createElement('div'); 573 learnMoreLinkText.id = 'learn-more-text'; 574 learnMoreLinkText.className = 'keyboard-overlay-learn-more-text'; 575 learnMoreLinkText.addEventListener('click', learnMoreClicked); 576 var learnMoreLinkAnchor = document.createElement('a'); 577 learnMoreLinkAnchor.href = 578 loadTimeData.getString('keyboardOverlayLearnMoreURL'); 579 learnMoreLinkAnchor.textContent = 580 loadTimeData.getString('keyboardOverlayLearnMore'); 581 learnMoreLinkText.appendChild(learnMoreLinkAnchor); 582 instructions.appendChild(learnMoreLinkText); 583 keyboard.appendChild(instructions); 584} 585 586/** 587 * Returns true if the device has a diamond key. 588 * @return {boolean} Returns true if the device has a diamond key. 589 */ 590function hasDiamondKey() { 591 return loadTimeData.getBoolean('keyboardOverlayHasChromeOSDiamondKey'); 592} 593 594/** 595 * Returns true if display scaling feature is enabled. 596 * @return {boolean} True if display scaling feature is enabled. 597 */ 598function isDisplayUIScalingEnabled() { 599 return loadTimeData.getBoolean('keyboardOverlayIsDisplayUIScalingEnabled'); 600} 601 602/** 603 * Initializes the layout and the key labels for the keyboard that has a diamond 604 * key. 605 */ 606function initDiamondKey() { 607 var newLayoutData = { 608 '1D': [65.0, 287.0, 60.0, 60.0], // left Ctrl 609 '38': [185.0, 287.0, 60.0, 60.0], // left Alt 610 'E0 5B': [125.0, 287.0, 60.0, 60.0], // search 611 '3A': [5.0, 167.0, 105.0, 60.0], // caps lock 612 '5B': [803.0, 6.0, 72.0, 35.0], // lock key 613 '5D': [5.0, 287.0, 60.0, 60.0] // diamond key 614 }; 615 616 var layout = getLayout(); 617 var powerKeyIndex = -1; 618 var powerKeyId = '00'; 619 for (var i = 0; i < layout.length; i++) { 620 var keyId = layout[i][0]; 621 if (keyId in newLayoutData) { 622 layout[i] = [keyId].concat(newLayoutData[keyId]); 623 delete newLayoutData[keyId]; 624 } 625 if (keyId == powerKeyId) 626 powerKeyIndex = i; 627 } 628 for (var keyId in newLayoutData) 629 layout.push([keyId].concat(newLayoutData[keyId])); 630 631 // Remove the power key. 632 if (powerKeyIndex != -1) 633 layout.splice(powerKeyIndex, 1); 634 635 var keyData = getKeyboardGlyphData()['keys']; 636 var newKeyData = { 637 '3A': {'label': 'caps lock', 'format': 'left'}, 638 '5B': {'label': 'lock'}, 639 '5D': {'label': 'diamond', 'format': 'left'} 640 }; 641 for (var keyId in newKeyData) 642 keyData[keyId] = newKeyData[keyId]; 643} 644 645/** 646 * A callback function for the onload event of the body element. 647 */ 648function init() { 649 document.addEventListener('keydown', handleKeyEvent); 650 document.addEventListener('keyup', handleKeyEvent); 651 chrome.send('getLabelMap'); 652} 653 654/** 655 * Initializes the global map for remapping identifiers of modifier keys based 656 * on the preference. 657 * Called after sending the 'getLabelMap' message. 658 * @param {Object} remap Identifier map. 659 */ 660function initIdentifierMap(remap) { 661 for (var key in remap) { 662 var val = remap[key]; 663 if ((key in LABEL_TO_IDENTIFIER) && 664 (val in LABEL_TO_IDENTIFIER)) { 665 identifierMap[LABEL_TO_IDENTIFIER[key]] = 666 LABEL_TO_IDENTIFIER[val]; 667 } else { 668 console.error('Invalid label map element: ' + key + ', ' + val); 669 } 670 } 671 chrome.send('getInputMethodId'); 672} 673 674/** 675 * Initializes the global keyboad overlay ID and the layout of keys. 676 * Called after sending the 'getInputMethodId' message. 677 * @param {inputMethodId} inputMethodId Input Method Identifier. 678 */ 679function initKeyboardOverlayId(inputMethodId) { 680 // Libcros returns an empty string when it cannot find the keyboard overlay ID 681 // corresponding to the current input method. 682 // In such a case, fallback to the default ID (en_US). 683 var inputMethodIdToOverlayId = 684 keyboardOverlayData['inputMethodIdToOverlayId']; 685 if (inputMethodId) { 686 if (inputMethodId.indexOf(IME_ID_PREFIX) == 0) { 687 // If the input method is a component extension IME, remove the prefix: 688 // _comp_ime_<ext_id> 689 // The extension id is a hash value with 32 characters. 690 inputMethodId = inputMethodId.slice( 691 IME_ID_PREFIX.length + EXTENSION_ID_LEN); 692 } 693 keyboardOverlayId = inputMethodIdToOverlayId[inputMethodId]; 694 } 695 if (!keyboardOverlayId) { 696 console.error('No keyboard overlay ID for ' + inputMethodId); 697 keyboardOverlayId = 'en_US'; 698 } 699 while (document.body.firstChild) { 700 document.body.removeChild(document.body.firstChild); 701 } 702 // We show Japanese layout as-is because the user has chosen the layout 703 // that is quite diffrent from the physical layout that has a diamond key. 704 if (hasDiamondKey() && getLayoutName() != 'J') 705 initDiamondKey(); 706 initLayout(); 707 update([]); 708 window.webkitRequestAnimationFrame(function() { 709 chrome.send('didPaint'); 710 }); 711} 712 713/** 714 * Handles click events of the learn more link. 715 * @param {Event} e Mouse click event. 716 */ 717function learnMoreClicked(e) { 718 chrome.send('openLearnMorePage'); 719 chrome.send('dialogClose'); 720 e.preventDefault(); 721} 722 723document.addEventListener('DOMContentLoaded', init); 724