Remove the old signature of NotificationManager::closePersistent().
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / braille / braille_input_handler.js
blobf8c54d8a8d43029f1e1ff0fc0cb2bc90c7da298e
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 /**
6  * @fileoverview Handles braille input keys when the user is typing or editing
7  * text in an input field.  This class cooperates with the Braille IME
8  * that is built into Chrome OS to do the actual text editing.
9  */
11 goog.provide('cvox.BrailleInputHandler');
13 goog.require('cvox.BrailleKeyCommand');
14 goog.require('cvox.BrailleKeyEvent');
15 goog.require('cvox.ExpandingBrailleTranslator');
18 /**
19  * @param {cvox.BrailleTranslatorManager} translatorManager Keeps track of
20  *     the current braille translator(s).
21  * @constructor
22  */
23 cvox.BrailleInputHandler = function(translatorManager) {
24   /**
25    * Port of the connected IME if any.
26    * @type {Port}
27    * @private
28    */
29   this.imePort_ = null;
30   /**
31    * {code true} when the Braille IME is connected and has signaled that it is
32    * active.
33    * @type {boolean}
34    * @private
35    */
36   this.imeActive_ = false;
37   /**
38    * The input context of the current input field, as reported by the IME.
39    * {@code null} if no input field has focus.
40    * @type {{contextID: number, type: string}?}
41    * @private
42    */
43   this.inputContext_ = null;
44   /**
45    * @type {cvox.BrailleTranslatorManager}
46    * @private
47    */
48   this.translatorManager_ = translatorManager;
49   /**
50    * The translator currently used for typing, if
51    * {@code this.cells_.length > 0}.
52    * @type {cvox.LibLouis.Translator}
53    * @private
54    */
55   this.activeTranslator_ = null;
56   /**
57    * Braille cells that have been typed by the user so far.
58    * @type {Array<number>}
59    * @private
60    */
61   this.cells_ = [];
62   /**
63    * Text resulting from translating {@code this.cells_}.
64    * @type {string}
65    * @private
66    */
67   this.text_ = '';
68   /**
69    * Text that currently precedes the first selection end-point.
70    * @type {string}
71    * @private
72    */
73   this.currentTextBefore_ = '';
74   /**
75    * Text that currently follows the last selection end-point.
76    * @type {string}
77    * @private
78    */
79   this.currentTextAfter_ = '';
80   /**
81    * List of strings that we expect to be set as preceding text of the
82    * selection.  This is populated when we send text changes to the IME so that
83    * our own changes don't reset the pending cells.
84    * @type {Array<string>}
85    * @private
86    */
87   this.pendingTextsBefore_ = [];
88   /**
89    * Cells that were entered while the IME wasn't active.  These will be
90    * submitted once the IME becomes active and reports the current input field.
91    * This is necessary because the IME is activated on the first braille
92    * dots command, but we'll receive the command in parallel.  To work around
93    * the race, we store the cell entered until we can submit it to the IME.
94    * @type {Array<number>}
95    * @private
96    */
97   this.pendingCells_ = [];
99   this.translatorManager_.addChangeListener(this.resetText_.bind(this));
104  * The ID of the Braille IME extension built into Chrome OS.
105  * @const {string}
106  * @private
107  */
108 cvox.BrailleInputHandler.IME_EXTENSION_ID_ =
109     'jddehjeebkoimngcbdkaahpobgicbffp';
113  * Name of the port to use for communicating with the Braille IME.
114  * @const {string}
115  * @private
116  */
117 cvox.BrailleInputHandler.IME_PORT_NAME_ = 'cvox.BrailleIme.Port';
121  * Starts to listen for connections from the ChromeOS braille IME.
122  */
123 cvox.BrailleInputHandler.prototype.init = function() {
124   chrome.runtime.onConnectExternal.addListener(
125       goog.bind(this.onImeConnect_, this));
130  * Called when the content on the braille display is updated.  Modifies the
131  * input state according to the new content.
132  * @param {cvox.Spannable} text Text, optionally with value and selection
133  *     spans.
134  */
135 cvox.BrailleInputHandler.prototype.onDisplayContentChanged = function(text) {
136   var valueSpan = text.getSpanInstanceOf(cvox.ValueSpan);
137   var selectionSpan = text.getSpanInstanceOf(cvox.ValueSelectionSpan);
138   if (!(valueSpan && selectionSpan)) {
139     return;
140   }
141   // The type casts are ok because the spans are known to exist.
142   var valueStart = /** @type {number} */ (text.getSpanStart(valueSpan));
143   var valueEnd = /** @type {number} */ (text.getSpanEnd(valueSpan));
144   var selectionStart =
145       /** @type {number} */ (text.getSpanStart(selectionSpan));
146   var selectionEnd = /** @type {number} */ (text.getSpanEnd(selectionSpan));
147   if (selectionStart < valueStart || selectionEnd > valueEnd) {
148     console.error('Selection outside of value in braille content');
149     this.resetText_();
150     return;
151   }
152   var oldTextBefore = this.currentTextBefore_;
153   this.currentTextBefore_ = text.toString().substring(
154       valueStart, selectionStart);
155   this.currentTextAfter_ = text.toString().substring(selectionEnd, valueEnd);
156   if (this.cells_.length > 0) {
157     // Ignore this change if the preceding text hasn't changed.
158     if (oldTextBefore === this.currentTextBefore_) {
159       return;
160     }
161     // See if we are expecting this change as a result of one of our own edits.
162     if (this.pendingTextsBefore_.length > 0) {
163       // Allow changes to be coalesced by the input system in an attempt to not
164       // be too brittle.
165       for (var i = 0; i < this.pendingTextsBefore_.length; ++i) {
166         if (this.currentTextBefore_ === this.pendingTextsBefore_[i]) {
167           // Delete all previous expected changes and ignore this one.
168           this.pendingTextsBefore_.splice(0, i + 1);
169           return;
170         }
171       }
172     }
173     // There was an actual text change (or cursor movement) that we hadn't
174     // caused ourselves, reset any pending input.
175     this.resetText_();
176   } else {
177     this.updateActiveTranslator_();
178   }
183  * Handles braille key events used for input by editing the current input field
184  * appropriately.
185  * @param {!cvox.BrailleKeyEvent} event The key event.
186  * @return {boolean} {@code true} if the event was handled, {@code false}
187  *     if it should propagate further.
188  */
189 cvox.BrailleInputHandler.prototype.onBrailleKeyEvent = function(event) {
190   if (event.command === cvox.BrailleKeyCommand.DOTS) {
191     return this.onBrailleDots_(/** @type {number} */(event.brailleDots));
192   }
193   // Any other braille command cancels the pending cells.
194   this.pendingCells_.length = 0;
195   if (event.command === cvox.BrailleKeyCommand.STANDARD_KEY) {
196     if (event.standardKeyCode === 'Backspace' &&
197         !event.altKey && !event.ctrlKey && !event.shiftKey &&
198         this.onBackspace_()) {
199       return true;
200     } else {
201       this.sendKeyEventPair_(event);
202       return true;
203     }
204   }
205   return false;
210  * Returns how the value of the currently displayed content should be expanded
211  * given the current input state.
212  * @return {cvox.ExpandingBrailleTranslator.ExpansionType}
213  *     The current expansion type.
214  */
215 cvox.BrailleInputHandler.prototype.getExpansionType = function() {
216   if (this.inAlwaysUncontractedContext_()) {
217     return cvox.ExpandingBrailleTranslator.ExpansionType.ALL;
218   }
219   if (this.cells_.length > 0 &&
220       this.activeTranslator_ ===
221       this.translatorManager_.getDefaultTranslator()) {
222     return cvox.ExpandingBrailleTranslator.ExpansionType.NONE;
223   }
224   return cvox.ExpandingBrailleTranslator.ExpansionType.SELECTION;
229  * @return {boolean} {@code true} if we have an input context and uncontracted
230  *     braille should always be used for that context.
231  * @private
232  */
233 cvox.BrailleInputHandler.prototype.inAlwaysUncontractedContext_ = function() {
234   if (this.inputContext_) {
235     var inputType = this.inputContext_.type;
236     return inputType === 'url' || inputType === 'email';
237   }
238   return false;
243  * Called when a user typed a braille cell.
244  * @param {number} dots The dot pattern of the cell.
245  * @return {boolean} Whether the event was handled or should be allowed to
246  *    propagate further.
247  * @private
248  */
249 cvox.BrailleInputHandler.prototype.onBrailleDots_ = function(dots) {
250   if (!this.imeActive_) {
251     this.pendingCells_.push(dots);
252     return true;
253   }
254   if (!this.inputContext_ || !this.activeTranslator_) {
255     return false;
256   }
257   // Avoid accumulating cells forever when typing without moving the cursor
258   // by flushing the input when we see a blank cell.
259   // Note that this might switch to contracted if appropriate.
260   if (this.cells_.length > 0 && this.cells_[this.cells_.length - 1] == 0) {
261     this.resetText_();
262   }
263   this.cells_.push(dots);
264   this.updateText_();
265   return true;
270  * Handles the backspace key by deleting the last typed cell if possible.
271  * @return {boolean} {@code true} if the event was handled, {@code false}
272  *     if it wasn't and should propagate further.
273  * @private
274  */
275 cvox.BrailleInputHandler.prototype.onBackspace_ = function() {
276   if (this.imeActive_ && this.cells_.length > 0) {
277     --this.cells_.length;
278     this.updateText_();
279     return true;
280   }
281   return false;
286  * Updates the translated text based on the current cells and sends the
287  * delta to the IME.
288  * @private
289  */
290 cvox.BrailleInputHandler.prototype.updateText_ = function() {
291   var cellsBuffer = new Uint8Array(this.cells_).buffer;
292   this.activeTranslator_.backTranslate(cellsBuffer, goog.bind(function(result) {
293     if (result === null) {
294       console.error('Error when backtranslating braille cells');
295       return;
296     }
297     var oldLength = this.text_.length;
298     // Find the common prefix of the old and new text.
299     var commonPrefixLength = this.longestCommonPrefixLength_(
300         this.text_, result);
301     this.text_ = result;
302     // How many characters we need to delete from the existing text to replace
303     // them with characters from the new text.
304     var deleteLength = oldLength - commonPrefixLength;
305     // New text, if any, to insert after deleting the deleteLength characters
306     // before the cursor.
307     var toInsert = result.substring(commonPrefixLength);
308     if (deleteLength > 0 || toInsert.length > 0) {
309       // After deleting, we expect this text to be present before the cursor.
310       var textBeforeAfterDelete = this.currentTextBefore_.substring(
311           0, this.currentTextBefore_.length - deleteLength);
312       if (deleteLength > 0) {
313         // Queue this text up to be ignored when the change comes in.
314         this.pendingTextsBefore_.push(textBeforeAfterDelete);
315       }
316       if (toInsert.length > 0) {
317         // Likewise, queue up what we expect to be before the cursor after
318         // the replacement text is inserted.
319         this.pendingTextsBefore_.push(textBeforeAfterDelete + toInsert);
320       }
321       // Send the replace operation to be performed asynchronously by
322       // the IME.
323       this.postImeMessage_({type: 'replaceText',
324                             contextID: this.inputContext_.contextID,
325                             deleteBefore: deleteLength,
326                             newText: toInsert});
327     }
328   }, this));
333  * Resets the pending braille input and text.
334  * @private
335  */
336 cvox.BrailleInputHandler.prototype.resetText_ = function() {
337   this.cells_.length = 0;
338   this.text_ = '';
339   this.pendingTextsBefore_.length = 0;
340   this.updateActiveTranslator_();
345  * Updates the active translator based on the current input context.
346  * @private
347  */
348 cvox.BrailleInputHandler.prototype.updateActiveTranslator_ = function() {
349   this.activeTranslator_ = this.translatorManager_.getDefaultTranslator();
350   var uncontractedTranslator =
351       this.translatorManager_.getUncontractedTranslator();
352   if (uncontractedTranslator) {
353     var textBefore = this.currentTextBefore_;
354     var textAfter = this.currentTextAfter_;
355     if (this.inAlwaysUncontractedContext_() ||
356         (textBefore.length > 0 && /\S$/.test(textBefore)) ||
357         (textAfter.length > 0 && /^\S/.test(textAfter))) {
358       this.activeTranslator_ = uncontractedTranslator;
359     }
360   }
365  * Called when another extension connects to this extension.  Accepts
366  * connections from the ChromeOS builtin Braille IME and ignores connections
367  * from other extensions.
368  * @param {Port} port The port used to communicate with the other extension.
369  * @private
370  */
371 cvox.BrailleInputHandler.prototype.onImeConnect_ = function(port) {
372   if (port.name !== cvox.BrailleInputHandler.IME_PORT_NAME_ ||
373       port.sender.id !== cvox.BrailleInputHandler.IME_EXTENSION_ID_) {
374     return;
375   }
376   if (this.imePort_) {
377     this.imePort_.disconnect();
378   }
379   port.onDisconnect.addListener(goog.bind(this.onImeDisconnect_, this, port));
380   port.onMessage.addListener(goog.bind(this.onImeMessage_, this));
381   this.imePort_ = port;
386  * Called when a message is received from the IME.
387  * @param {*} message The message.
388  * @private
389  */
390 cvox.BrailleInputHandler.prototype.onImeMessage_ = function(message) {
391   if (!goog.isObject(message)) {
392     console.error('Unexpected message from Braille IME: ',
393                   JSON.stringify(message));
394   }
395   switch (message.type) {
396     case 'activeState':
397       this.imeActive_ = message.active;
398       break;
399     case 'inputContext':
400       this.inputContext_ = message.context;
401       this.resetText_();
402       if (this.imeActive_ && this.inputContext_) {
403         this.pendingCells_.forEach(goog.bind(this.onBrailleDots_, this));
404       }
405       this.pendingCells_.length = 0;
406       break;
407     case 'brailleDots':
408       this.onBrailleDots_(message['dots']);
409       break;
410     case 'backspace':
411       // Note that we can't send the backspace key through the
412       // virtualKeyboardPrivate API in this case because it would then be
413       // processed by the IME again, leading to an infinite loop.
414       this.postImeMessage_(
415           {type: 'keyEventHandled', requestId: message['requestId'],
416            result: this.onBackspace_()});
417       break;
418     case 'reset':
419       this.resetText_();
420       break;
421     default:
422       console.error('Unexpected message from Braille IME: ',
423                     JSON.stringify(message));
424     break;
425   }
430  * Called when the IME port is disconnected.
431  * @param {Port} port The port that was disconnected.
432  * @private
433  */
434 cvox.BrailleInputHandler.prototype.onImeDisconnect_ = function(port) {
435   this.imePort_ = null;
436   this.resetText_();
437   this.imeActive_ = false;
438   this.inputContext_ = null;
443  * Posts a message to the IME.
444  * @param {Object} message The message.
445  * @return {boolean} {@code true} if the message was sent, {@code false} if
446  *     there was no connection open to the IME.
447  * @private
448  */
449 cvox.BrailleInputHandler.prototype.postImeMessage_ = function(message) {
450   if (this.imePort_) {
451     this.imePort_.postMessage(message);
452     return true;
453   }
454   return false;
459  * Sends a {@code keydown} key event followed by a {@code keyup} event
460  * corresponding to an event generated by the braille display.
461  * @param {!cvox.BrailleKeyEvent} event The braille key event to base the
462  *     key events on.
463  * @private
464  */
465 cvox.BrailleInputHandler.prototype.sendKeyEventPair_ = function(event) {
466   // Use the virtual keyboard API instead of the IME key event API
467   // so that these keys work even if the Braille IME is not active.
468   var keyName = /** @type {string} */ (event.standardKeyCode);
469   var numericCode = cvox.BrailleKeyEvent.keyCodeToLegacyCode(keyName);
470   if (!goog.isDef(numericCode)) {
471     throw Error('Unknown key code in event: ' + JSON.stringify(event));
472   }
473   var keyEvent = {
474     type: 'keydown',
475     keyCode: numericCode,
476     keyName: keyName,
477     charValue: cvox.BrailleKeyEvent.keyCodeToCharValue(keyName),
478     // See chrome/common/extensions/api/virtual_keyboard_private.json for
479     // these constants.
480     modifiers: (event.shiftKey ? 2 : 0) |
481         (event.ctrlKey ? 4 : 0) |
482         (event.altKey ? 8 : 0)
483   };
484   chrome.virtualKeyboardPrivate.sendKeyEvent(keyEvent);
485   keyEvent.type = 'keyup';
486   chrome.virtualKeyboardPrivate.sendKeyEvent(keyEvent);
491  * Returns the length of the longest common prefix of two strings.
492  * @param {string} first The first string.
493  * @param {string} second The second string.
494  * @return {number} The longest common prefix, which may be 0 for an
495  *     empty common prefix.
496  * @private
497  */
498 cvox.BrailleInputHandler.prototype.longestCommonPrefixLength_ = function(
499     first, second) {
500   var limit = Math.min(first.length, second.length);
501   var i;
502   for (i = 0; i < limit; ++i) {
503     if (first.charAt(i) != second.charAt(i)) {
504       break;
505     }
506   }
507   return i;