Merge Chromium + Blink git repositories
[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
23 cvox.BrailleInputHandler = function(translatorManager) {
24 /**
25 * Port of the connected IME if any.
26 * @type {Port}
27 * @private
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
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
43 this.inputContext_ = null;
44 /**
45 * @type {!cvox.BrailleTranslatorManager}
46 * @private
48 this.translatorManager_ = translatorManager;
49 /**
50 * Text that currently precedes the first selection end-point.
51 * @type {string}
52 * @private
54 this.currentTextBefore_ = '';
55 /**
56 * Text that currently follows the last selection end-point.
57 * @type {string}
58 * @private
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
70 this.pendingCells_ = [];
71 /**
72 * @type {cvox.BrailleInputHandler.EntryState_}
73 * @private
75 this.entryState_ = null;
76 /**
77 * @type {cvox.ExtraCellsSpan}
78 * @private
80 this.uncommittedCellsSpan_ = null;
81 /**
82 * @type {function()?}
83 * @private
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
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
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
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
120 cvox.BrailleInputHandler.ENDS_WITH_NON_WHITESPACE_RE_ = /\S$/;
122 cvox.BrailleInputHandler.prototype = {
124 * Starts to listen for connections from the Chrome OS braille IME.
126 init: function() {
127 chrome.runtime.onConnectExternal.addListener(this.onImeConnect_.bind(this));
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.
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;
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);
169 this.uncommittedCellsChangedListener_ = listener;
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.
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;
195 return false;
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.
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;
212 return cvox.ExpandingBrailleTranslator.ExpansionType.SELECTION;
216 * @return {boolean} {@code true} if we have an input context and
217 * uncontracted braille should always be used for that context.
218 * @private
220 inAlwaysUncontractedContext_: function() {
221 var inputType = this.inputContext_ ? this.inputContext_.type : '';
222 return inputType === 'url' || inputType === 'email';
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
232 onBrailleDots_: function(dots) {
233 if (!this.imeActive_) {
234 this.pendingCells_.push(dots);
235 return true;
237 if (!this.inputContext_)
238 return false;
239 if (!this.entryState_) {
240 if (!(this.entryState_ = this.createEntryState_()))
241 return false;
243 this.entryState_.appendCell(dots);
244 return true;
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
253 onBackspace_: function() {
254 if (this.imeActive_ && this.entryState_) {
255 this.entryState_.deleteLastCell();
256 return true;
258 return false;
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
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_;
290 return new constructor(this, translator);
294 * Commits the current entry state and clears it, if any.
295 * @private
297 commitAndClearEntryState_: function() {
298 if (this.entryState_) {
299 this.entryState_.commit();
300 this.clearEntryState_();
305 * Clears the current entry state without committing it.
306 * @private
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;
318 * @param {ArrayBuffer} cells
319 * @private
321 updateUncommittedCells_: function(cells) {
322 if (this.uncommittedCellsSpan_)
323 this.uncommittedCellsSpan_.cells = cells;
324 if (this.uncommittedCellsChangedListener_)
325 this.uncommittedCellsChangedListener_();
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
335 onImeConnect_: function(port) {
336 if (port.name !== cvox.BrailleInputHandler.IME_PORT_NAME_ ||
337 port.sender.id !== cvox.BrailleInputHandler.IME_EXTENSION_ID_) {
338 return;
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;
348 * Called when a message is received from the IME.
349 * @param {*} message The message.
350 * @private
352 onImeMessage_: function(message) {
353 if (!goog.isObject(message)) {
354 console.error('Unexpected message from Braille IME: ',
355 JSON.stringify(message));
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;
390 * Called when the IME port is disconnected.
391 * @param {Port} port The port that was disconnected.
392 * @private
394 onImeDisconnect_: function(port) {
395 this.imePort_ = null;
396 this.clearEntryState_();
397 this.imeActive_ = false;
398 this.inputContext_ = null;
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
408 postImeMessage_: function(message) {
409 if (this.imePort_) {
410 this.imePort_.postMessage(message);
411 return true;
413 return false;
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
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)
441 chrome.virtualKeyboardPrivate.sendKeyEvent(keyEvent);
442 keyEvent.type = 'keyup';
443 chrome.virtualKeyboardPrivate.sendKeyEvent(keyEvent);
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
456 cvox.BrailleInputHandler.EntryState_ = function(inputHandler, translator) {
458 * @type {cvox.BrailleInputHandler}
459 * @private
461 this.inputHandler_ = inputHandler;
463 * The translator currently used for typing, if
464 * {@code this.cells_.length > 0}.
465 * @type {!cvox.LibLouis.Translator}
466 * @private
468 this.translator_ = translator;
470 * Braille cells that have been typed by the user so far.
471 * @type {!Array<number>}
472 * @private
474 this.cells_ = [];
476 * Text resulting from translating {@code this.cells_}.
477 * @type {string}
478 * @private
480 this.text_ = '';
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
488 this.pendingTextsBefore_ = [];
491 cvox.BrailleInputHandler.EntryState_.prototype = {
493 * @return {!cvox.LibLouis.Translator} The translator used by this entry
494 * state. This doesn't change for a given object.
496 get translator() {
497 return this.translator_;
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.
505 appendCell: function(cell) {
506 this.cells_.push(cell);
507 this.updateText_();
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.
515 deleteLastCell: function() {
516 if (--this.cells_.length <= 0) {
517 this.sendTextChange_('');
518 this.inputHandler_.clearEntryState_();
519 return;
521 this.updateText_();
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.
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;
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_();
547 * Makes sure the current text is permanently added to the edit field.
548 * After this call, this object should be abandoned.
550 commit: function() {
554 * @return {boolean} true if the entry state uses uncommitted cells.
556 get usesUncommittedCells() {
557 return false;
561 * Updates the translated text based on the current cells and sends the
562 * delta to the IME.
563 * @private
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;
575 if (!this.inputHandler_)
576 return;
577 this.sendTextChange_(result);
578 this.text_ = result;
579 if (commit)
580 this.inputHandler_.commitAndClearEntryState_();
581 }.bind(this));
585 * @return {boolean}
586 * @private
588 get lastCellIsBlank_() {
589 return this.cells_[this.cells_.length - 1] === 0;
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
598 sendTextChange_: function(newText) {
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
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);
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);
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});
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_}
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});
680 /** @override */
681 get usesUncommittedCells() {
682 return true;
685 /** @override */
686 sendTextChange_: function(newText) {
687 this.inputHandler_.postImeMessage_(
688 {type: 'setUncommitted',
689 contextID: this.inputHandler_.inputContext_.contextID,
690 text: newText});