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