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 @@
* Sent on focus/blur to inform ChromeVox of the type of the current field.
* In the latter case (blur), context is null.
* {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}
* Sent when the user typed a braille cell using the standard keyboard.
* ChromeVox treats this similarly to entering braille input using the
......@@ -35,6 +37,14 @@
* 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
* 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}
* Response to a {@code backspace} message indicating whether the
* backspace was handled by ChromeVox or should be allowed to propagate
......@@ -130,6 +140,13 @@ BrailleIme.prototype = {
*/
port_: null,
/**
* Uncommitted text and context ID.
* @type {?{contextID: number, text: string}}
* @private
*/
uncommitted_: null,
/**
* Registers event listeners in the chrome IME API.
*/
......@@ -216,9 +233,8 @@ BrailleIme.prototype = {
*/
onKeyEvent_: function(engineID, event) {
var result = this.processKey_(event);
if (result !== undefined) {
chrome.input.ime.keyEventHandled(event.requestId, result);
}
if (result !== undefined)
this.keyEventHandled_(event.requestId, event.type, result);
},
/**
......@@ -356,7 +372,17 @@ BrailleIme.prototype = {
case 'keyEventHandled':
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;
default:
console.error('Unknown message from ChromeVox: ' +
......@@ -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.
*/
......
......@@ -276,3 +276,36 @@ TEST_F('BrailleImeUnitTest', 'ReplaceText', function() {
assertFalse(hasSelection);
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) {
* @private
*/
this.entryState_ = null;
/**
* @type {cvox.ExtraCellsSpan}
* @private
*/
this.uncommittedCellsSpan_ = null;
/**
* @type {function()?}
* @private
*/
this.uncommittedCellsChangedListener_ = null;
this.translatorManager_.addChangeListener(
this.clearEntryState_.bind(this));
this.commitAndClearEntryState_.bind(this));
};
/**
......@@ -122,12 +132,18 @@ cvox.BrailleInputHandler.prototype = {
* input state according to the new content.
* @param {cvox.Spannable} text Text, optionally with value and selection
* 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 selectionSpan = text.getSpanInstanceOf(cvox.ValueSelectionSpan);
if (!(valueSpan && selectionSpan))
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.
var valueStart = /** @type {number} */ (text.getSpanStart(valueSpan));
var valueEnd = /** @type {number} */ (text.getSpanEnd(valueSpan));
......@@ -144,6 +160,13 @@ cvox.BrailleInputHandler.prototype = {
this.entryState_.onTextBeforeChanged(newTextBefore);
this.currentTextBefore_ = newTextBefore;
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 = {
this.onBackspace_()) {
return true;
} else {
this.clearEntryState_();
this.commitAndClearEntryState_();
this.sendKeyEventPair_(event);
return true;
}
......@@ -213,14 +236,8 @@ cvox.BrailleInputHandler.prototype = {
}
if (!this.inputContext_)
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_) {
this.entryState_ = this.createEntryState_();
if (!this.entryState_)
if (!(this.entryState_ = this.createEntryState_()))
return false;
}
this.entryState_.appendCell(dots);
......@@ -255,6 +272,7 @@ cvox.BrailleInputHandler.prototype = {
return null;
var uncontractedTranslator =
this.translatorManager_.getUncontractedTranslator();
var constructor = cvox.BrailleInputHandler.EditsEntryState_;
if (uncontractedTranslator) {
var textBefore = this.currentTextBefore_;
var textAfter = this.currentTextAfter_;
......@@ -264,10 +282,23 @@ cvox.BrailleInputHandler.prototype = {
(cvox.BrailleInputHandler.STARTS_WITH_NON_WHITESPACE_RE_.test(
textAfter))) {
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 = {
*/
clearEntryState_: function() {
if (this.entryState_) {
if (this.entryState_.usesUncommittedCells)
this.updateUncommittedCells_(new ArrayBuffer(0));
this.entryState_.inputHandler_ = 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
* connections from the ChromeOS builtin Braille IME and ignores connections
......@@ -499,9 +543,18 @@ cvox.BrailleInputHandler.EntryState_.prototype = {
this.inputHandler_.clearEntryState_();
},
/** @return {boolean} */
lastCellIsBlank: function() {
return this.cells_[this.cells_.length - 1] === 0;
/**
* Makes sure the current text is permanently added to the edit field.
* 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 = {
*/
updateText_: function() {
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) {
if (result === null) {
console.error('Error when backtranslating braille cells');
......@@ -520,9 +576,19 @@ cvox.BrailleInputHandler.EntryState_.prototype = {
return;
this.sendTextChange_(result);
this.text_ = result;
if (commit)
this.inputHandler_.commitAndClearEntryState_();
}.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.
* The old text is still available in the {@code text_} property.
......@@ -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) {
this.contextID_ = 0;
/** @private {boolean} */
this.allowDeletes_ = false;
/** @private {string} */
this.uncommittedText_ = '';
/** @private {?Array<number>} */
this.extraCells_ = [];
port.postMessage = goog.bind(this.handleMessage_, this);
}
......@@ -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.
* @param {Object} msg The message to send.
......@@ -151,9 +174,17 @@ FakeEditor.prototype.message_ = function(msg) {
* @private
*/
FakeEditor.prototype.callOnDisplayContentChanged_ = function() {
this.inputHandler_.onDisplayContentChanged(
cvox.BrailleUtil.createValue(
this.text_, this.selectionStart_, this.selectionEnd_));
var content = cvox.BrailleUtil.createValue(
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,8 +217,9 @@ FakeEditor.prototype.blur = function() {
* @private
*/
FakeEditor.prototype.handleMessage_ = function(msg) {
assertEquals('replaceText', msg.type);
assertEquals(this.contextID_, msg.contextID);
switch(msg.type) {
case 'replaceText':
var deleteBefore = msg.deleteBefore;
var newText = msg.newText;
assertTrue(goog.isNumber(deleteBefore));
......@@ -203,6 +235,18 @@ FakeEditor.prototype.handleMessage_ = function(msg) {
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));
}
};
/*
......@@ -362,6 +406,8 @@ FakeTranslatorManager.prototype = {
* pattern (dot 1 uses bit 0, etc).
*/
function cellsToArray(cells) {
if (!cells)
return [];
return cells.split(/\s+/).map(function(cellString) {
var cell = 0;
assertTrue(cellString.length > 0);
......@@ -532,25 +578,32 @@ TEST_F('CvoxBrailleInputHandlerUnitTest', 'InputContracted', function() {
var editor = this.createEditor();
this.translatorManager.setTranslators(this.contractedTranslator,
this.uncontractedTranslator);
editor.setContent('', 0);
editor.setActive(true);
editor.focus('text');
this.assertExpandingSelection();
// First, type a 'b'.
assertTrue(this.sendCells('12'));
editor.assertContentIs('', 0);
// Remember that the contracted translator produces uppercase.
editor.assertContentIs('BUT', 'BUT'.length);
editor.assertUncommittedTextIs('BUT');
editor.assertExtraCellsAre('12');
this.assertExpandingNone();
// From here on, the input handler needs to replace already entered text.
editor.setAllowDeletes(true);
// Typing 'rl' changes to a different contraction.
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.
assertTrue(this.sendCells('0'));
editor.assertContentIs('BRAILLE ', 'BRAILLE '.length);
this.assertExpandingNone();
editor.assertUncommittedTextIs('');
editor.assertExtraCellsAre('');
this.assertExpandingSelection();
// Move the cursor to the beginning.
editor.select(0);
......@@ -567,15 +620,16 @@ TEST_F('CvoxBrailleInputHandlerUnitTest', 'InputContracted', function() {
// Move to the end, where contracted typing should work.
editor.select('bBRAILLEb '.length);
assertTrue(this.sendCells('1456 0')); // Symbol for 'this', then space.
this.assertExpandingNone();
editor.assertContentIs('bBRAILLEb THIS ', 'bBRAILLEb this '.length);
this.assertExpandingSelection();
editor.assertContentIs('bBRAILLEb THIS ', 'bBRAILLEb THIS '.length);
// Move between the two words.
// Move to between the two words.
editor.select('bBRAILLEb'.length);
this.assertExpandingSelection();
assertTrue(this.sendCells('0'));
assertTrue(this.sendCells('12')); // 'b' for 'but'
editor.assertContentIs('bBRAILLEb BUT THIS ', 'bBRAILLEb BUT'.length);
assertTrue(this.sendCells('0 12')); // Space plus 'b' for 'but'
editor.assertUncommittedTextIs('BUT');
editor.assertExtraCellsAre('12');
editor.assertContentIs('bBRAILLEb THIS ', 'bBRAILLEb '.length);
this.assertExpandingNone();
});
......@@ -614,19 +668,17 @@ TEST_F('CvoxBrailleInputHandlerUnitTest', 'Backspace', function() {
// Add some text that we can delete later.
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.
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.
this.sendKeyEvent('Backspace');
editor.assertContentIs('Text BR', 'Text BR'.length);
editor.assertUncommittedTextIs('BR');
this.sendKeyEvent('Backspace');
editor.assertContentIs('Text BUT', 'Text BUT'.length);
editor.assertUncommittedTextIs('BUT');
this.sendKeyEvent('Backspace');
editor.assertContentIs('Text ', 'Text '.length);
editor.assertUncommittedTextIs('');
// Now, backspace should be handled as usual, synthetizing key events.
assertEquals(0, this.keyEvents.length);
......
......@@ -9,6 +9,7 @@
goog.provide('cvox.ExpandingBrailleTranslator');
goog.require('cvox.ExtraCellsSpan');
goog.require('cvox.LibLouis');
goog.require('cvox.Spannable');
goog.require('cvox.ValueSelectionSpan');
......@@ -88,7 +89,12 @@ cvox.ExpandingBrailleTranslator.ExpansionType = {
cvox.ExpandingBrailleTranslator.prototype.translate =
function(text, expansionType, callback) {
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(
text.toString(),
cvox.ExpandingBrailleTranslator.nullParamsToEmptyAdapter_(
......@@ -97,9 +103,29 @@ cvox.ExpandingBrailleTranslator.prototype.translate =
}
var chunks = [];
function addChunk(translator, start, end) {
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) {
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;
for (var i = 0; i < expandRanges.length; ++i) {
var range = expandRanges[i];
......@@ -109,20 +135,20 @@ cvox.ExpandingBrailleTranslator.prototype.translate =
addChunk(this.uncontractedTranslator_, range.start, range.end);
lastEnd = range.end;
}
if (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) {
chunk.cells = cells;
chunk.textToBraille = textToBraille;
chunk.brailleToText = brailleToText;
if (--numPendingCallbacks <= 0) {
if (--numPendingCallbacks <= 0)
finish();
}
}
function finish() {
var totalCells = chunks.reduce(
......@@ -145,11 +171,15 @@ cvox.ExpandingBrailleTranslator.prototype.translate =
callback(cells.buffer, textToBraille, brailleToText);
}
for (var i = 0, chunk; chunk = chunks[i]; ++i) {
if (chunksToTranslate.length > 0) {
chunksToTranslate.forEach(function(chunk) {
chunk.translator.translate(
text.toString().substring(chunk.start, chunk.end),
cvox.ExpandingBrailleTranslator.nullParamsToEmptyAdapter_(
chunk.end - chunk.start, goog.partial(chunkTranslated, chunk)));
});
} else {
finish();
}
};
......
......@@ -116,6 +116,7 @@ var totalRunTranslationTests = 0;
*/
function doTranslationTest(name, contracted, valueExpansion, text,
expectedOutput) {
try {
totalRunTranslationTests++;
var uncontractedTranslator = new FakeTranslator('u');
var expandingTranslator;
......@@ -127,17 +128,30 @@ function doTranslationTest(name, contracted, valueExpansion, text,
expandingTranslator = new cvox.ExpandingBrailleTranslator(
uncontractedTranslator);
}
var expectedMapping = [];
for (var i = 0; i < expectedOutput.length; ++i) {
expectedMapping[i] = i;
var extraCellsSpan = text.getSpanInstanceOf(cvox.ExtraCellsSpan);
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(expectedMapping, textToBraille, name);
assertEqualsJSON(expectedMapping, brailleToText, name);
assertEqualsJSON(expectedTextToBraille, textToBraille, name);
assertEqualsJSON(expectedBrailleToText, brailleToText, name);
});
} catch (e) {
console.error('Subtest ' + name + ' failed.');
throw e;
}
};
/**
......@@ -149,16 +163,27 @@ function doTranslationTest(name, contracted, valueExpansion, text,
* and contracted translators.
* @param {cvox.ExpandingBrailleTranslation.ExpansionType} valueExpansion
* What kind of value expansion to apply.
* @param {cvox.Spannable} text Input text.
* @param {string=} opt_expectedContractedOutput Expected output (see
* {@code TESTDATA}).
* @param {boolean} withExtraCells Whether to insert an extra cells span
* right before the selection in the input.
*/
function runTranslationTestVariants(testCase, contracted, valueExpansion) {
function runTranslationTestVariants(testCase, contracted, valueExpansion,
withExtraCells) {
var expType = cvox.ExpandingBrailleTranslator.ExpansionType;
// Construct the full name.
var fullName = contracted ? 'Contracted_' : 'Uncontracted_';
fullName += 'Expansion' + valueExpansion + '_';
if (withExtraCells)
fullName += 'ExtraCells_';
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.
var outputChar = contracted ? 'c' : 'u';
var expectedOutput;
......@@ -170,14 +195,18 @@ function runTranslationTestVariants(testCase, contracted, valueExpansion) {
expectedOutput =
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);
// Run another test, with the value surrounded by some text.
var surroundedText = new cvox.Spannable('Name: ');
var surroundedExpectedOutput =
new Array('Name: '.length + 1).join(outputChar);
surroundedText.append(testCase.input);
surroundedText.append(input);
surroundedExpectedOutput += expectedOutput;
if (testCase.input.getLength() > 0) {
surroundedText.append(' ');
......@@ -250,15 +279,23 @@ TEST_F('CvoxExpandingBrailleTranslatorUnitTest', 'successfulTranslations',
input: createText(TEXT, 2, 9),
contractedOutput: 'uuuuuucuuuuuu' }
];
var TESTDATA_WITH_SELECTION = TESTDATA.filter(function(testCase) {
return testCase.input.getSpanInstanceOf(cvox.ValueSelectionSpan);
});
var expType = cvox.ExpandingBrailleTranslator.ExpansionType;
for (var i = 0, testCase; testCase = TESTDATA[i]; ++i) {
runTranslationTestVariants(testCase, false, expType.SELECTION);
runTranslationTestVariants(testCase, true, expType.NONE);
runTranslationTestVariants(testCase, true, expType.SELECTION);
runTranslationTestVariants(testCase, true, expType.ALL);
runTranslationTestVariants(testCase, false, expType.SELECTION, false);
runTranslationTestVariants(testCase, true, expType.NONE, false);
runTranslationTestVariants(testCase, true, expType.SELECTION, false);
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
// tests.
assertEquals(64, totalRunTranslationTests);
// test variants.
var totalExpectedTranslationTests =
2 * (TESTDATA.length * 4 + TESTDATA_WITH_SELECTION.length);
assertEquals(totalExpectedTranslationTests, totalRunTranslationTests);
});
......@@ -7,6 +7,7 @@
* and selections.
*/
goog.provide('cvox.ExtraCellsSpan');
goog.provide('cvox.ValueSelectionSpan');
goog.provide('cvox.ValueSpan');
......@@ -63,3 +64,15 @@ cvox.ValueSelectionSpan = function() {
cvox.Spannable.registerStatelessSerializableSpan(
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) {
// Zero-length spans are explicitly allowed, because it is possible to
// query for position by annotation as well as the reverse.
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 {
throw new RangeError('span out of range (start=' + start +
', end=' + end + ', len=' + this.string_.length + ')');
......@@ -141,6 +147,8 @@ cvox.Spannable.prototype.getSpanInstanceOf = function(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.
* @return {!Array<Object>} Array of object.
*/
......
......@@ -64,11 +64,7 @@ goog.inherits(cvox.BrailleBackground, cvox.AbstractBraille);
/** @override */
cvox.BrailleBackground.prototype.write = function(params) {
this.lastContentId_ = null;
this.lastContent_ = null;
this.inputHandler_.onDisplayContentChanged(params.text);
this.displayManager_.setContent(
params, this.inputHandler_.getExpansionType());
this.setContent_(params, null);
};
......@@ -94,15 +90,30 @@ cvox.BrailleBackground.prototype.getTranslatorManager = function() {
*/
cvox.BrailleBackground.prototype.onBrailleMessage = function(msg) {
if (msg['action'] == 'write') {
this.lastContentId_ = msg['contentId'];
this.lastContent_ = cvox.NavBraille.fromJson(msg['params']);
this.inputHandler_.onDisplayContentChanged(this.lastContent_.text);
this.displayManager_.setContent(
this.lastContent_, this.inputHandler_.getExpansionType());
this.setContent_(cvox.NavBraille.fromJson(msg['params']),
msg['contentId']);
}
};
/**
* @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
* 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