Home | History | Annotate | Download | only in keyboard
      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 
      5 /**
      6  * @fileoverview A simple, English virtual keyboard implementation.
      7  */
      8 
      9 var KEY_MODE = 'key';
     10 var SHIFT_MODE = 'shift';
     11 var NUMBER_MODE = 'number';
     12 var SYMBOL_MODE = 'symbol';
     13 var MODES = [ KEY_MODE, SHIFT_MODE, NUMBER_MODE, SYMBOL_MODE ];
     14 var currentMode = KEY_MODE;
     15 var MODE_TRANSITIONS = {};
     16 
     17 MODE_TRANSITIONS[KEY_MODE + SHIFT_MODE] = SHIFT_MODE;
     18 MODE_TRANSITIONS[KEY_MODE + NUMBER_MODE] = NUMBER_MODE;
     19 MODE_TRANSITIONS[SHIFT_MODE + SHIFT_MODE] = KEY_MODE;
     20 MODE_TRANSITIONS[SHIFT_MODE + NUMBER_MODE] = NUMBER_MODE;
     21 MODE_TRANSITIONS[NUMBER_MODE + SHIFT_MODE] = SYMBOL_MODE;
     22 MODE_TRANSITIONS[NUMBER_MODE + NUMBER_MODE] = KEY_MODE;
     23 MODE_TRANSITIONS[SYMBOL_MODE + SHIFT_MODE] = NUMBER_MODE;
     24 MODE_TRANSITIONS[SYMBOL_MODE + NUMBER_MODE] = KEY_MODE;
     25 
     26 /**
     27  * Transition the mode according to the given transition.
     28  * @param {string} transition The transition to take.
     29  * @return {void}
     30  */
     31 function transitionMode(transition) {
     32   currentMode = MODE_TRANSITIONS[currentMode + transition];
     33 }
     34 
     35 /**
     36  * Plain-old-data class to represent a character.
     37  * @param {string} display The HTML to be displayed.
     38  * @param {string} id The key identifier for this Character.
     39  * @constructor
     40  */
     41 function Character(display, id) {
     42   this.display = display;
     43   this.keyIdentifier = id;
     44 }
     45 
     46 /**
     47  * Convenience function to make the keyboard data more readable.
     48  * @param {string} display Both the display and id for the created Character.
     49  */
     50 function C(display) {
     51   return new Character(display, display);
     52 }
     53 
     54 /**
     55  * An abstract base-class for all keys on the keyboard.
     56  * @constructor
     57  */
     58 function BaseKey() {}
     59 
     60 BaseKey.prototype = {
     61   /**
     62    * The aspect ratio of this key.
     63    * @type {number}
     64    */
     65   aspect_: 1,
     66 
     67   /**
     68    * The cell type of this key.  Determines the background colour.
     69    * @type {string}
     70    */
     71   cellType_: '',
     72 
     73   /**
     74    * @return {number} The aspect ratio of this key.
     75    */
     76   get aspect() {
     77     return this.aspect_;
     78   },
     79 
     80   /**
     81    * Set the position, a.k.a. row, of this key.
     82    * @param {string} position The position.
     83    * @return {void}
     84    */
     85   set position(position) {
     86     for (var i in this.modeElements_) {
     87       this.modeElements_[i].classList.add(this.cellType_ + 'r' + position);
     88     }
     89   },
     90 
     91   /**
     92    * Returns the amount of padding for the top of the key.
     93    * @param {string} mode The mode for the key.
     94    * @param {number} height The height of the key.
     95    * @return {number} Padding in pixels.
     96    */
     97   getPadding: function(mode, height) {
     98     return Math.floor(height / 3.5);
     99   },
    100 
    101   /**
    102    * Size the DOM elements of this key.
    103    * @param {string} mode The mode to be sized.
    104    * @param {number} height The height of the key.
    105    * @return {void}
    106    */
    107   sizeElement: function(mode, height) {
    108     var padding = this.getPadding(mode, height);
    109     var border = 1;
    110     var margin = 5;
    111     var width = Math.floor(height * this.aspect_);
    112 
    113     var extraHeight = margin + padding + 2 * border;
    114     var extraWidth = margin + 2 * border;
    115 
    116     this.modeElements_[mode].style.width = (width - extraWidth) + 'px';
    117     this.modeElements_[mode].style.height = (height - extraHeight) + 'px';
    118     this.modeElements_[mode].style.marginLeft = margin + 'px';
    119     this.modeElements_[mode].style.fontSize = (height / 3.5) + 'px';
    120     this.modeElements_[mode].style.paddingTop = padding + 'px';
    121   },
    122 
    123   /**
    124    * Resize all modes of this key based on the given height.
    125    * @param {number} height The height of the key.
    126    * @return {void}
    127    */
    128   resize: function(height) {
    129     for (var i in this.modeElements_) {
    130       this.sizeElement(i, height);
    131     }
    132   },
    133 
    134   /**
    135    * Create the DOM elements for the given keyboard mode.  Must be overridden.
    136    * @param {string} mode The keyboard mode to create elements for.
    137    * @param {number} height The height of the key.
    138    * @return {Element} The top-level DOM Element for the key.
    139    */
    140   makeDOM: function(mode, height) {
    141     throw new Error('makeDOM not implemented in BaseKey');
    142   },
    143 };
    144 
    145 /**
    146  * A simple key which displays Characters.
    147  * @param {Character} key The Character for KEY_MODE.
    148  * @param {Character} shift The Character for SHIFT_MODE.
    149  * @param {Character} num The Character for NUMBER_MODE.
    150  * @param {Character} symbol The Character for SYMBOL_MODE.
    151  * @constructor
    152  * @extends {BaseKey}
    153  */
    154 function Key(key, shift, num, symbol) {
    155   this.modeElements_ = {};
    156   this.aspect_ = 1;  // ratio width:height
    157   this.cellType_ = '';
    158 
    159   this.modes_ = {};
    160   this.modes_[KEY_MODE] = key;
    161   this.modes_[SHIFT_MODE] = shift;
    162   this.modes_[NUMBER_MODE] = num;
    163   this.modes_[SYMBOL_MODE] = symbol;
    164 }
    165 
    166 Key.prototype = {
    167   __proto__: BaseKey.prototype,
    168 
    169   /** @inheritDoc */
    170   makeDOM: function(mode, height) {
    171     this.modeElements_[mode] = document.createElement('div');
    172     this.modeElements_[mode].textContent = this.modes_[mode].display;
    173     this.modeElements_[mode].className = 'key';
    174 
    175     this.sizeElement(mode, height);
    176 
    177     this.modeElements_[mode].onclick =
    178         sendKeyFunction(this.modes_[mode].keyIdentifier);
    179 
    180     return this.modeElements_[mode];
    181   }
    182 };
    183 
    184 /**
    185  * A key which displays an SVG image.
    186  * @param {number} aspect The aspect ratio of the key.
    187  * @param {string} className The class that provides the image.
    188  * @param {string} keyId The key identifier for the key.
    189  * @constructor
    190  * @extends {BaseKey}
    191  */
    192 function SvgKey(aspect, className, keyId) {
    193   this.modeElements_ = {};
    194   this.aspect_ = aspect;
    195   this.cellType_ = 'nc';
    196   this.className_ = className;
    197   this.keyId_ = keyId;
    198 }
    199 
    200 SvgKey.prototype = {
    201   __proto__: BaseKey.prototype,
    202 
    203   /** @inheritDoc */
    204   getPadding: function(mode, height) { return 0; },
    205 
    206   /** @inheritDoc */
    207   makeDOM: function(mode, height) {
    208     this.modeElements_[mode] = document.createElement('div');
    209     this.modeElements_[mode].className = 'key';
    210 
    211     var img = document.createElement('div');
    212     img.className = 'image-key ' + this.className_;
    213     this.modeElements_[mode].appendChild(img);
    214 
    215     this.modeElements_[mode].onclick = sendKeyFunction(this.keyId_);
    216 
    217     this.sizeElement(mode, height);
    218 
    219     return this.modeElements_[mode];
    220   }
    221 };
    222 
    223 /**
    224  * A Key that remains the same through all modes.
    225  * @param {number} aspect The aspect ratio of the key.
    226  * @param {string} content The display text for the key.
    227  * @param {string} keyId The key identifier for the key.
    228  * @constructor
    229  * @extends {BaseKey}
    230  */
    231 function SpecialKey(aspect, content, keyId) {
    232   this.modeElements_ = {};
    233   this.aspect_ = aspect;
    234   this.cellType_ = 'nc';
    235   this.content_ = content;
    236   this.keyId_ = keyId;
    237 }
    238 
    239 SpecialKey.prototype = {
    240   __proto__: BaseKey.prototype,
    241 
    242   /** @inheritDoc */
    243   makeDOM: function(mode, height) {
    244     this.modeElements_[mode] = document.createElement('div');
    245     this.modeElements_[mode].textContent = this.content_;
    246     this.modeElements_[mode].className = 'key';
    247 
    248     this.modeElements_[mode].onclick = sendKeyFunction(this.keyId_);
    249 
    250     this.sizeElement(mode, height);
    251 
    252     return this.modeElements_[mode];
    253   }
    254 };
    255 
    256 /**
    257  * A shift key.
    258  * @param {number} aspect The aspect ratio of the key.
    259  * @constructor
    260  * @extends {BaseKey}
    261  */
    262 function ShiftKey(aspect) {
    263   this.modeElements_ = {};
    264   this.aspect_ = aspect;
    265   this.cellType_ = 'nc';
    266 }
    267 
    268 ShiftKey.prototype = {
    269   __proto__: BaseKey.prototype,
    270 
    271   /** @inheritDoc */
    272   getPadding: function(mode, height) {
    273     if (mode == NUMBER_MODE || mode == SYMBOL_MODE) {
    274       return BaseKey.prototype.getPadding.call(this, mode, height);
    275     }
    276     return 0;
    277   },
    278 
    279   /** @inheritDoc */
    280   makeDOM: function(mode, height) {
    281     this.modeElements_[mode] = document.createElement('div');
    282 
    283     if (mode == KEY_MODE || mode == SHIFT_MODE) {
    284       var shift = document.createElement('div');
    285       shift.className = 'image-key shift';
    286       this.modeElements_[mode].appendChild(shift);
    287     } else if (mode == NUMBER_MODE) {
    288       this.modeElements_[mode].textContent = 'more';
    289     } else if (mode == SYMBOL_MODE) {
    290       this.modeElements_[mode].textContent = '#123';
    291     }
    292 
    293     if (mode == SHIFT_MODE || mode == SYMBOL_MODE) {
    294       this.modeElements_[mode].className = 'moddown key';
    295     } else {
    296       this.modeElements_[mode].className = 'key';
    297     }
    298 
    299     this.sizeElement(mode, height);
    300 
    301     this.modeElements_[mode].onclick = function() {
    302       transitionMode(SHIFT_MODE);
    303       setMode(currentMode);
    304     };
    305     return this.modeElements_[mode];
    306   },
    307 };
    308 
    309 /**
    310  * The symbol key: switches the keyboard into symbol mode.
    311  * @constructor
    312  * @extends {BaseKey}
    313  */
    314 function SymbolKey() {
    315   this.modeElements_ = {}
    316   this.aspect_ = 1.3;
    317   this.cellType_ = 'nc';
    318 }
    319 
    320 SymbolKey.prototype = {
    321   __proto__: BaseKey.prototype,
    322 
    323   /** @inheritDoc */
    324   makeDOM: function(mode, height) {
    325     this.modeElements_[mode] = document.createElement('div');
    326 
    327     if (mode == KEY_MODE || mode == SHIFT_MODE) {
    328       this.modeElements_[mode].textContent = '#123';
    329     } else if (mode == NUMBER_MODE || mode == SYMBOL_MODE) {
    330       this.modeElements_[mode].textContent = 'abc';
    331     }
    332 
    333     if (mode == NUMBER_MODE || mode == SYMBOL_MODE) {
    334       this.modeElements_[mode].className = 'moddown key';
    335     } else {
    336       this.modeElements_[mode].className = 'key';
    337     }
    338 
    339     this.sizeElement(mode, height);
    340 
    341     this.modeElements_[mode].onclick = function() {
    342       transitionMode(NUMBER_MODE);
    343       setMode(currentMode);
    344     };
    345 
    346     return this.modeElements_[mode];
    347   }
    348 };
    349 
    350 /**
    351  * The ".com" key.
    352  * @constructor
    353  * @extends {BaseKey}
    354  */
    355 function DotComKey() {
    356   this.modeElements_ = {}
    357   this.aspect_ = 1.3;
    358   this.cellType_ = 'nc';
    359 }
    360 
    361 DotComKey.prototype = {
    362   __proto__: BaseKey.prototype,
    363 
    364   /** @inheritDoc */
    365   makeDOM: function(mode, height) {
    366     this.modeElements_[mode] = document.createElement('div');
    367     this.modeElements_[mode].textContent = '.com';
    368     this.modeElements_[mode].className = 'key';
    369 
    370     this.sizeElement(mode, height);
    371 
    372     this.modeElements_[mode].onclick = function() {
    373       sendKey('.');
    374       sendKey('c');
    375       sendKey('o');
    376       sendKey('m');
    377     };
    378 
    379     return this.modeElements_[mode];
    380   }
    381 };
    382 
    383 /**
    384  * The key that hides the keyboard.
    385  * @constructor
    386  * @extends {BaseKey}
    387  */
    388 function HideKeyboardKey() {
    389   this.modeElements_ = {}
    390   this.aspect_ = 1.3;
    391   this.cellType_ = 'nc';
    392 }
    393 
    394 HideKeyboardKey.prototype = {
    395   __proto__: BaseKey.prototype,
    396 
    397   /** @inheritDoc */
    398   getPadding: function(mode, height) { return 0; },
    399 
    400   /** @inheritDoc */
    401   makeDOM: function(mode, height) {
    402     this.modeElements_[mode] = document.createElement('div');
    403     this.modeElements_[mode].className = 'key';
    404 
    405     var hide = document.createElement('div');
    406     hide.className = 'image-key hide';
    407     this.modeElements_[mode].appendChild(hide);
    408 
    409     this.sizeElement(mode, height);
    410 
    411     this.modeElements_[mode].onclick = function() {
    412       // TODO(bryeung): need a way to cancel the keyboard
    413     };
    414 
    415     return this.modeElements_[mode];
    416   }
    417 };
    418 
    419 /**
    420  * A container for keys.
    421  * @param {number} position The position of the row (0-3).
    422  * @param {Array.<BaseKey>} keys The keys in the row.
    423  * @constructor
    424  */
    425 function Row(position, keys) {
    426   this.position_ = position;
    427   this.keys_ = keys;
    428   this.element_ = null;
    429   this.modeElements_ = {};
    430 }
    431 
    432 Row.prototype = {
    433   /**
    434    * Get the total aspect ratio of the row.
    435    * @return {number} The aspect ratio relative to a height of 1 unit.
    436    */
    437   get aspect() {
    438     var total = 0;
    439     for (var i = 0; i < this.keys_.length; ++i) {
    440       total += this.keys_[i].aspect;
    441     }
    442     return total;
    443   },
    444 
    445   /**
    446    * Create the DOM elements for the row.
    447    * @return {Element} The top-level DOM Element for the row.
    448    */
    449   makeDOM: function(height) {
    450     this.element_ = document.createElement('div');
    451     this.element_.className = 'row';
    452     for (var i = 0; i < MODES.length; ++i) {
    453       var mode = MODES[i];
    454       this.modeElements_[mode] = document.createElement('div');
    455       this.modeElements_[mode].style.display = 'none';
    456       this.element_.appendChild(this.modeElements_[mode]);
    457     }
    458 
    459     for (var j = 0; j < this.keys_.length; ++j) {
    460       var key = this.keys_[j];
    461       for (var i = 0; i < MODES.length; ++i) {
    462         this.modeElements_[MODES[i]].appendChild(key.makeDOM(MODES[i]), height);
    463       }
    464     }
    465 
    466     for (var i = 0; i < MODES.length; ++i) {
    467       var clearingDiv = document.createElement('div');
    468       clearingDiv.style.clear = 'both';
    469       this.modeElements_[MODES[i]].appendChild(clearingDiv);
    470     }
    471 
    472     for (var i = 0; i < this.keys_.length; ++i) {
    473       this.keys_[i].position = this.position_;
    474     }
    475 
    476     return this.element_;
    477   },
    478 
    479   /**
    480    * Shows the given mode.
    481    * @param {string} mode The mode to show.
    482    * @return {void}
    483    */
    484   showMode: function(mode) {
    485     for (var i = 0; i < MODES.length; ++i) {
    486       this.modeElements_[MODES[i]].style.display = 'none';
    487     }
    488     this.modeElements_[mode].style.display = 'block';
    489   },
    490 
    491   /**
    492    * Resizes all keys in the row according to the global size.
    493    * @param {number} height The height of the key.
    494    * @return {void}
    495    */
    496   resize: function(height) {
    497     for (var i = 0; i < this.keys_.length; ++i) {
    498       this.keys_[i].resize(height);
    499     }
    500   },
    501 };
    502 
    503 /**
    504  * All keys for the rows of the keyboard.
    505  * NOTE: every row below should have an aspect of 12.6.
    506  * @type {Array.<Array.<BaseKey>>}
    507  */
    508 var KEYS = [
    509   [
    510     new SvgKey(1, 'tab', 'Tab'),
    511     new Key(C('q'), C('Q'), C('1'), C('`')),
    512     new Key(C('w'), C('W'), C('2'), C('~')),
    513     new Key(C('e'), C('E'), C('3'), new Character('<', 'LessThan')),
    514     new Key(C('r'), C('R'), C('4'), new Character('>', 'GreaterThan')),
    515     new Key(C('t'), C('T'), C('5'), C('[')),
    516     new Key(C('y'), C('Y'), C('6'), C(']')),
    517     new Key(C('u'), C('U'), C('7'), C('{')),
    518     new Key(C('i'), C('I'), C('8'), C('}')),
    519     new Key(C('o'), C('O'), C('9'), C('\'')),
    520     new Key(C('p'), C('P'), C('0'), C('|')),
    521     new SvgKey(1.6, 'backspace', 'Backspace')
    522   ],
    523   [
    524     new SymbolKey(),
    525     new Key(C('a'), C('A'), C('!'), C('+')),
    526     new Key(C('s'), C('S'), C('@'), C('=')),
    527     new Key(C('d'), C('D'), C('#'), C(' ')),
    528     new Key(C('f'), C('F'), C('$'), C(' ')),
    529     new Key(C('g'), C('G'), C('%'), C(' ')),
    530     new Key(C('h'), C('H'), C('^'), C(' ')),
    531     new Key(C('j'), C('J'), new Character('&', 'Ampersand'), C(' ')),
    532     new Key(C('k'), C('K'), C('*'), C('#')),
    533     new Key(C('l'), C('L'), C('('), C(' ')),
    534     new Key(C('\''), C('\''), C(')'), C(' ')),
    535     new SvgKey(1.3, 'return', 'Enter')
    536   ],
    537   [
    538     new ShiftKey(1.6),
    539     new Key(C('z'), C('Z'), C('/'), C(' ')),
    540     new Key(C('x'), C('X'), C('-'), C(' ')),
    541     new Key(C('c'), C('C'), C('\''), C(' ')),
    542     new Key(C('v'), C('V'), C('"'), C(' ')),
    543     new Key(C('b'), C('B'), C(':'), C('.')),
    544     new Key(C('n'), C('N'), C(';'), C(' ')),
    545     new Key(C('m'), C('M'), C('_'), C(' ')),
    546     new Key(C('!'), C('!'), C('{'), C(' ')),
    547     new Key(C('?'), C('?'), C('}'), C(' ')),
    548     new Key(C('/'), C('/'), C('\\'), C(' ')),
    549     new ShiftKey(1)
    550   ],
    551   [
    552     new SvgKey(1.3, 'mic', ''),
    553     new DotComKey(),
    554     new SpecialKey(1.3, '@', '@'),
    555     // TODO(bryeung): the spacebar needs to be a little bit more stretchy,
    556     // since this row has only 7 keys (as opposed to 12), the truncation
    557     // can cause it to not be wide enough.
    558     new SpecialKey(4.8, ' ', 'Spacebar'),
    559     new SpecialKey(1.3, ',', ','),
    560     new SpecialKey(1.3, '.', '.'),
    561     new HideKeyboardKey()
    562   ]
    563 ];
    564 
    565 /**
    566  * All of the rows in the keyboard.
    567  * @type {Array.<Row>}
    568  */
    569 var allRows = [];  // Populated during start()
    570 
    571 /**
    572  * Calculate the height of the row based on the size of the page.
    573  * @return {number} The height of each row, in pixels.
    574  */
    575 function getRowHeight() {
    576   var x = window.innerWidth;
    577   var y = window.innerHeight;
    578   return (x > kKeyboardAspect * y) ?
    579       (height = Math.floor(y / 4)) :
    580       (height = Math.floor(x / (kKeyboardAspect * 4)));
    581 }
    582 
    583 /**
    584  * Set the keyboard mode.
    585  * @param {string} mode The new mode.
    586  * @return {void}
    587  */
    588 function setMode(mode) {
    589   for (var i = 0; i < allRows.length; ++i) {
    590     allRows[i].showMode(mode);
    591   }
    592 }
    593 
    594 /**
    595  * The keyboard's aspect ratio.
    596  * @type {number}
    597  */
    598 var kKeyboardAspect = 3.3;
    599 
    600 /**
    601  * Send the given key to chrome, via the experimental extension API.
    602  * @param {string} key The key to send.
    603  * @return {void}
    604  */
    605 function sendKey(key) {
    606   if (!chrome.experimental) {
    607     console.log(key);
    608     return;
    609   }
    610 
    611   var keyEvent = {'type': 'keydown', 'keyIdentifier': key};
    612   if (currentMode == SHIFT_MODE)
    613     keyEvent['shiftKey'] = true;
    614 
    615   chrome.experimental.input.sendKeyboardEvent(keyEvent);
    616   keyEvent['type'] = 'keyup';
    617   chrome.experimental.input.sendKeyboardEvent(keyEvent);
    618 
    619   // TODO(bryeung): deactivate shift after a successful keypress
    620 }
    621 
    622 /**
    623  * Create a closure for the sendKey function.
    624  * @param {string} key The parameter to sendKey.
    625  * @return {void}
    626  */
    627 function sendKeyFunction(key) {
    628   return function() { sendKey(key); }
    629 }
    630 
    631 /**
    632  * Resize the keyboard according to the new window size.
    633  * @return {void}
    634  */
    635 window.onresize = function() {
    636   var height = getRowHeight();
    637   var newX = document.documentElement.clientWidth;
    638 
    639   // All rows should have the same aspect, so just use the first one
    640   var totalWidth = Math.floor(height * allRows[0].aspect);
    641   var leftPadding = Math.floor((newX - totalWidth) / 2);
    642   document.getElementById('b').style.paddingLeft = leftPadding + 'px';
    643 
    644   for (var i = 0; i < allRows.length; ++i) {
    645     allRows[i].resize(height);
    646   }
    647 }
    648 
    649 /**
    650  * Init the keyboard.
    651  * @return {void}
    652  */
    653 window.onload = function() {
    654   var body = document.getElementById('b');
    655   for (var i = 0; i < KEYS.length; ++i) {
    656     allRows.push(new Row(i, KEYS[i]));
    657   }
    658 
    659   for (var i = 0; i < allRows.length; ++i) {
    660     body.appendChild(allRows[i].makeDOM(getRowHeight()));
    661     allRows[i].showMode(KEY_MODE);
    662   }
    663 
    664   window.onresize();
    665 }
    666 
    667 // TODO(bryeung): would be nice to leave less gutter (without causing
    668 // rendering issues with floated divs wrapping at some sizes).
    669