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