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('cvox.BrailleKeyCommand');
14 goog.require('cvox.BrailleKeyEvent');
15 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 * The translator currently used for typing, if
51 * {@code this.cells_.length > 0}.
52 * @type {cvox.LibLouis.Translator}
55 this.activeTranslator_ = null;
57 * Braille cells that have been typed by the user so far.
58 * @type {Array<number>}
63 * Text resulting from translating {@code this.cells_}.
69 * Text that currently precedes the first selection end-point.
73 this.currentTextBefore_ = '';
75 * Text that currently follows the last selection end-point.
79 this.currentTextAfter_ = '';
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>}
87 this.pendingTextsBefore_ = [];
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>}
97 this.pendingCells_ = [];
99 this.translatorManager_.addChangeListener(this.resetText_.bind(this));
104 * The ID of the Braille IME extension built into Chrome OS.
108 cvox.BrailleInputHandler.IME_EXTENSION_ID_ =
109 'jddehjeebkoimngcbdkaahpobgicbffp';
113 * Name of the port to use for communicating with the Braille IME.
117 cvox.BrailleInputHandler.IME_PORT_NAME_ = 'cvox.BrailleIme.Port';
121 * Starts to listen for connections from the ChromeOS braille IME.
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
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)) {
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));
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');
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_) {
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
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);
173 // There was an actual text change (or cursor movement) that we hadn't
174 // caused ourselves, reset any pending input.
177 this.updateActiveTranslator_();
183 * Handles braille key events used for input by editing the current input field
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.
189 cvox.BrailleInputHandler.prototype.onBrailleKeyEvent = function(event) {
190 if (event.command === cvox.BrailleKeyCommand.DOTS) {
191 return this.onBrailleDots_(/** @type {number} */(event.brailleDots));
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_()) {
201 this.sendKeyEventPair_(event);
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.
215 cvox.BrailleInputHandler.prototype.getExpansionType = function() {
216 if (this.inAlwaysUncontractedContext_()) {
217 return cvox.ExpandingBrailleTranslator.ExpansionType.ALL;
219 if (this.cells_.length > 0 &&
220 this.activeTranslator_ ===
221 this.translatorManager_.getDefaultTranslator()) {
222 return cvox.ExpandingBrailleTranslator.ExpansionType.NONE;
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.
233 cvox.BrailleInputHandler.prototype.inAlwaysUncontractedContext_ = function() {
234 if (this.inputContext_) {
235 var inputType = this.inputContext_.type;
236 return inputType === 'url' || inputType === 'email';
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
249 cvox.BrailleInputHandler.prototype.onBrailleDots_ = function(dots) {
250 if (!this.imeActive_) {
251 this.pendingCells_.push(dots);
254 if (!this.inputContext_ || !this.activeTranslator_) {
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) {
263 this.cells_.push(dots);
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.
275 cvox.BrailleInputHandler.prototype.onBackspace_ = function() {
276 if (this.imeActive_ && this.cells_.length > 0) {
277 --this.cells_.length;
286 * Updates the translated text based on the current cells and sends the
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');
297 var oldLength = this.text_.length;
298 // Find the common prefix of the old and new text.
299 var commonPrefixLength = this.longestCommonPrefixLength_(
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);
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);
321 // Send the replace operation to be performed asynchronously by
323 this.postImeMessage_({type: 'replaceText',
324 contextID: this.inputContext_.contextID,
325 deleteBefore: deleteLength,
333 * Resets the pending braille input and text.
336 cvox.BrailleInputHandler.prototype.resetText_ = function() {
337 this.cells_.length = 0;
339 this.pendingTextsBefore_.length = 0;
340 this.updateActiveTranslator_();
345 * Updates the active translator based on the current input context.
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;
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.
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_) {
377 this.imePort_.disconnect();
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.
390 cvox.BrailleInputHandler.prototype.onImeMessage_ = function(message) {
391 if (!goog.isObject(message)) {
392 console.error('Unexpected message from Braille IME: ',
393 JSON.stringify(message));
395 switch (message.type) {
397 this.imeActive_ = message.active;
400 this.inputContext_ = message.context;
402 if (this.imeActive_ && this.inputContext_) {
403 this.pendingCells_.forEach(goog.bind(this.onBrailleDots_, this));
405 this.pendingCells_.length = 0;
408 this.onBrailleDots_(message['dots']);
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_()});
422 console.error('Unexpected message from Braille IME: ',
423 JSON.stringify(message));
430 * Called when the IME port is disconnected.
431 * @param {Port} port The port that was disconnected.
434 cvox.BrailleInputHandler.prototype.onImeDisconnect_ = function(port) {
435 this.imePort_ = null;
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.
449 cvox.BrailleInputHandler.prototype.postImeMessage_ = function(message) {
451 this.imePort_.postMessage(message);
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
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));
475 keyCode: numericCode,
477 charValue: cvox.BrailleKeyEvent.keyCodeToCharValue(keyName),
478 // See chrome/common/extensions/api/virtual_keyboard_private.json for
480 modifiers: (event.shiftKey ? 2 : 0) |
481 (event.ctrlKey ? 4 : 0) |
482 (event.altKey ? 8 : 0)
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.
498 cvox.BrailleInputHandler.prototype.longestCommonPrefixLength_ = function(
500 var limit = Math.min(first.length, second.length);
502 for (i = 0; i < limit; ++i) {
503 if (first.charAt(i) != second.charAt(i)) {