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