Add new certificateProvider extension API.
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / braille / braille_input_handler.js
blob5dc4c9a0ed9001c2a5a71942c25795b863c8de09
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('StringUtil');
14 goog.require('cvox.BrailleKeyCommand');
15 goog.require('cvox.BrailleKeyEvent');
16 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    * Text that currently precedes the first selection end-point.
51    * @type {string}
52    * @private
53    */
54   this.currentTextBefore_ = '';
55   /**
56    * Text that currently follows the last selection end-point.
57    * @type {string}
58    * @private
59    */
60   this.currentTextAfter_ = '';
61   /**
62    * Cells that were entered while the IME wasn't active.  These will be
63    * submitted once the IME becomes active and reports the current input field.
64    * This is necessary because the IME is activated on the first braille
65    * dots command, but we'll receive the command in parallel.  To work around
66    * the race, we store the cell entered until we can submit it to the IME.
67    * @type {!Array<number>}
68    * @private
69    */
70   this.pendingCells_ = [];
71   /**
72    * @type {cvox.BrailleInputHandler.EntryState_}
73    * @private
74    */
75   this.entryState_ = null;
76   /**
77    * @type {cvox.ExtraCellsSpan}
78    * @private
79    */
80   this.uncommittedCellsSpan_ = null;
81   /**
82    * @type {function()?}
83    * @private
84    */
85   this.uncommittedCellsChangedListener_ = null;
87   this.translatorManager_.addChangeListener(
88       this.commitAndClearEntryState_.bind(this));
91 /**
92  * The ID of the Braille IME extension built into Chrome OS.
93  * @const {string}
94  * @private
95  */
96 cvox.BrailleInputHandler.IME_EXTENSION_ID_ =
97     'jddehjeebkoimngcbdkaahpobgicbffp';
99 /**
100  * Name of the port to use for communicating with the Braille IME.
101  * @const {string}
102  * @private
103  */
104 cvox.BrailleInputHandler.IME_PORT_NAME_ = 'cvox.BrailleIme.Port';
107  * Regular expression that matches a string that starts with at least one
108  * non-whitespace character.
109  * @const {RegExp}
110  * @private
111  */
112 cvox.BrailleInputHandler.STARTS_WITH_NON_WHITESPACE_RE_ = /^\S/;
115  * Regular expression that matches a string that ends with at least one
116  * non-whitespace character.
117  * @const {RegExp}
118  * @private
119  */
120 cvox.BrailleInputHandler.ENDS_WITH_NON_WHITESPACE_RE_ = /\S$/;
122 cvox.BrailleInputHandler.prototype = {
123   /**
124    * Starts to listen for connections from the Chrome OS braille IME.
125    */
126   init: function() {
127     chrome.runtime.onConnectExternal.addListener(this.onImeConnect_.bind(this));
128   },
130   /**
131    * Called when the content on the braille display is updated.  Modifies the
132    * input state according to the new content.
133    * @param {cvox.Spannable} text Text, optionally with value and selection
134    *     spans.
135    * @param {function()} listener Called when the uncommitted cells
136    *     have changed.
137    */
138   onDisplayContentChanged: function(text, listener) {
139     var valueSpan = text.getSpanInstanceOf(cvox.ValueSpan);
140     var selectionSpan = text.getSpanInstanceOf(cvox.ValueSelectionSpan);
141     if (!(valueSpan && selectionSpan))
142       return;
143     // Don't call the old listener any further, since new content is being
144     // set.  If the old listener is not cleared here, it could be called
145     // spuriously if the entry state is cleared below.
146     this.uncommittedCellsChangedListener_ = null;
147     // The type casts are ok because the spans are known to exist.
148     var valueStart = /** @type {number} */ (text.getSpanStart(valueSpan));
149     var valueEnd = /** @type {number} */ (text.getSpanEnd(valueSpan));
150     var selectionStart =
151         /** @type {number} */ (text.getSpanStart(selectionSpan));
152     var selectionEnd = /** @type {number} */ (text.getSpanEnd(selectionSpan));
153     if (selectionStart < valueStart || selectionEnd > valueEnd) {
154       console.error('Selection outside of value in braille content');
155       this.clearEntryState_();
156       return;
157     }
158     var newTextBefore = text.toString().substring(valueStart, selectionStart);
159     if (this.currentTextBefore_ !== newTextBefore && this.entryState_)
160       this.entryState_.onTextBeforeChanged(newTextBefore);
161     this.currentTextBefore_ = newTextBefore;
162     this.currentTextAfter_ = text.toString().substring(selectionEnd, valueEnd);
163     this.uncommittedCellsSpan_ = new cvox.ExtraCellsSpan();
164     text.setSpan(this.uncommittedCellsSpan_, selectionStart, selectionStart);
165     if (this.entryState_ && this.entryState_.usesUncommittedCells) {
166       this.updateUncommittedCells_(
167           new Uint8Array(this.entryState_.cells_).buffer);
168     }
169     this.uncommittedCellsChangedListener_ = listener;
170   },
172   /**
173    * Handles braille key events used for input by editing the current input
174    * field appropriately.
175    * @param {!cvox.BrailleKeyEvent} event The key event.
176    * @return {boolean} {@code true} if the event was handled, {@code false}
177    *     if it should propagate further.
178    */
179   onBrailleKeyEvent: function(event) {
180     if (event.command === cvox.BrailleKeyCommand.DOTS)
181       return this.onBrailleDots_(/** @type {number} */(event.brailleDots));
182     // Any other braille command cancels the pending cells.
183     this.pendingCells_.length = 0;
184     if (event.command === cvox.BrailleKeyCommand.STANDARD_KEY) {
185       if (event.standardKeyCode === 'Backspace' &&
186           !event.altKey && !event.ctrlKey && !event.shiftKey &&
187           this.onBackspace_()) {
188         return true;
189       } else {
190         this.commitAndClearEntryState_();
191         this.sendKeyEventPair_(event);
192         return true;
193       }
194     }
195     return false;
196   },
198   /**
199    * Returns how the value of the currently displayed content should be
200    * expanded given the current input state.
201    * @return {cvox.ExpandingBrailleTranslator.ExpansionType}
202    *     The current expansion type.
203    */
204   getExpansionType: function() {
205     if (this.inAlwaysUncontractedContext_())
206       return cvox.ExpandingBrailleTranslator.ExpansionType.ALL;
207     if (this.entryState_ &&
208         this.entryState_.translator ===
209         this.translatorManager_.getDefaultTranslator()) {
210       return cvox.ExpandingBrailleTranslator.ExpansionType.NONE;
211     }
212     return cvox.ExpandingBrailleTranslator.ExpansionType.SELECTION;
213   },
215   /**
216    * @return {boolean} {@code true} if we have an input context and
217    *     uncontracted braille should always be used for that context.
218    * @private
219    */
220   inAlwaysUncontractedContext_: function() {
221     var inputType = this.inputContext_ ? this.inputContext_.type : '';
222     return inputType === 'url' || inputType === 'email';
223   },
225   /**
226    * Called when a user typed a braille cell.
227    * @param {number} dots The dot pattern of the cell.
228    * @return {boolean} Whether the event was handled or should be allowed to
229    *    propagate further.
230    * @private
231    */
232   onBrailleDots_: function(dots) {
233     if (!this.imeActive_) {
234       this.pendingCells_.push(dots);
235       return true;
236     }
237     if (!this.inputContext_)
238       return false;
239     if (!this.entryState_) {
240       if (!(this.entryState_ = this.createEntryState_()))
241         return false;
242     }
243     this.entryState_.appendCell(dots);
244     return true;
245   },
247   /**
248    * Handles the backspace key by deleting the last typed cell if possible.
249    * @return {boolean} {@code true} if the event was handled, {@code false}
250    *     if it wasn't and should propagate further.
251    * @private
252    */
253   onBackspace_: function() {
254     if (this.imeActive_ && this.entryState_) {
255       this.entryState_.deleteLastCell();
256       return true;
257     }
258     return false;
259   },
261   /**
262    * Creates a new empty {@code EntryState_} based on the current input context
263    * and surrounding text.
264    * @return {cvox.BrailleInputHandler.EntryState_} The newly created state
265    *     object, or null if it couldn't be created (e.g. if there's no braille
266    *     translator available yet).
267    * @private
268    */
269   createEntryState_: function() {
270     var translator = this.translatorManager_.getDefaultTranslator();
271     if (!translator)
272       return null;
273     var uncontractedTranslator =
274         this.translatorManager_.getUncontractedTranslator();
275     var constructor = cvox.BrailleInputHandler.EditsEntryState_;
276     if (uncontractedTranslator) {
277       var textBefore = this.currentTextBefore_;
278       var textAfter = this.currentTextAfter_;
279       if (this.inAlwaysUncontractedContext_() ||
280           (cvox.BrailleInputHandler.ENDS_WITH_NON_WHITESPACE_RE_.test(
281               textBefore)) ||
282           (cvox.BrailleInputHandler.STARTS_WITH_NON_WHITESPACE_RE_.test(
283               textAfter))) {
284         translator = uncontractedTranslator;
285       } else {
286         constructor = cvox.BrailleInputHandler.LateCommitEntryState_;
287       }
288     }
290     return new constructor(this, translator);
291   },
293   /**
294    * Commits the current entry state and clears it, if any.
295    * @private
296    */
297   commitAndClearEntryState_: function() {
298     if (this.entryState_) {
299       this.entryState_.commit();
300       this.clearEntryState_();
301     }
302   },
304   /**
305    * Clears the current entry state without committing it.
306    * @private
307    */
308   clearEntryState_: function() {
309     if (this.entryState_) {
310       if (this.entryState_.usesUncommittedCells)
311         this.updateUncommittedCells_(new ArrayBuffer(0));
312       this.entryState_.inputHandler_ = null;
313       this.entryState_ = null;
314     }
315   },
317   /**
318    * @param {ArrayBuffer} cells
319    * @private
320    */
321   updateUncommittedCells_: function(cells) {
322     if (this.uncommittedCellsSpan_)
323       this.uncommittedCellsSpan_.cells = cells;
324     if (this.uncommittedCellsChangedListener_)
325       this.uncommittedCellsChangedListener_();
326   },
328   /**
329    * Called when another extension connects to this extension.  Accepts
330    * connections from the ChromeOS builtin Braille IME and ignores connections
331    * from other extensions.
332    * @param {Port} port The port used to communicate with the other extension.
333    * @private
334    */
335   onImeConnect_: function(port) {
336     if (port.name !== cvox.BrailleInputHandler.IME_PORT_NAME_ ||
337         port.sender.id !== cvox.BrailleInputHandler.IME_EXTENSION_ID_) {
338       return;
339     }
340     if (this.imePort_)
341       this.imePort_.disconnect();
342     port.onDisconnect.addListener(this.onImeDisconnect_.bind(this, port));
343     port.onMessage.addListener(this.onImeMessage_.bind(this));
344     this.imePort_ = port;
345   },
347   /**
348    * Called when a message is received from the IME.
349    * @param {*} message The message.
350    * @private
351    */
352   onImeMessage_: function(message) {
353     if (!goog.isObject(message)) {
354       console.error('Unexpected message from Braille IME: ',
355                     JSON.stringify(message));
356     }
357     switch (message.type) {
358       case 'activeState':
359         this.imeActive_ = message.active;
360         break;
361       case 'inputContext':
362         this.inputContext_ = message.context;
363         this.clearEntryState_();
364         if (this.imeActive_ && this.inputContext_)
365           this.pendingCells_.forEach(this.onBrailleDots_, this);
366         this.pendingCells_.length = 0;
367         break;
368       case 'brailleDots':
369         this.onBrailleDots_(message['dots']);
370         break;
371       case 'backspace':
372         // Note that we can't send the backspace key through the
373         // virtualKeyboardPrivate API in this case because it would then be
374         // processed by the IME again, leading to an infinite loop.
375         this.postImeMessage_(
376             {type: 'keyEventHandled', requestId: message['requestId'],
377              result: this.onBackspace_()});
378         break;
379       case 'reset':
380         this.clearEntryState_();
381         break;
382       default:
383         console.error('Unexpected message from Braille IME: ',
384                       JSON.stringify(message));
385       break;
386     }
387   },
389   /**
390    * Called when the IME port is disconnected.
391    * @param {Port} port The port that was disconnected.
392    * @private
393    */
394   onImeDisconnect_: function(port) {
395     this.imePort_ = null;
396     this.clearEntryState_();
397     this.imeActive_ = false;
398     this.inputContext_ = null;
399   },
401   /**
402    * Posts a message to the IME.
403    * @param {Object} message The message.
404    * @return {boolean} {@code true} if the message was sent, {@code false} if
405    *     there was no connection open to the IME.
406    * @private
407    */
408   postImeMessage_: function(message) {
409     if (this.imePort_) {
410       this.imePort_.postMessage(message);
411       return true;
412     }
413     return false;
414   },
416   /**
417    * Sends a {@code keydown} key event followed by a {@code keyup} event
418    * corresponding to an event generated by the braille display.
419    * @param {!cvox.BrailleKeyEvent} event The braille key event to base the
420    *     key events on.
421    * @private
422    */
423   sendKeyEventPair_: function(event) {
424     // Use the virtual keyboard API instead of the IME key event API
425     // so that these keys work even if the Braille IME is not active.
426     var keyName = /** @type {string} */ (event.standardKeyCode);
427     var numericCode = cvox.BrailleKeyEvent.keyCodeToLegacyCode(keyName);
428     if (!goog.isDef(numericCode))
429       throw Error('Unknown key code in event: ' + JSON.stringify(event));
430     var keyEvent = {
431       type: 'keydown',
432       keyCode: numericCode,
433       keyName: keyName,
434       charValue: cvox.BrailleKeyEvent.keyCodeToCharValue(keyName),
435       // See chrome/common/extensions/api/virtual_keyboard_private.json for
436       // these constants.
437       modifiers: (event.shiftKey ? 2 : 0) |
438           (event.ctrlKey ? 4 : 0) |
439           (event.altKey ? 8 : 0)
440     };
441     chrome.virtualKeyboardPrivate.sendKeyEvent(keyEvent);
442     keyEvent.type = 'keyup';
443     chrome.virtualKeyboardPrivate.sendKeyEvent(keyEvent);
444   }
448  * The entry state is the state related to entering a series of braille cells
449  * without 'interruption', where interruption can be things like non braille
450  * keyboard input or unexpected changes to the text surrounding the cursor.
451  * @param {!cvox.BrailleInputHandler} inputHandler
452  * @param {!cvox.LibLouis.Translator} translator
453  * @constructor
454  * @private
455  */
456 cvox.BrailleInputHandler.EntryState_ = function(inputHandler, translator) {
457   /**
458    * @type {cvox.BrailleInputHandler}
459    * @private
460    */
461   this.inputHandler_ = inputHandler;
462   /**
463    * The translator currently used for typing, if
464    * {@code this.cells_.length > 0}.
465    * @type {!cvox.LibLouis.Translator}
466    * @private
467    */
468   this.translator_ = translator;
469   /**
470    * Braille cells that have been typed by the user so far.
471    * @type {!Array<number>}
472    * @private
473    */
474   this.cells_ = [];
475   /**
476    * Text resulting from translating {@code this.cells_}.
477    * @type {string}
478    * @private
479    */
480   this.text_ = '';
481   /**
482    * List of strings that we expect to be set as preceding text of the
483    * selection.  This is populated when we send text changes to the IME so that
484    * our own changes don't reset the pending cells.
485    * @type {!Array<string>}
486    * @private
487    */
488   this.pendingTextsBefore_ = [];
491 cvox.BrailleInputHandler.EntryState_.prototype = {
492   /**
493    * @return {!cvox.LibLouis.Translator} The translator used by this entry
494    *     state.  This doesn't change for a given object.
495    */
496   get translator() {
497     return this.translator_;
498   },
500   /**
501    * Appends a braille cell to the current input and updates the text if
502    * necessary.
503    * @param {number} cell The braille cell to append.
504    */
505   appendCell: function(cell) {
506     this.cells_.push(cell);
507     this.updateText_();
508   },
510   /**
511    * Deletes the last cell of the input and updates the text if neccary.
512    * If there's no more input in this object afterwards, clears the entry state
513    * of the input handler.
514    */
515   deleteLastCell: function() {
516     if (--this.cells_.length <= 0) {
517       this.sendTextChange_('');
518       this.inputHandler_.clearEntryState_();
519       return;
520     }
521     this.updateText_();
522   },
524   /**
525    * Called when the text before the cursor changes giving this object a
526    * chance to clear the entry state of the input handler if the change
527    * wasn't expected.
528    * @param {string} newText New text before the cursor.
529    */
530   onTextBeforeChanged: function(newText) {
531     // See if we are expecting this change as a result of one of our own edits.
532     // Allow changes to be coalesced by the input system in an attempt to not
533     // be too brittle.
534     for (var i = 0; i < this.pendingTextsBefore_.length; ++i) {
535       if (newText === this.pendingTextsBefore_[i]) {
536         // Delete all previous expected changes and ignore this one.
537         this.pendingTextsBefore_.splice(0, i + 1);
538         return;
539       }
540     }
541     // There was an actual text change (or cursor movement) that we hadn't
542     // caused ourselves, reset any pending input.
543     this.inputHandler_.clearEntryState_();
544   },
546   /**
547    * Makes sure the current text is permanently added to the edit field.
548    * After this call, this object should be abandoned.
549    */
550   commit: function() {
551   },
553   /**
554    * @return {boolean} true if the entry state uses uncommitted cells.
555    */
556   get usesUncommittedCells() {
557     return false;
558   },
560   /**
561    * Updates the translated text based on the current cells and sends the
562    * delta to the IME.
563    * @private
564    */
565   updateText_: function() {
566     var cellsBuffer = new Uint8Array(this.cells_).buffer;
567     var commit = this.lastCellIsBlank_;
568     if (!commit && this.usesUncommittedCells)
569       this.inputHandler_.updateUncommittedCells_(cellsBuffer);
570     this.translator_.backTranslate(cellsBuffer, function(result) {
571       if (result === null) {
572         console.error('Error when backtranslating braille cells');
573         return;
574       }
575       if (!this.inputHandler_)
576         return;
577       this.sendTextChange_(result);
578       this.text_ = result;
579       if (commit)
580         this.inputHandler_.commitAndClearEntryState_();
581     }.bind(this));
582   },
584   /**
585    * @return {boolean}
586    * @private
587    */
588   get lastCellIsBlank_() {
589     return this.cells_[this.cells_.length - 1] === 0;
590   },
592   /**
593    * Sends new text to the IME.  This dhould be overriden by subclasses.
594    * The old text is still available in the {@code text_} property.
595    * @param {string} newText Text to send.
596    * @private
597    */
598   sendTextChange_: function(newText) {
599   }
603  * Entry state that uses {@code deleteSurroundingText} and {@code commitText}
604  * calls to the IME to update the currently enetered text.
605  * @param {!cvox.BrailleInputHandler} inputHandler
606  * @param {!cvox.LibLouis.Translator} translator
607  * @constructor
608  * @extends {cvox.BrailleInputHandler.EntryState_}
609  * @private
610  */
611 cvox.BrailleInputHandler.EditsEntryState_ = function(
612     inputHandler, translator) {
613   cvox.BrailleInputHandler.EntryState_.call(this, inputHandler, translator);
616 cvox.BrailleInputHandler.EditsEntryState_.prototype = {
617   __proto__: cvox.BrailleInputHandler.EntryState_.prototype,
619   /** @override */
620   sendTextChange_: function(newText) {
621     var oldText = this.text_;
622     // Find the common prefix of the old and new text.
623     var commonPrefixLength = StringUtil.longestCommonPrefixLength(
624         oldText, newText);
625     // How many characters we need to delete from the existing text to replace
626     // them with characters from the new text.
627     var deleteLength = oldText.length - commonPrefixLength;
628     // New text, if any, to insert after deleting the deleteLength characters
629     // before the cursor.
630     var toInsert = newText.substring(commonPrefixLength);
631     if (deleteLength > 0 || toInsert.length > 0) {
632       // After deleting, we expect this text to be present before the cursor.
633       var textBeforeAfterDelete =
634           this.inputHandler_.currentTextBefore_.substring(
635               0, this.inputHandler_.currentTextBefore_.length - deleteLength);
636       if (deleteLength > 0) {
637         // Queue this text up to be ignored when the change comes in.
638         this.pendingTextsBefore_.push(textBeforeAfterDelete);
639       }
640       if (toInsert.length > 0) {
641         // Likewise, queue up what we expect to be before the cursor after
642         // the replacement text is inserted.
643         this.pendingTextsBefore_.push(textBeforeAfterDelete + toInsert);
644       }
645       // Send the replace operation to be performed asynchronously by the IME.
646       this.inputHandler_.postImeMessage_(
647           {type: 'replaceText',
648            contextID: this.inputHandler_.inputContext_.contextID,
649            deleteBefore: deleteLength,
650            newText: toInsert});
651     }
652   }
656  * Entry state that only updates the edit field when a blank cell is entered.
657  * During the input of a single 'word', the uncommitted text is stored by the
658  * IME.
659  * @param {!cvox.BrailleInputHandler} inputHandler
660  * @param {!cvox.LibLouis.Translator} translator
661  * @constructor
662  * @private
663  * @extends {cvox.BrailleInputHandler.EntryState_}
664  */
665 cvox.BrailleInputHandler.LateCommitEntryState_ = function(
666     inputHandler, translator) {
667   cvox.BrailleInputHandler.EntryState_.call(this, inputHandler, translator);
670 cvox.BrailleInputHandler.LateCommitEntryState_.prototype = {
671   __proto__: cvox.BrailleInputHandler.EntryState_.prototype,
673   /** @override */
674   commit: function() {
675     this.inputHandler_.postImeMessage_(
676         {type: 'commitUncommitted',
677          contextID: this.inputHandler_.inputContext_.contextID});
678   },
680   /** @override */
681   get usesUncommittedCells() {
682     return true;
683   },
685   /** @override */
686   sendTextChange_: function(newText) {
687     this.inputHandler_.postImeMessage_(
688         {type: 'setUncommitted',
689          contextID: this.inputHandler_.inputContext_.contextID,
690          text: newText});
691   }