1// Copyright (c) 2011 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
5var BASE_KEYBOARD = {
6  top: 0,
7  left: 0,
8  width: 1237,
9  height: 514
10};
11
12var BASE_INSTRUCTIONS = {
13  top: 194,
14  left: 370,
15  width: 498,
16  height: 112
17};
18
19var LABEL_TO_KEY_TEXT = {
20  alt: 'alt',
21  backspace: 'backspace',
22  ctrl: 'ctrl',
23  enter: 'enter',
24  esc: 'esc',
25  glyph_arrow_down: 'down',
26  glyph_arrow_left: 'left',
27  glyph_arrow_right: 'right',
28  glyph_arrow_up: 'up',
29  glyph_back: 'back',
30  glyph_backspace: 'backspace',
31  glyph_brightness_down: 'bright down',
32  glyph_brightness_up: 'bright up',
33  glyph_enter: 'enter',
34  glyph_forward: 'forward',
35  glyph_fullscreen: 'full screen',
36  glyph_ime: 'ime',
37  glyph_lock: 'lock',
38  glyph_overview: 'switch window',
39  glyph_power: 'power',
40  glyph_right: 'right',
41  glyph_reload: 'reload',
42  glyph_search: 'search',
43  glyph_shift: 'shift',
44  glyph_tab: 'tab',
45  glyph_tools: 'tools',
46  glyph_volume_down: 'vol. down',
47  glyph_volume_mute: 'mute',
48  glyph_volume_up: 'vol. up',
49  shift: 'shift',
50  tab: 'tab'
51};
52
53var MODIFIER_TO_CLASS = {
54  'SHIFT': 'modifier-shift',
55  'CTRL': 'modifier-ctrl',
56  'ALT': 'modifier-alt'
57};
58
59var IDENTIFIER_TO_CLASS = {
60  '2A': 'is-shift',
61  '1D': 'is-ctrl',
62  '38': 'is-alt'
63};
64
65var keyboardOverlayId = 'en_US';
66
67/**
68 * Returns layouts data.
69 */
70function getLayouts() {
71  return keyboardOverlayData['layouts'];
72}
73
74/**
75 * Returns shortcut data.
76 */
77function getShortcutData() {
78  return keyboardOverlayData['shortcut'];
79}
80
81/**
82 * Returns the keyboard overlay ID.
83 */
84function getKeyboardOverlayId() {
85  return keyboardOverlayId
86}
87
88/**
89 * Returns keyboard glyph data.
90 */
91function getKeyboardGlyphData() {
92  return keyboardOverlayData['keyboardGlyph'][getKeyboardOverlayId()];
93}
94
95/**
96 * Converts a single hex number to a character.
97 */
98function hex2char(hex) {
99  if (!hex) {
100    return '';
101  }
102  var result = '';
103  var n = parseInt(hex, 16);
104  if (n <= 0xFFFF) {
105    result += String.fromCharCode(n);
106  } else if (n <= 0x10FFFF) {
107    n -= 0x10000;
108    result += (String.fromCharCode(0xD800 | (n >> 10)) +
109               String.fromCharCode(0xDC00 | (n & 0x3FF)));
110  } else {
111    console.error('hex2Char error: Code point out of range :' + hex);
112  }
113  return result;
114}
115
116/**
117 * Returns a list of modifiers from the key event.
118 */
119function getModifiers(e) {
120  if (!e) {
121    return [];
122  }
123  var isKeyDown = (e.type == 'keydown');
124  var keyCodeToModifier = {
125    16: 'SHIFT',
126    17: 'CTRL',
127    18: 'ALT',
128    91: 'ALT', // left ALT pressed with SHIFT
129    92: 'ALT', // right ALT pressed with SHIFT
130  };
131  var modifierWithKeyCode = keyCodeToModifier[e.keyCode];
132  var isPressed = {'SHIFT': e.shiftKey, 'CTRL': e.ctrlKey, 'ALT': e.altKey};
133  // if e.keyCode is one of Shift, Ctrl and Alt, isPressed should
134  // be changed because the key currently pressed
135  // does not affect the values of e.shiftKey, e.ctrlKey and e.altKey
136  if(modifierWithKeyCode){
137    isPressed[modifierWithKeyCode] = isKeyDown;
138  }
139  // make the result array
140  return ['SHIFT', 'CTRL', 'ALT'].filter(
141      function(modifier) {
142        return isPressed[modifier];
143      }).sort();
144}
145
146/**
147 * Returns an ID of the key.
148 */
149function keyId(identifier, i) {
150  return identifier + '-key-' + i;
151}
152
153/**
154 * Returns an ID of the text on the key.
155 */
156function keyTextId(identifier, i) {
157  return identifier + '-key-text-' + i;
158}
159
160/**
161 * Returns an ID of the shortcut text.
162 */
163function shortcutTextId(identifier, i) {
164  return identifier + '-shortcut-text-' + i;
165}
166
167/**
168 * Returns true if |list| contains |e|.
169 */
170function contains(list, e) {
171  return list.indexOf(e) != -1;
172}
173
174/**
175 * Returns a list of the class names corresponding to the identifier and
176 * modifiers.
177 */
178function getKeyClasses(identifier, modifiers) {
179  var classes = ['keyboard-overlay-key'];
180  for (var i = 0; i < modifiers.length; ++i) {
181    classes.push(MODIFIER_TO_CLASS[modifiers[i]]);
182  }
183
184  if ((identifier == '2A' && contains(modifiers, 'SHIFT')) ||
185      (identifier == '1D' && contains(modifiers, 'CTRL')) ||
186      (identifier == '38' && contains(modifiers, 'ALT'))) {
187    classes.push('pressed');
188    classes.push(IDENTIFIER_TO_CLASS[identifier]);
189  }
190  return classes;
191}
192
193/**
194 * Returns true if a character is a ASCII character.
195 */
196function isAscii(c) {
197  var charCode = c.charCodeAt(0);
198  return 0x00 <= charCode && charCode <= 0x7F;
199}
200
201/**
202 * Returns a label of the key.
203 */
204function getKeyLabel(keyData, modifiers) {
205  if (!keyData) {
206    return '';
207  }
208  if (keyData.label in LABEL_TO_KEY_TEXT) {
209    return LABEL_TO_KEY_TEXT[keyData.label];
210  }
211  var keyLabel = '';
212  for (var j = 1; j <= 9; j++) {
213    var pos =  keyData['p' + j];
214    if (!pos) {
215      continue;
216    }
217    if (LABEL_TO_KEY_TEXT[pos]) {
218      return LABEL_TO_KEY_TEXT[pos];
219    }
220    keyLabel = hex2char(pos);
221    if (!keyLabel) {
222      continue;
223     }
224    if (isAscii(keyLabel) &&
225        getShortcutData()[getAction(keyLabel, modifiers)]) {
226      break;
227    }
228  }
229  return keyLabel;
230}
231
232/**
233 * Returns a normalized string used for a key of shortcutData.
234 *
235 * Examples:
236 *   keycode: 'd', modifiers: ['CTRL', 'SHIFT'] => 'd<>CTRL<>SHIFT'
237 *   keycode: 'alt', modifiers: ['ALT', 'SHIFT'] => 'ALT<>SHIFT'
238 */
239function getAction(keycode, modifiers) {
240  const SEPARATOR = '<>';
241  if (keycode.toUpperCase() in MODIFIER_TO_CLASS) {
242    keycode = keycode.toUpperCase();
243    if (keycode in modifiers) {
244      return modifiers.join(SEPARATOR);
245    } else {
246      var action = [keycode].concat(modifiers)
247      action.sort();
248      return action.join(SEPARATOR);
249    }
250  }
251  return [keycode].concat(modifiers).join(SEPARATOR);
252}
253
254/**
255 * Returns a text which displayed on a key.
256 */
257function getKeyTextValue(keyData) {
258  if (LABEL_TO_KEY_TEXT[keyData.label]) {
259    return LABEL_TO_KEY_TEXT[keyData.label];
260  }
261
262  var chars = [];
263  for (var j = 1; j <= 9; ++j) {
264    var pos = keyData['p' + j];
265    if (LABEL_TO_KEY_TEXT[pos]) {
266      return LABEL_TO_KEY_TEXT[pos];
267    }
268    if (pos && pos.length > 0) {
269      chars.push(hex2char(pos));
270    }
271  }
272  return chars.join(' ');
273}
274
275/**
276 * Updates the whole keyboard.
277 */
278function update(modifiers) {
279  var instructions = document.getElementById('instructions');
280  if (modifiers.length == 0) {
281    instructions.style.visibility = 'visible';
282  } else {
283    instructions.style.visibility = 'hidden';
284  }
285
286  var keyboardGlyphData = getKeyboardGlyphData();
287  var shortcutData = getShortcutData();
288  var layout = getLayouts()[keyboardGlyphData.layoutName];
289  for (var i = 0; i < layout.length; ++i) {
290    var identifier = layout[i][0];
291    var keyData = keyboardGlyphData.keys[identifier];
292    var classes = getKeyClasses(identifier, modifiers, keyData);
293    var keyLabel = getKeyLabel(keyData, modifiers);
294    var shortcutId = shortcutData[getAction(keyLabel, modifiers)];
295    if (shortcutId) {
296      classes.push('is-shortcut');
297    }
298
299    var key = document.getElementById(keyId(identifier, i));
300    key.className = classes.join(' ');
301
302    if (!keyData) {
303      continue;
304    }
305
306    var keyText = document.getElementById(keyTextId(identifier, i));
307    var keyTextValue = getKeyTextValue(keyData);
308    if (keyTextValue) {
309       keyText.style.visibility = 'visible';
310    } else {
311       keyText.style.visibility = 'hidden';
312    }
313    keyText.textContent = keyTextValue;
314
315    var shortcutText = document.getElementById(shortcutTextId(identifier, i));
316    if (shortcutId) {
317      shortcutText.style.visibility = 'visible';
318      shortcutText.textContent = templateData[shortcutId];
319    } else {
320      shortcutText.style.visibility = 'hidden';
321    }
322
323    if (keyData.format) {
324      var format = keyData.format;
325      if (format == 'left' || format == 'right') {
326        shortcutText.style.textAlign = format;
327        keyText.style.textAlign = format;
328      }
329    }
330  }
331}
332
333/**
334 * A callback function for onkeydown and onkeyup events.
335 */
336function handleKeyEvent(e){
337  var modifiers = getModifiers(e);
338  if (!getKeyboardOverlayId()) {
339    return;
340  }
341  update(modifiers);
342}
343
344/**
345 * Initializes the layout of the keys.
346 */
347function initLayout() {
348  var layout = getLayouts()[getKeyboardGlyphData().layoutName];
349  var keyboard = document.body;
350  var minX = window.innerWidth;
351  var maxX = 0;
352  var minY = window.innerHeight;
353  var maxY = 0;
354  var multiplier = 1.38 * window.innerWidth / BASE_KEYBOARD.width;
355  var keyMargin = 7;
356  var offsetX = 10;
357  var offsetY = 7;
358  for (var i = 0; i < layout.length; i++) {
359    var array = layout[i];
360    var identifier = array[0];
361    var x = Math.round((array[1] + offsetX) * multiplier);
362    var y = Math.round((array[2] + offsetY) * multiplier);
363    var w = Math.round((array[3] - keyMargin) * multiplier);
364    var h = Math.round((array[4] - keyMargin) * multiplier);
365
366    var key = document.createElement('div');
367    key.id = keyId(identifier, i);
368    key.className = 'keyboard-overlay-key';
369    key.style.left = x + 'px';
370    key.style.top = y + 'px';
371    key.style.width = w + 'px';
372    key.style.height = h + 'px';
373
374    var keyText = document.createElement('div');
375    keyText.id = keyTextId(identifier, i);
376    keyText.className = 'keyboard-overlay-key-text';
377    keyText.style.visibility = 'hidden';
378    key.appendChild(keyText);
379
380    var shortcutText = document.createElement('div');
381    shortcutText.id = shortcutTextId(identifier, i);
382    shortcutText.className = 'keyboard-overlay-shortcut-text';
383    shortcutText.style.visilibity = 'hidden';
384    key.appendChild(shortcutText);
385    keyboard.appendChild(key);
386
387    minX = Math.min(minX, x);
388    maxX = Math.max(maxX, x + w);
389    minY = Math.min(minY, y);
390    maxY = Math.max(maxY, y + h);
391  }
392
393  var width = maxX - minX + 1;
394  var height = maxY - minY + 1;
395  keyboard.style.width = (width + 2 * (minX + 1)) + 'px';
396  keyboard.style.height = (height + 2 * (minY + 1)) + 'px';
397
398  var instructions = document.createElement('div');
399  instructions.id = 'instructions';
400  instructions.className = 'keyboard-overlay-instructions';
401  instructions.style.left = ((BASE_INSTRUCTIONS.left - BASE_KEYBOARD.left) *
402                             width / BASE_KEYBOARD.width + minX) + 'px';
403  instructions.style.top = ((BASE_INSTRUCTIONS.top - BASE_KEYBOARD.top) *
404                            height / BASE_KEYBOARD.height + minY) + 'px';
405  instructions.style.width = (width * BASE_INSTRUCTIONS.width /
406                              BASE_KEYBOARD.width) + 'px';
407  instructions.style.height = (height * BASE_INSTRUCTIONS.height /
408                               BASE_KEYBOARD.height) + 'px';
409
410  var instructionsText = document.createElement('div');
411  instructionsText.id = 'instructions-text';
412  instructionsText.className = 'keyboard-overlay-instructions-text';
413  instructionsText.innerHTML = templateData.keyboardOverlayInstructions;
414  instructions.appendChild(instructionsText);
415  var instructionsHideText = document.createElement('div');
416  instructionsHideText.id = 'instructions-hide-text';
417  instructionsHideText.className = 'keyboard-overlay-instructions-hide-text';
418  instructionsHideText.innerHTML = templateData.keyboardOverlayInstructionsHide;
419  instructions.appendChild(instructionsHideText);
420  keyboard.appendChild(instructions);
421}
422
423/**
424 * A callback function for the onload event of the body element.
425 */
426function init() {
427  document.addEventListener('keydown', handleKeyEvent);
428  document.addEventListener('keyup', handleKeyEvent);
429  chrome.send('getKeyboardOverlayId');
430}
431
432/**
433 * Initializes the global keyboad overlay ID and the layout of keys.
434 * Called after sending the 'getKeyboardOverlayId' message.
435 */
436function initKeyboardOverlayId(overlayId) {
437  // Libcros returns an empty string when it cannot find the keyboard overlay ID
438  // corresponding to the current input method.
439  // In such a case, fallback to the default ID (en_US).
440  if (overlayId) {
441    keyboardOverlayId = overlayId;
442  }
443  while(document.body.firstChild) {
444    document.body.removeChild(document.body.firstChild);
445  }
446  initLayout();
447  update();
448}
449
450document.addEventListener('DOMContentLoaded', init);
451