Commit 4a88c7d0 authored by David Tseng's avatar David Tseng Committed by Commit Bot

Provide better output for incremental search

This change breaks up incremental change utterances spoken by ChromeVox.

For example:
- Search+/
- type 'chromi'
- speak 'Chromi, ',
' um, an open source project'
- type 'u'
- speak
'Chromiu, '
'm, an open source project'.
-

Change-Id: Ic34cfac814d8d376a041ae21c0cee44f75c9906e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1832529
Commit-Queue: David Tseng <dtseng@chromium.org>
Reviewed-by: default avatarDavid Tseng <dtseng@chromium.org>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#704442}
parent 5fae9add
...@@ -40,8 +40,11 @@ ISearchHandler.prototype = { ...@@ -40,8 +40,11 @@ ISearchHandler.prototype = {
/** /**
* Called when search result node changes. * Called when search result node changes.
* @param {!AutomationNode} node The new search result. * @param {!AutomationNode} node The new search result.
* @param {number} start The index into the name where the search match
* starts.
* @param {number} end The index into the name where the search match ends.
*/ */
onSearchResultChanged: function(node) {} onSearchResultChanged: function(node, start, end) {}
}; };
/** /**
...@@ -60,6 +63,9 @@ ISearch = function(cursor) { ...@@ -60,6 +63,9 @@ ISearch = function(cursor) {
/** @type {!cursors.Cursor} */ /** @type {!cursors.Cursor} */
this.cursor = cursors.Cursor.fromNode(leaf); this.cursor = cursors.Cursor.fromNode(leaf);
/** @private {number} */
this.callbackId_ = 0;
// Global exports. // Global exports.
/** Exported for this background script. */ /** Exported for this background script. */
cvox.ChromeVox = chrome.extension.getBackgroundPage()['cvox']['ChromeVox']; cvox.ChromeVox = chrome.extension.getBackgroundPage()['cvox']['ChromeVox'];
...@@ -77,40 +83,41 @@ ISearch.prototype = { ...@@ -77,40 +83,41 @@ ISearch.prototype = {
* Performs a search. * Performs a search.
* @param {string} searchStr * @param {string} searchStr
* @param {Dir} dir * @param {Dir} dir
* @param {boolean=} opt_nextObject
*/ */
search: function(searchStr, dir) { search: function(searchStr, dir, opt_nextObject) {
searchStr = searchStr.toLocaleLowerCase(); clearTimeout(this.callbackId_);
var node = this.cursor.node; var step = function() {
var result = node; searchStr = searchStr.toLocaleLowerCase();
var prev = node; var node = this.cursor.node;
do { var result = node;
// Because Closure cannot infer much about prev.
if (!prev) if (opt_nextObject) {
break; // We want to start/continue the search at the next object.
result =
// We want to start/continue the search at the next object. AutomationUtil.findNextNode(node, dir, AutomationPredicate.object);
result = }
AutomationUtil.findNextNode(prev, dir, AutomationPredicate.object);
if (!result)
break;
// Ask native to search the underlying data for a performance boost. do {
prev = result; // Ask native to search the underlying data for a performance boost.
result = result.getNextTextMatch(searchStr, dir == Dir.BACKWARD); result = result.getNextTextMatch(searchStr, dir == Dir.BACKWARD);
prev = result || prev; } while (result && !AutomationPredicate.object(result));
if (result) {
this.cursor = cursors.Cursor.fromNode(result);
var start = result.name.toLocaleLowerCase().indexOf(searchStr);
var end = start + searchStr.length;
this.handler_.onSearchResultChanged(result, start, end);
} else {
this.handler_.onSearchReachedBoundary(this.cursor.node);
}
};
// Check to ensure we have an object-like node; otherwise, continue. this.callbackId_ = setTimeout(step.bind(this), 0);
if (result && !AutomationPredicate.object(result)) },
continue;
} while (!result);
if (result) { clear: function() {
this.cursor = cursors.Cursor.fromNode(result); clearTimeout(this.callbackId_);
this.handler_.onSearchResultChanged(result);
} else {
this.handler_.onSearchReachedBoundary(this.cursor.node);
}
} }
}; };
...@@ -183,7 +190,7 @@ ISearchUI.prototype = { ...@@ -183,7 +190,7 @@ ISearchUI.prototype = {
default: default:
return false; return false;
} }
this.iSearch_.search(this.input_.value, this.dir_); this.iSearch_.search(this.input_.value, this.dir_, true);
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
return false; return false;
...@@ -196,6 +203,7 @@ ISearchUI.prototype = { ...@@ -196,6 +203,7 @@ ISearchUI.prototype = {
*/ */
onTextInput: function(evt) { onTextInput: function(evt) {
var searchStr = evt.target.value + evt.data; var searchStr = evt.target.value + evt.data;
this.iSearch_.clear();
this.iSearch_.search(searchStr, this.dir_); this.iSearch_.search(searchStr, this.dir_);
return true; return true;
}, },
...@@ -211,21 +219,31 @@ ISearchUI.prototype = { ...@@ -211,21 +219,31 @@ ISearchUI.prototype = {
/** /**
* @override * @override
*/ */
onSearchResultChanged: function(node) { onSearchResultChanged: function(node, start, end) {
this.output_(node); this.output_(node, start, end);
}, },
/** /**
* @param {!AutomationNode} node * @param {!AutomationNode} node
* @param {number=} opt_start
* @param {number=} opt_end
* @private * @private
*/ */
output_: function(node) { output_: function(node, opt_start, opt_end) {
Output.forceModeForNextSpeechUtterance(cvox.QueueMode.FLUSH); Output.forceModeForNextSpeechUtterance(cvox.QueueMode.FLUSH);
var o = var o = new Output();
new Output() if (opt_start && opt_end) {
.withRichSpeechAndBraille( o.withString([
cursors.Range.fromNode(node), null, Output.EventType.NAVIGATE) node.name.substr(0, opt_start),
.go(); node.name.substr(opt_start, opt_end - opt_start),
node.name.substr(opt_end)
].join(', '));
o.format('$role', node);
} else {
o.withRichSpeechAndBraille(
cursors.Range.fromNode(node), null, Output.EventType.NAVIGATE);
}
o.go();
this.background_.setCurrentRange(cursors.Range.fromNode(node)); this.background_.setCurrentRange(cursors.Range.fromNode(node));
}, },
......
...@@ -46,17 +46,23 @@ function FakeISearchHandler(testObj) { ...@@ -46,17 +46,23 @@ function FakeISearchHandler(testObj) {
FakeISearchHandler.prototype = { FakeISearchHandler.prototype = {
/** @override */ /** @override */
onSearchReachedBoundary: function(boundaryNode) { onSearchReachedBoundary: function(boundaryNode) {
this.expect_.shift()(boundaryNode, true); this.expect_.shift()({node: boundaryNode, isBoundary: true});
}, },
onSearchResultChanged: function(node) { /** @override */
this.expect_.shift()(node); onSearchResultChanged: function(node, start, end) {
this.expect_.shift()({node: node,
start: start,
end: end});
}, },
expect: function(str, opt_callback) { expect: function(str, opt_callback) {
this.expect_.push(this.test.newCallback(function(node, isBound) { this.expect_.push(this.test.newCallback(function(args) {
var node = args.node;
var actual = node.name || node.role; var actual = node.name || node.role;
if (isBound) if (args.start && args.end)
actual = 'start=' + args.start + ' end=' + args.end + ' text=' + actual;
if (args.isBoundary)
actual = 'boundary=' + actual; actual = 'boundary=' + actual;
assertEquals(str, actual); assertEquals(str, actual);
opt_callback && opt_callback(); opt_callback && opt_callback();
...@@ -64,15 +70,15 @@ FakeISearchHandler.prototype = { ...@@ -64,15 +70,15 @@ FakeISearchHandler.prototype = {
} }
}; };
TEST_F('ChromeVoxISearchTest', 'DISABLED_Simple', function() { TEST_F('ChromeVoxISearchTest', 'Simple', function() {
this.runWithLoadedTree(this.linksAndHeadingsDoc, function(rootNode) { this.runWithLoadedTree(this.linksAndHeadingsDoc, function(rootNode) {
var handler = new FakeISearchHandler(this); var handler = new FakeISearchHandler(this);
var search = new ISearch(rootNode); var search = new ISearch(new cursors.Cursor(rootNode, 0));
search.handler = handler; search.handler = handler;
// Simple forward search. // Simple forward search.
search.search('US', 'forward'); search.search('US', 'forward');
handler.expect('About US', handler.expect('start=6 end=8 text=About US',
search.search.bind(search, 'start', 'backward')); search.search.bind(search, 'start', 'backward'));
handler.expect('start', handler.expect('start',
...@@ -88,9 +94,14 @@ TEST_F('ChromeVoxISearchTest', 'DISABLED_Simple', function() { ...@@ -88,9 +94,14 @@ TEST_F('ChromeVoxISearchTest', 'DISABLED_Simple', function() {
// Mixed case substring. // Mixed case substring.
search.search.bind(search, 'bReak', 'forward')); search.search.bind(search, 'bReak', 'forward'));
handler.expect('Latest Breaking News', handler.expect('start=7 end=12 text=Latest Breaking News',
search.search.bind(search, 'bReak', 'forward')); search.search.bind(search, 'bReaki', 'forward'));
// Incremental search stays on the current node.
handler.expect('start=7 end=13 text=Latest Breaking News',
search.search.bind(search, 'bReakio', 'forward'));
// No results for the search.
handler.expect('boundary=Latest Breaking News'); handler.expect('boundary=Latest Breaking News');
}); });
}); });
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