Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / braille_ime / braille_ime.js
blobc91257446db690655240232958550566b045d3d0
1 // Copyright 2014 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.
5 'use strict';
7 /**
8  * @fileoverview Braille hardware keyboard input method.
9  *
10  * This method is automatically enabled when a braille display is connected
11  * and ChromeVox is turned on.  Most of the braille input and editing logic
12  * is located in ChromeVox where the braille translation library is available.
13  * This IME connects to ChromeVox and communicates using messages as follows:
14  *
15  * Sent from this IME to ChromeVox:
16  * {type: 'activeState', active: boolean}
17  * {type: 'inputContext', context: InputContext}
18  *   Sent on focus/blur to inform ChromeVox of the type of the current field.
19  *   In the latter case (blur), context is null.
20  * {type: 'reset'}
21  *   Sent when the {@code onReset} IME event fires or uncommitted text is
22  *   committed without being triggered by ChromeVox (e.g. because of a
23  *   key press).
24  * {type: 'brailleDots', dots: number}
25  *   Sent when the user typed a braille cell using the standard keyboard.
26  *   ChromeVox treats this similarly to entering braille input using the
27  *   braille display.
28  * {type: 'backspace', requestId: string}
29  *   Sent when the user presses the backspace key.
30  *   ChromeVox must respond with a {@code keyEventHandled} message
31  *   with the same request id.
32  *
33  * Sent from ChromeVox to this IME:
34  * {type: 'replaceText', contextID: number, deleteBefore: number,
35  *  newText: string}
36  *   Deletes {@code deleteBefore} characters before the cursor (or selection)
37  *   and inserts {@code newText}.  {@code contextID} identifies the text field
38  *   to apply the update to (no change will happen if focus has moved to a
39  *   different field).
40  * {type: 'setUncommitted', contextID: number, text: string}
41  *   Stores text for the field identified by contextID to be committed
42  *   either as a result of a 'commitUncommitted' message or a by the IME
43  *   unhandled key press event.  Unlike 'replaceText', this does not send the
44  *   uncommitted text to the input field, but instead stores it in the IME.
45  * {type: 'commitUncommitted', contextID: number}
46  *   Commits any uncommitted text if it matches the given context ID.
47  *   See 'setUncommitted' above.
48  * {type: 'keyEventHandled', requestId: string, result: boolean}
49  *   Response to a {@code backspace} message indicating whether the
50  *   backspace was handled by ChromeVox or should be allowed to propagate
51  *   through the normal event handling pipeline.
52  */
54 /**
55  * @constructor
56  */
57 var BrailleIme = function() {};
59 BrailleIme.prototype = {
60   /**
61    * Whether to enable extra debug logging for the IME.
62    * @const {boolean}
63    * @private
64    */
65   DEBUG: false,
67   /**
68    * ChromeVox extension ID.
69    * @const {string}
70    * @private
71    */
72   CHROMEVOX_EXTENSION_ID_: 'mndnfokpggljbaajbnioimlmbfngpief',
74   /**
75    * Name of the port used for communication with ChromeVox.
76    * @const {string}
77    * @private
78    */
79   PORT_NAME: 'cvox.BrailleIme.Port',
81   /**
82    * Identifier for the use standard keyboard option used in the menu and
83    * {@code localStorage}.  This can be switched on to type braille using the
84    * standard keyboard, or off (default) for the usual keyboard behaviour.
85    * @const {string}
86    */
87   USE_STANDARD_KEYBOARD_ID: 'useStandardKeyboard',
89   // State related to the support for typing braille using a standrad
90   // (qwerty) keyboard.
92   /** @private {boolean} */
93   useStandardKeyboard_: false,
95   /**
96    * Braille dots for keys that are currently pressed.
97    * @private {number}
98    */
99   pressed_: 0,
101   /**
102    * Dots that have been pressed at some point since {@code pressed_} was last
103    * {@code 0}.
104    * @private {number}
105    */
106   accumulated_: 0,
108   /**
109    * Bit in {@code pressed_} and {@code accumulated_} that represent
110    * the space key.
111    * @const {number}
112    */
113   SPACE: 0x100,
115   /**
116    * Maps key codes on a standard keyboard to the correspodning dots.
117    * Keys on the 'home row' correspond to the keys on a Perkins-style keyboard.
118    * Note that the mapping below is arranged like the dots in a braille cell.
119    * Only 6 dot input is supported.
120    * @private
121    * @const {Object<number>}
122    */
123   CODE_TO_DOT_: {'KeyF': 0x01, 'KeyJ': 0x08,
124                  'KeyD': 0x02, 'KeyK': 0x10,
125                  'KeyS': 0x04, 'KeyL': 0x20,
126                  'Space': 0x100 },
128   /**
129    * The current engine ID as set by {@code onActivate}, or the empty string if
130    * the IME is not active.
131    * @type {string}
132    * @private
133    */
134   engineID_: '',
136   /**
137    * The port used to communicate with ChromeVox.
138    * @type {Port} port_
139    * @private
140    */
141   port_: null,
143   /**
144    * Uncommitted text and context ID.
145    * @type {?{contextID: number, text: string}}
146    * @private
147    */
148   uncommitted_: null,
150   /**
151    * Registers event listeners in the chrome IME API.
152    */
153   init: function() {
154     chrome.input.ime.onActivate.addListener(this.onActivate_.bind(this));
155     chrome.input.ime.onDeactivated.addListener(this.onDeactivated_.bind(this));
156     chrome.input.ime.onFocus.addListener(this.onFocus_.bind(this));
157     chrome.input.ime.onBlur.addListener(this.onBlur_.bind(this));
158     chrome.input.ime.onInputContextUpdate.addListener(
159         this.onInputContextUpdate_.bind(this));
160     chrome.input.ime.onKeyEvent.addListener(this.onKeyEvent_.bind(this),
161                                             ['async']);
162     chrome.input.ime.onReset.addListener(this.onReset_.bind(this));
163     chrome.input.ime.onMenuItemActivated.addListener(
164         this.onMenuItemActivated_.bind(this));
165     this.connectChromeVox_();
166   },
168   /**
169    * Called by the IME framework when this IME is activated.
170    * @param {string} engineID Engine ID, should be 'braille'.
171    * @private
172    */
173   onActivate_: function(engineID) {
174     this.log_('onActivate', engineID);
175     this.engineID_ = engineID;
176     if (!this.port_) {
177       this.connectChromeVox_();
178     }
179     this.useStandardKeyboard_ =
180         localStorage[this.USE_STANDARD_KEYBOARD_ID] === String(true);
181     this.accumulated_ = 0;
182     this.pressed_ = 0;
183     this.updateMenuItems_();
184     this.sendActiveState_();
185   },
187   /**
188    * Called by the IME framework when this IME is deactivated.
189    * @param {string} engineID Engine ID, should be 'braille'.
190    * @private
191    */
192   onDeactivated_: function(engineID) {
193     this.log_('onDectivated', engineID);
194     this.engineID_ = '';
195     this.sendActiveState_();
196   },
198   /**
199    * Called by the IME framework when a text field receives focus.
200    * @param {InputContext} context Input field context.
201    * @private
202    */
203   onFocus_: function(context) {
204     this.log_('onFocus', context);
205     this.sendInputContext_(context);
206   },
208   /**
209    * Called by the IME framework when a text field looses focus.
210    * @param {number} contextID Input field context ID.
211    * @private
212    */
213   onBlur_: function(contextID) {
214     this.log_('onBlur', contextID + '');
215     this.sendInputContext_(null);
216   },
218   /**
219    * Called by the IME framework when the current input context is updated.
220    * @param {InputContext} context Input field context.
221    * @private
222    */
223   onInputContextUpdate_: function(context) {
224     this.log_('onInputContextUpdate', context);
225     this.sendInputContext_(context);
226   },
228   /**
229    * Called by the system when this IME is active and a key event is generated.
230    * @param {string} engineID Engine ID, should be 'braille'.
231    * @param {!ChromeKeyboardEvent} event The keyboard event.
232    * @private
233    */
234   onKeyEvent_: function(engineID, event) {
235     var result = this.processKey_(event);
236     if (result !== undefined)
237       this.keyEventHandled_(event.requestId, event.type, result);
238   },
240   /**
241    * Called when chrome ends the current text input session.
242    * @param {string} engineID Engine ID, should be 'braille'.
243    * @private
244    */
245   onReset_: function(engineID) {
246     this.log_('onReset', engineID);
247     this.engineID_ = engineID;
248     this.sendToChromeVox_({type: 'reset'});
249   },
251   /**
252    * Called by the IME framework when a menu item is activated.
253    * @param {string} engineID Engine ID, should be 'braille'.
254    * @param {string} itemID Identifies the menu item.
255    * @private
256    */
257   onMenuItemActivated_: function(engineID, itemID) {
258     if (engineID === this.engineID_ &&
259         itemID === this.USE_STANDARD_KEYBOARD_ID) {
260       this.useStandardKeyboard_ = !this.useStandardKeyboard_;
261       localStorage[this.USE_STANDARD_KEYBOARD_ID] =
262           String(this.useStandardKeyboard_);
263       if (!this.useStandardKeyboard_) {
264         this.accumulated_ = 0;
265         this.pressed_ = 0;
266       }
267       this.updateMenuItems_();
268     }
269   },
271   /**
272    * Outputs a log message to the console, only if {@link BrailleIme.DEBUG}
273    * is set to true.
274    * @param {string} func Name of the caller.
275    * @param {Object|string=} message Message to output.
276    * @private
277    */
278   log_: function(func, message) {
279     if (this.DEBUG) {
280       if (typeof(message) !== 'string') {
281         message = JSON.stringify(message);
282       }
283       console.log('BrailleIme.' + func + ': ' + message);
284     }
285   },
287   /**
288    * Handles a qwerty key on the home row as a braille key.
289    * @param {!ChromeKeyboardEvent} event Keyboard event.
290    * @return {boolean|undefined} Whether the event was handled, or
291    *     {@code undefined} if handling was delegated to ChromeVox.
292    * @private
293    */
294   processKey_: function(event) {
295     if (!this.useStandardKeyboard_) {
296       return false;
297     }
298     if (event.code === 'Backspace' && event.type === 'keydown') {
299       this.pressed_ = 0;
300       this.accumulated_ = 0;
301       this.sendToChromeVox_(
302           {type: 'backspace', requestId: event.requestId});
303       return undefined;
304     }
305     var dot = this.CODE_TO_DOT_[event.code];
306     if (!dot || event.altKey || event.ctrlKey || event.shiftKey ||
307         event.capsLock) {
308       this.pressed_ = 0;
309       this.accumulated_ = 0;
310       return false;
311     }
312     if (event.type === 'keydown') {
313       this.pressed_ |= dot;
314       this.accumulated_ |= this.pressed_;
315       return true;
316     } else if (event.type === 'keyup') {
317       this.pressed_ &= ~dot;
318       if (this.pressed_ === 0 && this.accumulated_ !== 0) {
319         var dotsToSend = this.accumulated_;
320         this.accumulated_ = 0;
321         if (dotsToSend & this.SPACE) {
322           if (dotsToSend != this.SPACE) {
323             // Can't combine space and actual dot keys.
324             return true;
325           }
326           // Space is sent as a blank cell.
327           dotsToSend = 0;
328         }
329         this.sendToChromeVox_({type: 'brailleDots', dots: dotsToSend});
330       }
331       return true;
332     }
333     return false;
334   },
336   /**
337    * Connects to the ChromeVox extension for message passing.
338    * @private
339    */
340   connectChromeVox_: function() {
341     if (this.port_) {
342       this.port_.disconnect();
343       this.port_ = null;
344     }
345     this.port_ = chrome.runtime.connect(
346         this.CHROMEVOX_EXTENSION_ID_, {name: this.PORT_NAME});
347     this.port_.onMessage.addListener(
348         this.onChromeVoxMessage_.bind(this));
349     this.port_.onDisconnect.addListener(
350         this.onChromeVoxDisconnect_.bind(this));
351   },
353   /**
354    * Handles a message from the ChromeVox extension.
355    * @param {*} message The message from the extension.
356    * @private
357    */
358   onChromeVoxMessage_: function(message) {
359     message = /** @type {{type: string}} */ (message);
360     this.log_('onChromeVoxMessage', message);
361     switch (message.type) {
362       case 'replaceText':
363         message =
364             /**
365              * @type {{contextID: number, deleteBefore: number,
366              *         newText: string}}
367              */
368             (message);
369         this.replaceText_(message.contextID, message.deleteBefore,
370                           message.newText);
371         break;
372       case 'keyEventHandled':
373         message =
374             /** @type {{requestId: string, result: boolean}} */ (message);
375         this.keyEventHandled_(message.requestId, 'keydown', message.result);
376         break;
377       case 'setUncommitted':
378         message =
379             /** @type {{contextID: number, text: string}} */ (message);
380         this.setUncommitted_(message.contextID, message.text);
381         break;
382       case 'commitUncommitted':
383         message =
384             /** @type {{contextID: number}} */ (message);
385         this.commitUncommitted_(message.contextID);
386         break;
387       default:
388         console.error('Unknown message from ChromeVox: ' +
389             JSON.stringify(message));
390         break;
391     }
392   },
394   /**
395    * Handles a disconnect event from the ChromeVox side.
396    * @private
397    */
398   onChromeVoxDisconnect_: function() {
399     this.port_ = null;
400     this.log_('onChromeVoxDisconnect', chrome.runtime.lastError);
401   },
403   /**
404    * Sends a message to the ChromeVox extension.
405    * @param {Object} message The message to send.
406    * @private
407    */
408   sendToChromeVox_: function(message) {
409     if (this.port_) {
410       this.port_.postMessage(message);
411     }
412   },
414   /**
415    * Sends the given input context to ChromeVox.
416    * @param {InputContext} context Input context, or null when there's no input
417    *    context.
418    * @private
419    */
420   sendInputContext_: function(context) {
421     this.sendToChromeVox_({type: 'inputContext', context: context});
422   },
424   /**
425    * Sends the active state to ChromeVox.
426    * @private
427    */
428   sendActiveState_: function() {
429     this.sendToChromeVox_({type: 'activeState',
430                            active: this.engineID_.length > 0});
431   },
433   /**
434    * Replaces text in the current text field.
435    * @param {number} contextID Context for the input field to replace the
436    *     text in.
437    * @param {number} deleteBefore How many characters to delete before the
438    *     cursor.
439    * @param {string} toInsert Text to insert at the cursor.
440    */
441   replaceText_: function(contextID, deleteBefore, toInsert) {
442     var addText = chrome.input.ime.commitText.bind(
443         null, {contextID: contextID, text: toInsert}, function() {});
444     if (deleteBefore > 0) {
445       var deleteText = chrome.input.ime.deleteSurroundingText.bind(null,
446           {engineID: this.engineID_, contextID: contextID,
447            offset: -deleteBefore, length: deleteBefore}, addText);
448       // Make sure there's no non-zero length selection so that
449       // deleteSurroundingText works correctly.
450       chrome.input.ime.deleteSurroundingText(
451           {engineID: this.engineID_, contextID: contextID,
452            offset: 0, length: 0}, deleteText);
453     } else {
454       addText();
455     }
456   },
458   /**
459    * Responds to an asynchronous key event, indicating whether it was handled
460    * or not.  If it wasn't handled, any uncommitted text is committed
461    * before sending the response to the IME API.
462    * @param {string} requestId Key event request id.
463    * @param {string} type Type of key event being responded to.
464    * @param {boolean} response Whether the IME handled the event.
465    */
466   keyEventHandled_: function(requestId, type, response) {
467     if (!response && type === 'keydown' && this.uncommitted_) {
468       this.commitUncommitted_(this.uncommitted_.contextID);
469       this.sendToChromeVox_({type: 'reset'});
470     }
471     chrome.input.ime.keyEventHandled(requestId, response);
472   },
474   /**
475    * Stores uncommitted text that will be committed on any key press or
476    * when {@code commitUncommitted_} is called.
477    * @param {number} contextID of the current field.
478    * @param {string} text to store.
479    */
480   setUncommitted_: function(contextID, text) {
481     this.uncommitted_ = {contextID: contextID, text: text};
482   },
484   /**
485    * Commits the last set uncommitted text if it matches the given context id.
486    * @param {number} contextID
487    */
488   commitUncommitted_: function(contextID) {
489     if (this.uncommitted_ && contextID === this.uncommitted_.contextID)
490       chrome.input.ime.commitText(this.uncommitted_);
491     this.uncommitted_ = null;
492   },
494   /**
495    * Updates the menu items for this IME.
496    */
497   updateMenuItems_: function() {
498     // TODO(plundblad): Localize when translations available.
499     chrome.input.ime.setMenuItems(
500         {engineID: this.engineID_,
501          items: [
502            {
503              id: this.USE_STANDARD_KEYBOARD_ID,
504              label: 'Use standard keyboard for braille',
505              style: 'check',
506              visible: true,
507              checked: this.useStandardKeyboard_,
508              enabled: true
509              }
510          ]
511         });
512   }