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.
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.
11 goog
.provide('cvox.BrailleInputHandler');
13 goog
.require('StringUtil');
14 goog
.require('cvox.BrailleKeyCommand');
15 goog
.require('cvox.BrailleKeyEvent');
16 goog
.require('cvox.ExpandingBrailleTranslator');
19 * @param {!cvox.BrailleTranslatorManager} translatorManager Keeps track of
20 * the current braille translator(s).
23 cvox
.BrailleInputHandler = function(translatorManager
) {
25 * Port of the connected IME if any.
31 * {code true} when the Braille IME is connected and has signaled that it is
36 this.imeActive_
= false;
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}?}
43 this.inputContext_
= null;
45 * @type {!cvox.BrailleTranslatorManager}
48 this.translatorManager_
= translatorManager
;
50 * Text that currently precedes the first selection end-point.
54 this.currentTextBefore_
= '';
56 * Text that currently follows the last selection end-point.
60 this.currentTextAfter_
= '';
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>}
70 this.pendingCells_
= [];
72 * @type {cvox.BrailleInputHandler.EntryState_}
75 this.entryState_
= null;
77 * @type {cvox.ExtraCellsSpan}
80 this.uncommittedCellsSpan_
= null;
85 this.uncommittedCellsChangedListener_
= null;
87 this.translatorManager_
.addChangeListener(
88 this.commitAndClearEntryState_
.bind(this));
92 * The ID of the Braille IME extension built into Chrome OS.
96 cvox
.BrailleInputHandler
.IME_EXTENSION_ID_
=
97 'jddehjeebkoimngcbdkaahpobgicbffp';
100 * Name of the port to use for communicating with the Braille IME.
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.
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.
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.
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
135 * @param {function()} listener Called when the uncommitted cells
138 onDisplayContentChanged: function(text
, listener
) {
139 var valueSpan
= text
.getSpanInstanceOf(cvox
.ValueSpan
);
140 var selectionSpan
= text
.getSpanInstanceOf(cvox
.ValueSelectionSpan
);
141 if (!(valueSpan
&& selectionSpan
))
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
));
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_();
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_()) {
190 this.commitAndClearEntryState_();
191 this.sendKeyEventPair_(event
);
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.
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
232 onBrailleDots_: function(dots
) {
233 if (!this.imeActive_
) {
234 this.pendingCells_
.push(dots
);
237 if (!this.inputContext_
)
239 if (!this.entryState_
) {
240 if (!(this.entryState_
= this.createEntryState_()))
243 this.entryState_
.appendCell(dots
);
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.
253 onBackspace_: function() {
254 if (this.imeActive_
&& this.entryState_
) {
255 this.entryState_
.deleteLastCell();
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).
269 createEntryState_: function() {
270 var translator
= this.translatorManager_
.getDefaultTranslator();
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(
282 (cvox
.BrailleInputHandler
.STARTS_WITH_NON_WHITESPACE_RE_
.test(
284 translator
= uncontractedTranslator
;
286 constructor = cvox
.BrailleInputHandler
.LateCommitEntryState_
;
290 return new constructor(this, translator
);
294 * Commits the current entry state and clears it, if any.
297 commitAndClearEntryState_: function() {
298 if (this.entryState_
) {
299 this.entryState_
.commit();
300 this.clearEntryState_();
305 * Clears the current entry state without committing it.
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
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.
335 onImeConnect_: function(port
) {
336 if (port
.name
!== cvox
.BrailleInputHandler
.IME_PORT_NAME_
||
337 port
.sender
.id
!== cvox
.BrailleInputHandler
.IME_EXTENSION_ID_
) {
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.
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
) {
359 this.imeActive_
= message
.active
;
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;
369 this.onBrailleDots_(message
['dots']);
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_()});
380 this.clearEntryState_();
383 console
.error('Unexpected message from Braille IME: ',
384 JSON
.stringify(message
));
390 * Called when the IME port is disconnected.
391 * @param {Port} port The port that was disconnected.
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.
408 postImeMessage_: function(message
) {
410 this.imePort_
.postMessage(message
);
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
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
));
432 keyCode
: numericCode
,
434 charValue
: cvox
.BrailleKeyEvent
.keyCodeToCharValue(keyName
),
435 // See chrome/common/extensions/api/virtual_keyboard_private.json for
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
456 cvox
.BrailleInputHandler
.EntryState_ = function(inputHandler
, translator
) {
458 * @type {cvox.BrailleInputHandler}
461 this.inputHandler_
= inputHandler
;
463 * The translator currently used for typing, if
464 * {@code this.cells_.length > 0}.
465 * @type {!cvox.LibLouis.Translator}
468 this.translator_
= translator
;
470 * Braille cells that have been typed by the user so far.
471 * @type {!Array<number>}
476 * Text resulting from translating {@code this.cells_}.
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>}
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.
497 return this.translator_
;
501 * Appends a braille cell to the current input and updates the text if
503 * @param {number} cell The braille cell to append.
505 appendCell: function(cell
) {
506 this.cells_
.push(cell
);
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_();
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
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
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);
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.
554 * @return {boolean} true if the entry state uses uncommitted cells.
556 get usesUncommittedCells() {
561 * Updates the translated text based on the current cells and sends the
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');
575 if (!this.inputHandler_
)
577 this.sendTextChange_(result
);
580 this.inputHandler_
.commitAndClearEntryState_();
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.
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
608 * @extends {cvox.BrailleInputHandler.EntryState_}
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,
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(
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
,
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
659 * @param {!cvox.BrailleInputHandler} inputHandler
660 * @param {!cvox.LibLouis.Translator} translator
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,
675 this.inputHandler_
.postImeMessage_(
676 {type
: 'commitUncommitted',
677 contextID
: this.inputHandler_
.inputContext_
.contextID
});
681 get usesUncommittedCells() {
686 sendTextChange_: function(newText
) {
687 this.inputHandler_
.postImeMessage_(
688 {type
: 'setUncommitted',
689 contextID
: this.inputHandler_
.inputContext_
.contextID
,