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 keyboardOverlayId = 'en_US';
101var identifierMap = {};
102
103/**
104 * Returns the layout name.
105 * @return {string} layout name.
106 */
107function getLayoutName() {
108  return getKeyboardGlyphData().layoutName;
109}
110
111/**
112 * Returns layout data.
113 * @return {Array} Keyboard layout data.
114 */
115function getLayout() {
116  return keyboardOverlayData['layouts'][getLayoutName()];
117}
118
119// Cache the shortcut data after it is constructed.
120var shortcutDataCache;
121
122/**
123 * Returns shortcut data.
124 * @return {Object} Keyboard shortcut data.
125 */
126function 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 */
148function getKeyboardOverlayId() {
149  return keyboardOverlayId;
150}
151
152/**
153 * Returns keyboard glyph data.
154 * @return {Object} Keyboard glyph data.
155 */
156function 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 */
165function 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
183var 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 */
190function 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 */
226function 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 */
236function 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 */
246function 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 */
256function 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 */
267function 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 */
288function 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 */
298function 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 */
308function 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 */
344function 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 */
364function 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 */
387function 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 */
456function 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 */
469function 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 */
570function 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 */
578function 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 */
586function 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 */
628function 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 */
640function 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 */
659function 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 */
690function learnMoreClicked(e) {
691  chrome.send('openLearnMorePage');
692  chrome.send('DialogClose');
693  e.preventDefault();
694}
695
696document.addEventListener('DOMContentLoaded', init);
697