Commit c8088647 authored by plundblad's avatar plundblad Committed by Commit bot

Make contracted braille input work in more contexts.

Before, when entering contracted (gradd 2) braille, backtracking was done
using the chrome.input.ime.deleteSurroundingText API.  This made text entry
visible in the text field immediately.  Unfortunately, some contexts don't
handle programmatic deletion well (Google Docs' editor is an example).

The new approach is to not commit the text ot the edit field until a word
has been entered or a non-braille key is entered. To make the user experience
better for the braille user, entered braille cells are shown on the
display right before the curent selection so that the user gets immediate
feedback when typing on the braille dipslay.

This approach has drawbacks as well. For example, clicking with a mouse
while typing could move focus before the IME has a chance to submit the
in-progress word. The assumption is that this would be less of a
problem in practice then not being able to use grade 2 at all in some
situations.

BUG=392854

Review URL: https://codereview.chromium.org/1039703002

Cr-Commit-Position: refs/heads/master@{#330711}
parent f51e7812
...@@ -18,7 +18,9 @@ ...@@ -18,7 +18,9 @@
* Sent on focus/blur to inform ChromeVox of the type of the current field. * Sent on focus/blur to inform ChromeVox of the type of the current field.
* In the latter case (blur), context is null. * In the latter case (blur), context is null.
* {type: 'reset'} * {type: 'reset'}
* Sent when the {@code onReset} IME event fires. * Sent when the {@code onReset} IME event fires or uncommitted text is
* committed without being triggered by ChromeVox (e.g. because of a
* key press).
* {type: 'brailleDots', dots: number} * {type: 'brailleDots', dots: number}
* Sent when the user typed a braille cell using the standard keyboard. * Sent when the user typed a braille cell using the standard keyboard.
* ChromeVox treats this similarly to entering braille input using the * ChromeVox treats this similarly to entering braille input using the
...@@ -35,6 +37,14 @@ ...@@ -35,6 +37,14 @@
* and inserts {@code newText}. {@code contextID} identifies the text field * and inserts {@code newText}. {@code contextID} identifies the text field
* to apply the update to (no change will happen if focus has moved to a * to apply the update to (no change will happen if focus has moved to a
* different field). * different field).
* {type: 'setUncommitted', contextID: number, text: string}
* Stores text for the field identified by contextID to be committed
* either as a result of a 'commitUncommitted' message or a by the IME
* unhandled key press event. Unlike 'replaceText', this does not send the
* uncommitted text to the input field, but instead stores it in the IME.
* {type: 'commitUncommitted', contextID: number}
* Commits any uncommitted text if it matches the given context ID.
* See 'setUncommitted' above.
* {type: 'keyEventHandled', requestId: string, result: boolean} * {type: 'keyEventHandled', requestId: string, result: boolean}
* Response to a {@code backspace} message indicating whether the * Response to a {@code backspace} message indicating whether the
* backspace was handled by ChromeVox or should be allowed to propagate * backspace was handled by ChromeVox or should be allowed to propagate
...@@ -130,6 +140,13 @@ BrailleIme.prototype = { ...@@ -130,6 +140,13 @@ BrailleIme.prototype = {
*/ */
port_: null, port_: null,
/**
* Uncommitted text and context ID.
* @type {?{contextID: number, text: string}}
* @private
*/
uncommitted_: null,
/** /**
* Registers event listeners in the chrome IME API. * Registers event listeners in the chrome IME API.
*/ */
...@@ -216,9 +233,8 @@ BrailleIme.prototype = { ...@@ -216,9 +233,8 @@ BrailleIme.prototype = {
*/ */
onKeyEvent_: function(engineID, event) { onKeyEvent_: function(engineID, event) {
var result = this.processKey_(event); var result = this.processKey_(event);
if (result !== undefined) { if (result !== undefined)
chrome.input.ime.keyEventHandled(event.requestId, result); this.keyEventHandled_(event.requestId, event.type, result);
}
}, },
/** /**
...@@ -356,7 +372,17 @@ BrailleIme.prototype = { ...@@ -356,7 +372,17 @@ BrailleIme.prototype = {
case 'keyEventHandled': case 'keyEventHandled':
message = message =
/** @type {{requestId: string, result: boolean}} */ (message); /** @type {{requestId: string, result: boolean}} */ (message);
chrome.input.ime.keyEventHandled(message.requestId, message.result); this.keyEventHandled_(message.requestId, 'keydown', message.result);
break;
case 'setUncommitted':
message =
/** @type {{contextID: number, text: string}} */ (message);
this.setUncommitted_(message.contextID, message.text);
break;
case 'commitUncommitted':
message =
/** @type {{contextID: number}} */ (message);
this.commitUncommitted_(message.contextID);
break; break;
default: default:
console.error('Unknown message from ChromeVox: ' + console.error('Unknown message from ChromeVox: ' +
...@@ -429,6 +455,42 @@ BrailleIme.prototype = { ...@@ -429,6 +455,42 @@ BrailleIme.prototype = {
} }
}, },
/**
* Responds to an asynchronous key event, indicating whether it was handled
* or not. If it wasn't handled, any uncommitted text is committed
* before sending the response to the IME API.
* @param {string} requestId Key event request id.
* @param {string} type Type of key event being responded to.
* @param {boolean} response Whether the IME handled the event.
*/
keyEventHandled_: function(requestId, type, response) {
if (!response && type === 'keydown' && this.uncommitted_) {
this.commitUncommitted_(this.uncommitted_.contextID);
this.sendToChromeVox_({type: 'reset'});
}
chrome.input.ime.keyEventHandled(requestId, response);
},
/**
* Stores uncommitted text that will be committed on any key press or
* when {@code commitUncommitted_} is called.
* @param {number} contextID of the current field.
* @param {string} text to store.
*/
setUncommitted_: function(contextID, text) {
this.uncommitted_ = {contextID: contextID, text: text};
},
/**
* Commits the last set uncommitted text if it matches the given context id.
* @param {number} contextID
*/
commitUncommitted_: function(contextID) {
if (this.uncommitted_ && contextID === this.uncommitted_.contextID)
chrome.input.ime.commitText(this.uncommitted_);
this.uncommitted_ = null;
},
/** /**
* Updates the menu items for this IME. * Updates the menu items for this IME.
*/ */
......
...@@ -276,3 +276,36 @@ TEST_F('BrailleImeUnitTest', 'ReplaceText', function() { ...@@ -276,3 +276,36 @@ TEST_F('BrailleImeUnitTest', 'ReplaceText', function() {
assertFalse(hasSelection); assertFalse(hasSelection);
assertEquals('Hi, good bye!', text); assertEquals('Hi, good bye!', text);
}); });
TEST_F('BrailleImeUnitTest', 'Uncommitted', function() {
var CONTEXT_ID = 1;
var text = '';
chrome.input.ime.commitText = function(params) {
assertEquals(CONTEXT_ID, params.contextID);
text += params.text;
};
var sendSetUncommitted = function(text) {
this.port.onMessage.dispatch(
{type: 'setUncommitted', contextID: CONTEXT_ID, text: text});
}.bind(this);
var sendCommitUncommitted = function(contextID) {
this.port.onMessage.dispatch(
{type: 'commitUncommitted', contextID: contextID});
}.bind(this);
this.activateIme();
sendSetUncommitted('Hi');
assertEquals('', text);
sendSetUncommitted('Hello');
sendCommitUncommitted(CONTEXT_ID);
assertEquals('Hello', text);
sendSetUncommitted(' there!');
sendCommitUncommitted(CONTEXT_ID + 1);
assertEquals('Hello', text);
sendSetUncommitted(' you!');
assertFalse(this.sendKeyDown('KeyY'));
assertEquals('Hello you!', text);
assertFalse(this.sendKeyUp('KeyY'));
assertEquals('Hello you!', text);
});
...@@ -73,9 +73,19 @@ cvox.BrailleInputHandler = function(translatorManager) { ...@@ -73,9 +73,19 @@ cvox.BrailleInputHandler = function(translatorManager) {
* @private * @private
*/ */
this.entryState_ = null; this.entryState_ = null;
/**
* @type {cvox.ExtraCellsSpan}
* @private
*/
this.uncommittedCellsSpan_ = null;
/**
* @type {function()?}
* @private
*/
this.uncommittedCellsChangedListener_ = null;
this.translatorManager_.addChangeListener( this.translatorManager_.addChangeListener(
this.clearEntryState_.bind(this)); this.commitAndClearEntryState_.bind(this));
}; };
/** /**
...@@ -122,12 +132,18 @@ cvox.BrailleInputHandler.prototype = { ...@@ -122,12 +132,18 @@ cvox.BrailleInputHandler.prototype = {
* input state according to the new content. * input state according to the new content.
* @param {cvox.Spannable} text Text, optionally with value and selection * @param {cvox.Spannable} text Text, optionally with value and selection
* spans. * spans.
* @param {function()} listener Called when the uncommitted cells
* have changed.
*/ */
onDisplayContentChanged: function(text) { onDisplayContentChanged: function(text, listener) {
var valueSpan = text.getSpanInstanceOf(cvox.ValueSpan); var valueSpan = text.getSpanInstanceOf(cvox.ValueSpan);
var selectionSpan = text.getSpanInstanceOf(cvox.ValueSelectionSpan); var selectionSpan = text.getSpanInstanceOf(cvox.ValueSelectionSpan);
if (!(valueSpan && selectionSpan)) if (!(valueSpan && selectionSpan))
return; return;
// Don't call the old listener any further, since new content is being
// set. If the old listener is not cleared here, it could be called
// spuriously if the entry state is cleared below.
this.uncommittedCellsChangedListener_ = null;
// The type casts are ok because the spans are known to exist. // The type casts are ok because the spans are known to exist.
var valueStart = /** @type {number} */ (text.getSpanStart(valueSpan)); var valueStart = /** @type {number} */ (text.getSpanStart(valueSpan));
var valueEnd = /** @type {number} */ (text.getSpanEnd(valueSpan)); var valueEnd = /** @type {number} */ (text.getSpanEnd(valueSpan));
...@@ -144,6 +160,13 @@ cvox.BrailleInputHandler.prototype = { ...@@ -144,6 +160,13 @@ cvox.BrailleInputHandler.prototype = {
this.entryState_.onTextBeforeChanged(newTextBefore); this.entryState_.onTextBeforeChanged(newTextBefore);
this.currentTextBefore_ = newTextBefore; this.currentTextBefore_ = newTextBefore;
this.currentTextAfter_ = text.toString().substring(selectionEnd, valueEnd); this.currentTextAfter_ = text.toString().substring(selectionEnd, valueEnd);
this.uncommittedCellsSpan_ = new cvox.ExtraCellsSpan();
text.setSpan(this.uncommittedCellsSpan_, selectionStart, selectionStart);
if (this.entryState_ && this.entryState_.usesUncommittedCells) {
this.updateUncommittedCells_(
new Uint8Array(this.entryState_.cells_).buffer);
}
this.uncommittedCellsChangedListener_ = listener;
}, },
/** /**
...@@ -164,7 +187,7 @@ cvox.BrailleInputHandler.prototype = { ...@@ -164,7 +187,7 @@ cvox.BrailleInputHandler.prototype = {
this.onBackspace_()) { this.onBackspace_()) {
return true; return true;
} else { } else {
this.clearEntryState_(); this.commitAndClearEntryState_();
this.sendKeyEventPair_(event); this.sendKeyEventPair_(event);
return true; return true;
} }
...@@ -213,14 +236,8 @@ cvox.BrailleInputHandler.prototype = { ...@@ -213,14 +236,8 @@ cvox.BrailleInputHandler.prototype = {
} }
if (!this.inputContext_) if (!this.inputContext_)
return false; return false;
// Avoid accumulating cells forever when typing without moving the cursor
// by flushing the input when we see a blank cell.
// Note that this might switch to contracted if appropriate.
if (this.entryState_ && this.entryState_.lastCellIsBlank())
this.clearEntryState_();
if (!this.entryState_) { if (!this.entryState_) {
this.entryState_ = this.createEntryState_(); if (!(this.entryState_ = this.createEntryState_()))
if (!this.entryState_)
return false; return false;
} }
this.entryState_.appendCell(dots); this.entryState_.appendCell(dots);
...@@ -255,6 +272,7 @@ cvox.BrailleInputHandler.prototype = { ...@@ -255,6 +272,7 @@ cvox.BrailleInputHandler.prototype = {
return null; return null;
var uncontractedTranslator = var uncontractedTranslator =
this.translatorManager_.getUncontractedTranslator(); this.translatorManager_.getUncontractedTranslator();
var constructor = cvox.BrailleInputHandler.EditsEntryState_;
if (uncontractedTranslator) { if (uncontractedTranslator) {
var textBefore = this.currentTextBefore_; var textBefore = this.currentTextBefore_;
var textAfter = this.currentTextAfter_; var textAfter = this.currentTextAfter_;
...@@ -264,10 +282,23 @@ cvox.BrailleInputHandler.prototype = { ...@@ -264,10 +282,23 @@ cvox.BrailleInputHandler.prototype = {
(cvox.BrailleInputHandler.STARTS_WITH_NON_WHITESPACE_RE_.test( (cvox.BrailleInputHandler.STARTS_WITH_NON_WHITESPACE_RE_.test(
textAfter))) { textAfter))) {
translator = uncontractedTranslator; translator = uncontractedTranslator;
} else {
constructor = cvox.BrailleInputHandler.LateCommitEntryState_;
} }
} }
return new cvox.BrailleInputHandler.EditsEntryState_(this, translator); return new constructor(this, translator);
},
/**
* Commits the current entry state and clears it, if any.
* @private
*/
commitAndClearEntryState_: function() {
if (this.entryState_) {
this.entryState_.commit();
this.clearEntryState_();
}
}, },
/** /**
...@@ -276,11 +307,24 @@ cvox.BrailleInputHandler.prototype = { ...@@ -276,11 +307,24 @@ cvox.BrailleInputHandler.prototype = {
*/ */
clearEntryState_: function() { clearEntryState_: function() {
if (this.entryState_) { if (this.entryState_) {
if (this.entryState_.usesUncommittedCells)
this.updateUncommittedCells_(new ArrayBuffer(0));
this.entryState_.inputHandler_ = null; this.entryState_.inputHandler_ = null;
this.entryState_ = null; this.entryState_ = null;
} }
}, },
/**
* @param {ArrayBuffer} cells
* @private
*/
updateUncommittedCells_: function(cells) {
if (this.uncommittedCellsSpan_)
this.uncommittedCellsSpan_.cells = cells;
if (this.uncommittedCellsChangedListener_)
this.uncommittedCellsChangedListener_();
},
/** /**
* Called when another extension connects to this extension. Accepts * Called when another extension connects to this extension. Accepts
* connections from the ChromeOS builtin Braille IME and ignores connections * connections from the ChromeOS builtin Braille IME and ignores connections
...@@ -499,9 +543,18 @@ cvox.BrailleInputHandler.EntryState_.prototype = { ...@@ -499,9 +543,18 @@ cvox.BrailleInputHandler.EntryState_.prototype = {
this.inputHandler_.clearEntryState_(); this.inputHandler_.clearEntryState_();
}, },
/** @return {boolean} */ /**
lastCellIsBlank: function() { * Makes sure the current text is permanently added to the edit field.
return this.cells_[this.cells_.length - 1] === 0; * After this call, this object should be abandoned.
*/
commit: function() {
},
/**
* @return {boolean} true if the entry state uses uncommitted cells.
*/
get usesUncommittedCells() {
return false;
}, },
/** /**
...@@ -511,6 +564,9 @@ cvox.BrailleInputHandler.EntryState_.prototype = { ...@@ -511,6 +564,9 @@ cvox.BrailleInputHandler.EntryState_.prototype = {
*/ */
updateText_: function() { updateText_: function() {
var cellsBuffer = new Uint8Array(this.cells_).buffer; var cellsBuffer = new Uint8Array(this.cells_).buffer;
var commit = this.lastCellIsBlank_;
if (!commit && this.usesUncommittedCells)
this.inputHandler_.updateUncommittedCells_(cellsBuffer);
this.translator_.backTranslate(cellsBuffer, function(result) { this.translator_.backTranslate(cellsBuffer, function(result) {
if (result === null) { if (result === null) {
console.error('Error when backtranslating braille cells'); console.error('Error when backtranslating braille cells');
...@@ -520,9 +576,19 @@ cvox.BrailleInputHandler.EntryState_.prototype = { ...@@ -520,9 +576,19 @@ cvox.BrailleInputHandler.EntryState_.prototype = {
return; return;
this.sendTextChange_(result); this.sendTextChange_(result);
this.text_ = result; this.text_ = result;
if (commit)
this.inputHandler_.commitAndClearEntryState_();
}.bind(this)); }.bind(this));
}, },
/**
* @return {boolean}
* @private
*/
get lastCellIsBlank_() {
return this.cells_[this.cells_.length - 1] === 0;
},
/** /**
* Sends new text to the IME. This dhould be overriden by subclasses. * Sends new text to the IME. This dhould be overriden by subclasses.
* The old text is still available in the {@code text_} property. * The old text is still available in the {@code text_} property.
...@@ -585,3 +651,42 @@ cvox.BrailleInputHandler.EditsEntryState_.prototype = { ...@@ -585,3 +651,42 @@ cvox.BrailleInputHandler.EditsEntryState_.prototype = {
} }
} }
}; };
/**
* Entry state that only updates the edit field when a blank cell is entered.
* During the input of a single 'word', the uncommitted text is stored by the
* IME.
* @param {!cvox.BrailleInputHandler} inputHandler
* @param {!cvox.LibLouis.Translator} translator
* @constructor
* @private
* @extends {cvox.BrailleInputHandler.EntryState_}
*/
cvox.BrailleInputHandler.LateCommitEntryState_ = function(
inputHandler, translator) {
cvox.BrailleInputHandler.EntryState_.call(this, inputHandler, translator);
};
cvox.BrailleInputHandler.LateCommitEntryState_.prototype = {
__proto__: cvox.BrailleInputHandler.EntryState_.prototype,
/** @override */
commit: function() {
this.inputHandler_.postImeMessage_(
{type: 'commitUncommitted',
contextID: this.inputHandler_.inputContext_.contextID});
},
/** @override */
get usesUncommittedCells() {
return true;
},
/** @override */
sendTextChange_: function(newText) {
this.inputHandler_.postImeMessage_(
{type: 'setUncommitted',
contextID: this.inputHandler_.inputContext_.contextID,
text: newText});
}
};
...@@ -38,6 +38,10 @@ function FakeEditor(port, inputHandler) { ...@@ -38,6 +38,10 @@ function FakeEditor(port, inputHandler) {
this.contextID_ = 0; this.contextID_ = 0;
/** @private {boolean} */ /** @private {boolean} */
this.allowDeletes_ = false; this.allowDeletes_ = false;
/** @private {string} */
this.uncommittedText_ = '';
/** @private {?Array<number>} */
this.extraCells_ = [];
port.postMessage = goog.bind(this.handleMessage_, this); port.postMessage = goog.bind(this.handleMessage_, this);
} }
...@@ -133,6 +137,25 @@ FakeEditor.prototype.assertContentIs = function( ...@@ -133,6 +137,25 @@ FakeEditor.prototype.assertContentIs = function(
}; };
/**
* Asserts that the uncommitted text last sent to the IME is the given text.
* @param {string} text
*/
FakeEditor.prototype.assertUncommittedTextIs = function(text) {
assertEquals(text, this.uncommittedText_);
};
/**
* Asserts that the input handler has added 'extra cells' for uncommitted
* text into the braille content.
* @param {string} cells Cells as a space-separated list of numbers.
*/
FakeEditor.prototype.assertExtraCellsAre = function(cells) {
assertEqualsJSON(cellsToArray(cells), this.extraCells_);
};
/** /**
* Sends a message from the IME to the input handler. * Sends a message from the IME to the input handler.
* @param {Object} msg The message to send. * @param {Object} msg The message to send.
...@@ -151,9 +174,17 @@ FakeEditor.prototype.message_ = function(msg) { ...@@ -151,9 +174,17 @@ FakeEditor.prototype.message_ = function(msg) {
* @private * @private
*/ */
FakeEditor.prototype.callOnDisplayContentChanged_ = function() { FakeEditor.prototype.callOnDisplayContentChanged_ = function() {
this.inputHandler_.onDisplayContentChanged( var content = cvox.BrailleUtil.createValue(
cvox.BrailleUtil.createValue( this.text_, this.selectionStart_, this.selectionEnd_)
this.text_, this.selectionStart_, this.selectionEnd_)); var grabExtraCells = function() {
var span = content.getSpanInstanceOf(cvox.ExtraCellsSpan);
assertNotEquals(null, span);
// Convert the ArrayBuffer to a normal array for easier comparision.
this.extraCells_ = Array.prototype.map.call(new Uint8Array(span.cells),
function(a) {return a;});
}.bind(this);
this.inputHandler_.onDisplayContentChanged(content, grabExtraCells);
grabExtraCells();
}; };
...@@ -186,23 +217,36 @@ FakeEditor.prototype.blur = function() { ...@@ -186,23 +217,36 @@ FakeEditor.prototype.blur = function() {
* @private * @private
*/ */
FakeEditor.prototype.handleMessage_ = function(msg) { FakeEditor.prototype.handleMessage_ = function(msg) {
assertEquals('replaceText', msg.type);
assertEquals(this.contextID_, msg.contextID); assertEquals(this.contextID_, msg.contextID);
var deleteBefore = msg.deleteBefore; switch(msg.type) {
var newText = msg.newText; case 'replaceText':
assertTrue(goog.isNumber(deleteBefore)); var deleteBefore = msg.deleteBefore;
assertTrue(goog.isString(newText)); var newText = msg.newText;
assertTrue(deleteBefore <= this.selectionStart_); assertTrue(goog.isNumber(deleteBefore));
if (deleteBefore > 0) { assertTrue(goog.isString(newText));
assertTrue(this.allowDeletes_); assertTrue(deleteBefore <= this.selectionStart_);
this.text_ = if (deleteBefore > 0) {
this.text_.substring(0, this.selectionStart_ - deleteBefore) + assertTrue(this.allowDeletes_);
this.text_.substring(this.selectionEnd_); this.text_ =
this.selectionStart_ -= deleteBefore; this.text_.substring(0, this.selectionStart_ - deleteBefore) +
this.selectionEnd_ = this.selectionStart_; this.text_.substring(this.selectionEnd_);
this.callOnDisplayContentChanged_(); this.selectionStart_ -= deleteBefore;
this.selectionEnd_ = this.selectionStart_;
this.callOnDisplayContentChanged_();
}
this.insert(newText);
break;
case 'setUncommitted':
assertTrue(goog.isString(msg.text));
this.uncommittedText_ = msg.text;
break;
case 'commitUncommitted':
this.insert(this.uncommittedText_);
this.uncommittedText_ = '';
break;
default:
throw new Error('Unexpected message to IME: ' + JSON.stringify(msg));
} }
this.insert(newText);
}; };
/* /*
...@@ -362,6 +406,8 @@ FakeTranslatorManager.prototype = { ...@@ -362,6 +406,8 @@ FakeTranslatorManager.prototype = {
* pattern (dot 1 uses bit 0, etc). * pattern (dot 1 uses bit 0, etc).
*/ */
function cellsToArray(cells) { function cellsToArray(cells) {
if (!cells)
return [];
return cells.split(/\s+/).map(function(cellString) { return cells.split(/\s+/).map(function(cellString) {
var cell = 0; var cell = 0;
assertTrue(cellString.length > 0); assertTrue(cellString.length > 0);
...@@ -532,25 +578,32 @@ TEST_F('CvoxBrailleInputHandlerUnitTest', 'InputContracted', function() { ...@@ -532,25 +578,32 @@ TEST_F('CvoxBrailleInputHandlerUnitTest', 'InputContracted', function() {
var editor = this.createEditor(); var editor = this.createEditor();
this.translatorManager.setTranslators(this.contractedTranslator, this.translatorManager.setTranslators(this.contractedTranslator,
this.uncontractedTranslator); this.uncontractedTranslator);
editor.setContent('', 0);
editor.setActive(true); editor.setActive(true);
editor.focus('text'); editor.focus('text');
this.assertExpandingSelection(); this.assertExpandingSelection();
// First, type a 'b'. // First, type a 'b'.
assertTrue(this.sendCells('12')); assertTrue(this.sendCells('12'));
editor.assertContentIs('', 0);
// Remember that the contracted translator produces uppercase. // Remember that the contracted translator produces uppercase.
editor.assertContentIs('BUT', 'BUT'.length); editor.assertUncommittedTextIs('BUT');
editor.assertExtraCellsAre('12');
this.assertExpandingNone(); this.assertExpandingNone();
// From here on, the input handler needs to replace already entered text.
editor.setAllowDeletes(true);
// Typing 'rl' changes to a different contraction. // Typing 'rl' changes to a different contraction.
assertTrue(this.sendCells('1235 123')); assertTrue(this.sendCells('1235 123'));
editor.assertContentIs('BRAILLE', 'BRAILLE'.length); editor.assertUncommittedTextIs('BRAILLE');
editor.assertContentIs('', 0);
editor.assertExtraCellsAre('12 1235 123');
this.assertExpandingNone();
// Now, finish the word. // Now, finish the word.
assertTrue(this.sendCells('0')); assertTrue(this.sendCells('0'));
editor.assertContentIs('BRAILLE ', 'BRAILLE '.length); editor.assertContentIs('BRAILLE ', 'BRAILLE '.length);
this.assertExpandingNone(); editor.assertUncommittedTextIs('');
editor.assertExtraCellsAre('');
this.assertExpandingSelection();
// Move the cursor to the beginning. // Move the cursor to the beginning.
editor.select(0); editor.select(0);
...@@ -567,15 +620,16 @@ TEST_F('CvoxBrailleInputHandlerUnitTest', 'InputContracted', function() { ...@@ -567,15 +620,16 @@ TEST_F('CvoxBrailleInputHandlerUnitTest', 'InputContracted', function() {
// Move to the end, where contracted typing should work. // Move to the end, where contracted typing should work.
editor.select('bBRAILLEb '.length); editor.select('bBRAILLEb '.length);
assertTrue(this.sendCells('1456 0')); // Symbol for 'this', then space. assertTrue(this.sendCells('1456 0')); // Symbol for 'this', then space.
this.assertExpandingNone(); this.assertExpandingSelection();
editor.assertContentIs('bBRAILLEb THIS ', 'bBRAILLEb this '.length); editor.assertContentIs('bBRAILLEb THIS ', 'bBRAILLEb THIS '.length);
// Move between the two words. // Move to between the two words.
editor.select('bBRAILLEb'.length); editor.select('bBRAILLEb'.length);
this.assertExpandingSelection(); this.assertExpandingSelection();
assertTrue(this.sendCells('0')); assertTrue(this.sendCells('0 12')); // Space plus 'b' for 'but'
assertTrue(this.sendCells('12')); // 'b' for 'but' editor.assertUncommittedTextIs('BUT');
editor.assertContentIs('bBRAILLEb BUT THIS ', 'bBRAILLEb BUT'.length); editor.assertExtraCellsAre('12');
editor.assertContentIs('bBRAILLEb THIS ', 'bBRAILLEb '.length);
this.assertExpandingNone(); this.assertExpandingNone();
}); });
...@@ -614,19 +668,17 @@ TEST_F('CvoxBrailleInputHandlerUnitTest', 'Backspace', function() { ...@@ -614,19 +668,17 @@ TEST_F('CvoxBrailleInputHandlerUnitTest', 'Backspace', function() {
// Add some text that we can delete later. // Add some text that we can delete later.
editor.setContent('Text ', 'Text '.length); editor.setContent('Text ', 'Text '.length);
// The IME needs to delete text, even when typing.
editor.setAllowDeletes(true);
// Type 'brl' to make sure replacement works when deleting text. // Type 'brl' to make sure replacement works when deleting text.
assertTrue(this.sendCells('12 1235 123')); assertTrue(this.sendCells('12 1235 123'));
editor.assertContentIs('Text BRAILLE', 'Text BRAILLE'.length); editor.assertUncommittedTextIs('BRAILLE');
// Delete what we just typed, one cell at a time. // Delete what we just typed, one cell at a time.
this.sendKeyEvent('Backspace'); this.sendKeyEvent('Backspace');
editor.assertContentIs('Text BR', 'Text BR'.length); editor.assertUncommittedTextIs('BR');
this.sendKeyEvent('Backspace'); this.sendKeyEvent('Backspace');
editor.assertContentIs('Text BUT', 'Text BUT'.length); editor.assertUncommittedTextIs('BUT');
this.sendKeyEvent('Backspace'); this.sendKeyEvent('Backspace');
editor.assertContentIs('Text ', 'Text '.length); editor.assertUncommittedTextIs('');
// Now, backspace should be handled as usual, synthetizing key events. // Now, backspace should be handled as usual, synthetizing key events.
assertEquals(0, this.keyEvents.length); assertEquals(0, this.keyEvents.length);
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
goog.provide('cvox.ExpandingBrailleTranslator'); goog.provide('cvox.ExpandingBrailleTranslator');
goog.require('cvox.ExtraCellsSpan');
goog.require('cvox.LibLouis'); goog.require('cvox.LibLouis');
goog.require('cvox.Spannable'); goog.require('cvox.Spannable');
goog.require('cvox.ValueSelectionSpan'); goog.require('cvox.ValueSelectionSpan');
...@@ -88,7 +89,12 @@ cvox.ExpandingBrailleTranslator.ExpansionType = { ...@@ -88,7 +89,12 @@ cvox.ExpandingBrailleTranslator.ExpansionType = {
cvox.ExpandingBrailleTranslator.prototype.translate = cvox.ExpandingBrailleTranslator.prototype.translate =
function(text, expansionType, callback) { function(text, expansionType, callback) {
var expandRanges = this.findExpandRanges_(text, expansionType); var expandRanges = this.findExpandRanges_(text, expansionType);
if (expandRanges.length == 0) { var extraCellsSpans = text.getSpansInstanceOf(cvox.ExtraCellsSpan)
.filter(function(span) { return span.cells.byteLength > 0;});
var extraCellsPositions = extraCellsSpans.map(function(span) {
return text.getSpanStart(span);
});
if (expandRanges.length == 0 && extraCellsSpans.length == 0) {
this.defaultTranslator_.translate( this.defaultTranslator_.translate(
text.toString(), text.toString(),
cvox.ExpandingBrailleTranslator.nullParamsToEmptyAdapter_( cvox.ExpandingBrailleTranslator.nullParamsToEmptyAdapter_(
...@@ -97,8 +103,28 @@ cvox.ExpandingBrailleTranslator.prototype.translate = ...@@ -97,8 +103,28 @@ cvox.ExpandingBrailleTranslator.prototype.translate =
} }
var chunks = []; var chunks = [];
function maybeAddChunkToTranslate(translator, start, end) {
if (start < end)
chunks.push({translator: translator, start: start, end: end});
}
function addExtraCellsChunk(pos, cells) {
var chunk = {translator: null,
start: pos,
end: pos,
cells: cells,
textToBraille: [],
brailleToText: new Array(cells.byteLength)};
for (var i = 0; i < cells.byteLength; ++i)
chunk.brailleToText[i] = 0;
chunks.push(chunk);
}
function addChunk(translator, start, end) { function addChunk(translator, start, end) {
chunks.push({translator: translator, start: start, end: end}); while (extraCellsSpans.length > 0 && extraCellsPositions[0] <= end) {
maybeAddChunkToTranslate(translator, start, extraCellsPositions[0]);
start = extraCellsPositions.shift();
addExtraCellsChunk(start, extraCellsSpans.shift().cells);
}
maybeAddChunkToTranslate(translator, start, end);
} }
var lastEnd = 0; var lastEnd = 0;
for (var i = 0; i < expandRanges.length; ++i) { for (var i = 0; i < expandRanges.length; ++i) {
...@@ -109,19 +135,19 @@ cvox.ExpandingBrailleTranslator.prototype.translate = ...@@ -109,19 +135,19 @@ cvox.ExpandingBrailleTranslator.prototype.translate =
addChunk(this.uncontractedTranslator_, range.start, range.end); addChunk(this.uncontractedTranslator_, range.start, range.end);
lastEnd = range.end; lastEnd = range.end;
} }
if (lastEnd < text.getLength()) { addChunk(this.defaultTranslator_, lastEnd, text.getLength());
addChunk(this.defaultTranslator_, lastEnd, text.getLength());
}
var numPendingCallbacks = chunks.length; var chunksToTranslate = chunks.filter(function(chunk) {
return chunk.translator;
});
var numPendingCallbacks = chunksToTranslate.length;
function chunkTranslated(chunk, cells, textToBraille, brailleToText) { function chunkTranslated(chunk, cells, textToBraille, brailleToText) {
chunk.cells = cells; chunk.cells = cells;
chunk.textToBraille = textToBraille; chunk.textToBraille = textToBraille;
chunk.brailleToText = brailleToText; chunk.brailleToText = brailleToText;
if (--numPendingCallbacks <= 0) { if (--numPendingCallbacks <= 0)
finish(); finish();
}
} }
function finish() { function finish() {
...@@ -145,11 +171,15 @@ cvox.ExpandingBrailleTranslator.prototype.translate = ...@@ -145,11 +171,15 @@ cvox.ExpandingBrailleTranslator.prototype.translate =
callback(cells.buffer, textToBraille, brailleToText); callback(cells.buffer, textToBraille, brailleToText);
} }
for (var i = 0, chunk; chunk = chunks[i]; ++i) { if (chunksToTranslate.length > 0) {
chunk.translator.translate( chunksToTranslate.forEach(function(chunk) {
text.toString().substring(chunk.start, chunk.end), chunk.translator.translate(
cvox.ExpandingBrailleTranslator.nullParamsToEmptyAdapter_( text.toString().substring(chunk.start, chunk.end),
chunk.end - chunk.start, goog.partial(chunkTranslated, chunk))); cvox.ExpandingBrailleTranslator.nullParamsToEmptyAdapter_(
chunk.end - chunk.start, goog.partial(chunkTranslated, chunk)));
});
} else {
finish();
} }
}; };
......
...@@ -116,28 +116,42 @@ var totalRunTranslationTests = 0; ...@@ -116,28 +116,42 @@ var totalRunTranslationTests = 0;
*/ */
function doTranslationTest(name, contracted, valueExpansion, text, function doTranslationTest(name, contracted, valueExpansion, text,
expectedOutput) { expectedOutput) {
totalRunTranslationTests++; try {
var uncontractedTranslator = new FakeTranslator('u'); totalRunTranslationTests++;
var expandingTranslator; var uncontractedTranslator = new FakeTranslator('u');
if (contracted) { var expandingTranslator;
var contractedTranslator = new FakeTranslator('c'); if (contracted) {
expandingTranslator = new cvox.ExpandingBrailleTranslator( var contractedTranslator = new FakeTranslator('c');
contractedTranslator, uncontractedTranslator); expandingTranslator = new cvox.ExpandingBrailleTranslator(
} else { contractedTranslator, uncontractedTranslator);
expandingTranslator = new cvox.ExpandingBrailleTranslator( } else {
uncontractedTranslator); expandingTranslator = new cvox.ExpandingBrailleTranslator(
} uncontractedTranslator);
var expectedMapping = []; }
for (var i = 0; i < expectedOutput.length; ++i) { var extraCellsSpan = text.getSpanInstanceOf(cvox.ExtraCellsSpan);
expectedMapping[i] = i; if (extraCellsSpan)
var extraCellsSpanPos = text.getSpanStart(extraCellsSpan);
var expectedTextToBraille = [];
var expectedBrailleToText = [];
for (var i = 0, pos = 0; i < text.getLength(); ++i, ++pos) {
if (i === extraCellsSpanPos)
++pos;
expectedTextToBraille.push(pos);
expectedBrailleToText.push(i);
}
if (extraCellsSpan)
expectedBrailleToText.splice(extraCellsSpanPos, 0, extraCellsSpanPos);
expandingTranslator.translate(
text, valueExpansion, function(cells, textToBraille, brailleToText) {
assertArrayBufferMatches(expectedOutput, cells, name);
assertEqualsJSON(expectedTextToBraille, textToBraille, name);
assertEqualsJSON(expectedBrailleToText, brailleToText, name);
});
} catch (e) {
console.error('Subtest ' + name + ' failed.');
throw e;
} }
expandingTranslator.translate(
text, valueExpansion, function(cells, textToBraille, brailleToText) {
assertArrayBufferMatches(expectedOutput, cells, name);
assertEqualsJSON(expectedMapping, textToBraille, name);
assertEqualsJSON(expectedMapping, brailleToText, name);
});
}; };
/** /**
...@@ -149,16 +163,27 @@ function doTranslationTest(name, contracted, valueExpansion, text, ...@@ -149,16 +163,27 @@ function doTranslationTest(name, contracted, valueExpansion, text,
* and contracted translators. * and contracted translators.
* @param {cvox.ExpandingBrailleTranslation.ExpansionType} valueExpansion * @param {cvox.ExpandingBrailleTranslation.ExpansionType} valueExpansion
* What kind of value expansion to apply. * What kind of value expansion to apply.
* @param {cvox.Spannable} text Input text. * @param {boolean} withExtraCells Whether to insert an extra cells span
* @param {string=} opt_expectedContractedOutput Expected output (see * right before the selection in the input.
* {@code TESTDATA}).
*/ */
function runTranslationTestVariants(testCase, contracted, valueExpansion) { function runTranslationTestVariants(testCase, contracted, valueExpansion,
withExtraCells) {
var expType = cvox.ExpandingBrailleTranslator.ExpansionType; var expType = cvox.ExpandingBrailleTranslator.ExpansionType;
// Construct the full name. // Construct the full name.
var fullName = contracted ? 'Contracted_' : 'Uncontracted_'; var fullName = contracted ? 'Contracted_' : 'Uncontracted_';
fullName += 'Expansion' + valueExpansion + '_'; fullName += 'Expansion' + valueExpansion + '_';
if (withExtraCells)
fullName += 'ExtraCells_';
fullName += testCase.name; fullName += testCase.name;
var input = testCase.input;
if (withExtraCells) {
input = input.substring(0); // Shallow copy.
var selectionStart = input.getSpanStart(
input.getSpanInstanceOf(cvox.ValueSelectionSpan));
var extraCellsSpan = new cvox.ExtraCellsSpan();
extraCellsSpan.cells = new Uint8Array(['e'.charCodeAt(0)]).buffer;
input.setSpan(extraCellsSpan, selectionStart, selectionStart);
}
// The expected output depends on the contraction mode and value expansion. // The expected output depends on the contraction mode and value expansion.
var outputChar = contracted ? 'c' : 'u'; var outputChar = contracted ? 'c' : 'u';
var expectedOutput; var expectedOutput;
...@@ -170,14 +195,18 @@ function runTranslationTestVariants(testCase, contracted, valueExpansion) { ...@@ -170,14 +195,18 @@ function runTranslationTestVariants(testCase, contracted, valueExpansion) {
expectedOutput = expectedOutput =
new Array(testCase.input.getLength() + 1).join(outputChar); new Array(testCase.input.getLength() + 1).join(outputChar);
} }
doTranslationTest(fullName, contracted, valueExpansion, testCase.input, if (withExtraCells) {
expectedOutput = expectedOutput.substring(0, selectionStart) + 'e' +
expectedOutput.substring(selectionStart);
}
doTranslationTest(fullName, contracted, valueExpansion, input,
expectedOutput); expectedOutput);
// Run another test, with the value surrounded by some text. // Run another test, with the value surrounded by some text.
var surroundedText = new cvox.Spannable('Name: '); var surroundedText = new cvox.Spannable('Name: ');
var surroundedExpectedOutput = var surroundedExpectedOutput =
new Array('Name: '.length + 1).join(outputChar); new Array('Name: '.length + 1).join(outputChar);
surroundedText.append(testCase.input); surroundedText.append(input);
surroundedExpectedOutput += expectedOutput; surroundedExpectedOutput += expectedOutput;
if (testCase.input.getLength() > 0) { if (testCase.input.getLength() > 0) {
surroundedText.append(' '); surroundedText.append(' ');
...@@ -186,7 +215,7 @@ function runTranslationTestVariants(testCase, contracted, valueExpansion) { ...@@ -186,7 +215,7 @@ function runTranslationTestVariants(testCase, contracted, valueExpansion) {
surroundedText.append('edtxt'); surroundedText.append('edtxt');
surroundedExpectedOutput += surroundedExpectedOutput +=
new Array('edtxt'.length + 1).join(outputChar); new Array('edtxt'.length + 1).join(outputChar);
doTranslationTest(fullName + '_Surrounded', contracted, valueExpansion, doTranslationTest(fullName + '_Surrounded', contracted, valueExpansion,
surroundedText, surroundedExpectedOutput); surroundedText, surroundedExpectedOutput);
} }
...@@ -250,15 +279,23 @@ TEST_F('CvoxExpandingBrailleTranslatorUnitTest', 'successfulTranslations', ...@@ -250,15 +279,23 @@ TEST_F('CvoxExpandingBrailleTranslatorUnitTest', 'successfulTranslations',
input: createText(TEXT, 2, 9), input: createText(TEXT, 2, 9),
contractedOutput: 'uuuuuucuuuuuu' } contractedOutput: 'uuuuuucuuuuuu' }
]; ];
var TESTDATA_WITH_SELECTION = TESTDATA.filter(function(testCase) {
return testCase.input.getSpanInstanceOf(cvox.ValueSelectionSpan);
});
var expType = cvox.ExpandingBrailleTranslator.ExpansionType; var expType = cvox.ExpandingBrailleTranslator.ExpansionType;
for (var i = 0, testCase; testCase = TESTDATA[i]; ++i) { for (var i = 0, testCase; testCase = TESTDATA[i]; ++i) {
runTranslationTestVariants(testCase, false, expType.SELECTION); runTranslationTestVariants(testCase, false, expType.SELECTION, false);
runTranslationTestVariants(testCase, true, expType.NONE); runTranslationTestVariants(testCase, true, expType.NONE, false);
runTranslationTestVariants(testCase, true, expType.SELECTION); runTranslationTestVariants(testCase, true, expType.SELECTION, false);
runTranslationTestVariants(testCase, true, expType.ALL); runTranslationTestVariants(testCase, true, expType.ALL, false);
} }
for (var i = 0, testCase; testCase = TESTDATA_WITH_SELECTION[i]; ++i)
runTranslationTestVariants(testCase, true, expType.SELECTION, true);
// Make sure that the logic above runs the tests, adjust when adding more // Make sure that the logic above runs the tests, adjust when adding more
// tests. // test variants.
assertEquals(64, totalRunTranslationTests); var totalExpectedTranslationTests =
2 * (TESTDATA.length * 4 + TESTDATA_WITH_SELECTION.length);
assertEquals(totalExpectedTranslationTests, totalRunTranslationTests);
}); });
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
* and selections. * and selections.
*/ */
goog.provide('cvox.ExtraCellsSpan');
goog.provide('cvox.ValueSelectionSpan'); goog.provide('cvox.ValueSelectionSpan');
goog.provide('cvox.ValueSpan'); goog.provide('cvox.ValueSpan');
...@@ -63,3 +64,15 @@ cvox.ValueSelectionSpan = function() { ...@@ -63,3 +64,15 @@ cvox.ValueSelectionSpan = function() {
cvox.Spannable.registerStatelessSerializableSpan( cvox.Spannable.registerStatelessSerializableSpan(
cvox.ValueSelectionSpan, cvox.ValueSelectionSpan,
'cvox.ValueSelectionSpan'); 'cvox.ValueSelectionSpan');
/**
* Causes raw cells to be added when translating from text to braille.
* This is supported by the {@code cvox.ExpandingBrailleTranslator}
* class.
* @constructor
*/
cvox.ExtraCellsSpan = function() {
/** @type {ArrayBuffer} */
this.cells = new Uint8Array(0).buffer;
};
...@@ -69,6 +69,12 @@ cvox.Spannable.prototype.setSpan = function(value, start, end) { ...@@ -69,6 +69,12 @@ cvox.Spannable.prototype.setSpan = function(value, start, end) {
// Zero-length spans are explicitly allowed, because it is possible to // Zero-length spans are explicitly allowed, because it is possible to
// query for position by annotation as well as the reverse. // query for position by annotation as well as the reverse.
this.spans_.push({ value: value, start: start, end: end }); this.spans_.push({ value: value, start: start, end: end });
this.spans_.sort(function(a, b) {
var ret = a.start - b.start;
if (ret == 0)
ret = a.end - b.end;
return ret;
});
} else { } else {
throw new RangeError('span out of range (start=' + start + throw new RangeError('span out of range (start=' + start +
', end=' + end + ', len=' + this.string_.length + ')'); ', end=' + end + ', len=' + this.string_.length + ')');
...@@ -141,6 +147,8 @@ cvox.Spannable.prototype.getSpanInstanceOf = function(constructor) { ...@@ -141,6 +147,8 @@ cvox.Spannable.prototype.getSpanInstanceOf = function(constructor) {
/** /**
* Returns all span values which are an instance of a given constructor. * Returns all span values which are an instance of a given constructor.
* Spans are returned in the order of their starting index and ending index
* for spans with equals tarting indices.
* @param {!Function} constructor Constructor. * @param {!Function} constructor Constructor.
* @return {!Array<Object>} Array of object. * @return {!Array<Object>} Array of object.
*/ */
......
...@@ -64,11 +64,7 @@ goog.inherits(cvox.BrailleBackground, cvox.AbstractBraille); ...@@ -64,11 +64,7 @@ goog.inherits(cvox.BrailleBackground, cvox.AbstractBraille);
/** @override */ /** @override */
cvox.BrailleBackground.prototype.write = function(params) { cvox.BrailleBackground.prototype.write = function(params) {
this.lastContentId_ = null; this.setContent_(params, null);
this.lastContent_ = null;
this.inputHandler_.onDisplayContentChanged(params.text);
this.displayManager_.setContent(
params, this.inputHandler_.getExpansionType());
}; };
...@@ -94,15 +90,30 @@ cvox.BrailleBackground.prototype.getTranslatorManager = function() { ...@@ -94,15 +90,30 @@ cvox.BrailleBackground.prototype.getTranslatorManager = function() {
*/ */
cvox.BrailleBackground.prototype.onBrailleMessage = function(msg) { cvox.BrailleBackground.prototype.onBrailleMessage = function(msg) {
if (msg['action'] == 'write') { if (msg['action'] == 'write') {
this.lastContentId_ = msg['contentId']; this.setContent_(cvox.NavBraille.fromJson(msg['params']),
this.lastContent_ = cvox.NavBraille.fromJson(msg['params']); msg['contentId']);
this.inputHandler_.onDisplayContentChanged(this.lastContent_.text);
this.displayManager_.setContent(
this.lastContent_, this.inputHandler_.getExpansionType());
} }
}; };
/**
* @param {!cvox.NavBraille} newContent
* @param {?string} newContentId
* @private
*/
cvox.BrailleBackground.prototype.setContent_ = function(
newContent, newContentId) {
var updateContent = function() {
this.lastContent_ = newContentId ? newContent : null;
this.lastContentId_ = newContentId;
this.displayManager_.setContent(
newContent, this.inputHandler_.getExpansionType());
}.bind(this);
this.inputHandler_.onDisplayContentChanged(newContent.text, updateContent);
updateContent();
};
/** /**
* Handles braille key events by dispatching either to the input handler or * Handles braille key events by dispatching either to the input handler or
* a content script. * a content script.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment