Commit b310c0db authored by David Tseng's avatar David Tseng Committed by Commit Bot

Add hints to ChromeVox

Bug: 809083,806470, 806469
Cq-Include-Trybots: master.tryserver.chromium.linux:closure_compilation
Change-Id: I56b36811d6125f10ee042a317156d8dd96ac06d3
Reviewed-on: https://chromium-review.googlesource.com/902405
Commit-Queue: David Tseng <dtseng@chromium.org>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#534648}
parent 631176ab
...@@ -314,6 +314,8 @@ IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, FocusShelf) { ...@@ -314,6 +314,8 @@ IN_PROC_BROWSER_TEST_P(SpokenFeedbackTest, FocusShelf) {
EXPECT_EQ("Shelf", speech_monitor_.GetNextUtterance()); EXPECT_EQ("Shelf", speech_monitor_.GetNextUtterance());
EXPECT_EQ("Tool bar", speech_monitor_.GetNextUtterance()); EXPECT_EQ("Tool bar", speech_monitor_.GetNextUtterance());
EXPECT_EQ(", window", speech_monitor_.GetNextUtterance()); EXPECT_EQ(", window", speech_monitor_.GetNextUtterance());
EXPECT_EQ("Press Search plus Space to activate.",
speech_monitor_.GetNextUtterance());
SendKeyPress(ui::VKEY_TAB); SendKeyPress(ui::VKEY_TAB);
EXPECT_TRUE(base::MatchPattern(speech_monitor_.GetNextUtterance(), "*")); EXPECT_TRUE(base::MatchPattern(speech_monitor_.GetNextUtterance(), "*"));
......
...@@ -46,18 +46,23 @@ AutomationPredicate.roles = function(roles) { ...@@ -46,18 +46,23 @@ AutomationPredicate.roles = function(roles) {
/** /**
* Constructs a predicate given a list of roles or predicates. * Constructs a predicate given a list of roles or predicates.
* @param {{anyRole: (Array<Role>|undefined), * @param {{anyRole: (Array<Role>|undefined),
* anyPredicate: (Array<AutomationPredicate.Unary>|undefined)}} params * anyPredicate: (Array<AutomationPredicate.Unary>|undefined),
* anyAttribute: (Object|undefined)}} params
* @return {AutomationPredicate.Unary} * @return {AutomationPredicate.Unary}
*/ */
AutomationPredicate.match = function(params) { AutomationPredicate.match = function(params) {
var anyRole = params.anyRole || []; var anyRole = params.anyRole || [];
var anyPredicate = params.anyPredicate || []; var anyPredicate = params.anyPredicate || [];
var anyAttribute = params.anyAttribute || {};
return function(node) { return function(node) {
return anyRole.some(function(role) { return anyRole.some(function(role) {
return role == node.role; return role == node.role;
}) || }) ||
anyPredicate.some(function(p) { anyPredicate.some(function(p) {
return p(node); return p(node);
}) ||
Object.keys(anyAttribute).some(function(key) {
return node[key] === anyAttribute[key];
}); });
}; };
}; };
...@@ -393,6 +398,16 @@ AutomationPredicate.checkable = AutomationPredicate.roles([ ...@@ -393,6 +398,16 @@ AutomationPredicate.checkable = AutomationPredicate.roles([
Role.MENU_ITEM_RADIO, Role.TREE_ITEM Role.MENU_ITEM_RADIO, Role.TREE_ITEM
]); ]);
/**
* Returns if the node is clickable.
* @param {!AutomationNode} node
* @return {boolean}
*/
AutomationPredicate.clickable = AutomationPredicate.match({
anyPredicate: [AutomationPredicate.button, AutomationPredicate.link],
anyAttribute: {clickable: true}
});
// Table related predicates. // Table related predicates.
/** /**
* Returns if the node has a cell like role. * Returns if the node has a cell like role.
......
...@@ -139,16 +139,16 @@ TEST_F('BackgroundTest', 'ForwardBackwardNavigation', function() { ...@@ -139,16 +139,16 @@ TEST_F('BackgroundTest', 'ForwardBackwardNavigation', function() {
mockFeedback.call(doCmd('nextLink')) mockFeedback.call(doCmd('nextLink'))
.expectSpeech('alpha', 'Link') .expectSpeech('alpha', 'Link')
.expectBraille('alpha lnk'); .expectBraille('alpha lnk Press Search+Space to activate.');
mockFeedback.call(doCmd('nextLink')) mockFeedback.call(doCmd('nextLink'))
.expectSpeech('beta', 'Link') .expectSpeech('beta', 'Link')
.expectBraille('beta lnk'); .expectBraille('beta lnk Press Search+Space to activate.');
mockFeedback.call(doCmd('nextLink')) mockFeedback.call(doCmd('nextLink'))
.expectSpeech('delta', 'Link') .expectSpeech('delta', 'Link')
.expectBraille('delta lnk'); .expectBraille('delta lnk Press Search+Space to activate.');
mockFeedback.call(doCmd('previousLink')) mockFeedback.call(doCmd('previousLink'))
.expectSpeech('beta', 'Link') .expectSpeech('beta', 'Link')
.expectBraille('beta lnk'); .expectBraille('beta lnk Press Search+Space to activate.');
mockFeedback.call(doCmd('nextHeading')) mockFeedback.call(doCmd('nextHeading'))
.expectSpeech('charlie', 'Heading 1') .expectSpeech('charlie', 'Heading 1')
.expectBraille('charlie h1'); .expectBraille('charlie h1');
...@@ -161,10 +161,10 @@ TEST_F('BackgroundTest', 'ForwardBackwardNavigation', function() { ...@@ -161,10 +161,10 @@ TEST_F('BackgroundTest', 'ForwardBackwardNavigation', function() {
mockFeedback.call(doCmd('nextObject')) mockFeedback.call(doCmd('nextObject'))
.expectSpeech('delta', 'Link') .expectSpeech('delta', 'Link')
.expectBraille('delta lnk'); .expectBraille('delta lnk Press Search+Space to activate.');
mockFeedback.call(doCmd('nextObject')) mockFeedback.call(doCmd('nextObject'))
.expectSpeech('echo', 'Link') .expectSpeech('echo', 'Link')
.expectBraille('echo lnk'); .expectBraille('echo lnk Press Search+Space to activate.');
mockFeedback.call(doCmd('nextObject')) mockFeedback.call(doCmd('nextObject'))
.expectSpeech('foxtraut', 'Heading 2') .expectSpeech('foxtraut', 'Heading 2')
.expectBraille('foxtraut h2'); .expectBraille('foxtraut h2');
...@@ -265,7 +265,7 @@ TEST_F('BackgroundTest', 'SelectSingleBasic', function() { ...@@ -265,7 +265,7 @@ TEST_F('BackgroundTest', 'SelectSingleBasic', function() {
var incrementSelectedIndex = var incrementSelectedIndex =
this.incrementSelectedIndex.bind(this, undefined, '#fruitSelect'); this.incrementSelectedIndex.bind(this, undefined, '#fruitSelect');
mockFeedback.expectSpeech('apple', 'has pop up', 'Collapsed') mockFeedback.expectSpeech('apple', 'has pop up', 'Collapsed')
.expectBraille('apple btn +popup +') .expectBraille('apple btn +popup + Press Search+Space to activate.')
.call(incrementSelectedIndex) .call(incrementSelectedIndex)
.expectSpeech('grape', /2 of 3/) .expectSpeech('grape', /2 of 3/)
.expectBraille('grape mnuitm 2/3 (x)') .expectBraille('grape mnuitm 2/3 (x)')
...@@ -307,7 +307,8 @@ TEST_F('BackgroundTest', 'AriaLabel', function() { ...@@ -307,7 +307,8 @@ TEST_F('BackgroundTest', 'AriaLabel', function() {
rootNode.find({role: RoleType.LINK}).focus(); rootNode.find({role: RoleType.LINK}).focus();
mockFeedback.expectSpeech('foo') mockFeedback.expectSpeech('foo')
.expectSpeech('Link') .expectSpeech('Link')
.expectBraille('foo lnk'); .expectSpeech('Press Search+Space to activate.')
.expectBraille('foo lnk Press Search+Space to activate.');
mockFeedback.replay(); mockFeedback.replay();
} }
); );
......
...@@ -238,8 +238,10 @@ CommandHandler.onCommand = function(command) { ...@@ -238,8 +238,10 @@ CommandHandler.onCommand = function(command) {
} }
// Require a current range. // Require a current range.
if (!ChromeVoxState.instance.currentRange_) if (!ChromeVoxState.instance.currentRange_) {
new Output().format('@warning_no_current_range').go();
return true; return true;
}
var current = ChromeVoxState.instance.currentRange_; var current = ChromeVoxState.instance.currentRange_;
...@@ -514,6 +516,7 @@ CommandHandler.onCommand = function(command) { ...@@ -514,6 +516,7 @@ CommandHandler.onCommand = function(command) {
newRange.select(); newRange.select();
new Output() new Output()
.withoutHints()
.withRichSpeechAndBraille( .withRichSpeechAndBraille(
ChromeVoxState.instance.currentRange_, prevRange, ChromeVoxState.instance.currentRange_, prevRange,
Output.EventType.NAVIGATE) Output.EventType.NAVIGATE)
......
...@@ -97,6 +97,9 @@ Output = function() { ...@@ -97,6 +97,9 @@ Output = function() {
/** @private {!Object<string, boolean>} */ /** @private {!Object<string, boolean>} */
this.suppressions_ = {}; this.suppressions_ = {};
/** @private {boolean} */
this.enableHints_ = true;
}; };
/** /**
...@@ -783,6 +786,15 @@ Output.prototype = { ...@@ -783,6 +786,15 @@ Output.prototype = {
return this; return this;
}, },
/**
* Don't include hints in subsequent output.
* @return {Output}
*/
withoutHints: function() {
this.enableHints_ = false;
return this;
},
/** /**
* Suppresses processing of a token for subsequent formatting commands. * Suppresses processing of a token for subsequent formatting commands.
* @param {string} token * @param {string} token
...@@ -956,6 +968,8 @@ Output.prototype = { ...@@ -956,6 +968,8 @@ Output.prototype = {
this.subNode_(range, prevRange, type, buff); this.subNode_(range, prevRange, type, buff);
else else
this.range_(range, prevRange, type, buff); this.range_(range, prevRange, type, buff);
this.hint_(range, uniqueAncestors, buff);
}, },
/** /**
...@@ -1708,6 +1722,39 @@ Output.prototype = { ...@@ -1708,6 +1722,39 @@ Output.prototype = {
this.locations_.push(loc); this.locations_.push(loc);
}, },
hint_: function(range, uniqueAncestors, buff) {
if (!this.enableHints_ || localStorage['useVerboseMode'] != 'true')
return;
var node = range.start.node;
if (!node) {
this.append_(buff, Msgs.getMsg('warning_no_current_range'));
return;
}
// Prioritized hints.
if (node.state[StateType.EDITABLE] && cvox.ChromeVox.isStickyPrefOn)
this.format_(node, '@sticky_mode_enabled', buff);
if (AutomationPredicate.checkable(node))
this.format_(node, '@hint_checkable', buff);
if (AutomationPredicate.clickable(node))
this.format_(node, '@hint_clickable', buff);
if (node.autoComplete == 'list' || node.autoComplete == 'both')
this.format_(node, '@hint_autocomplete_list', buff);
if (node.autoComplete == 'inline' || node.autoComplete == 'both')
this.format_(node, '@hint_autocomplete_inline', buff);
if (node.accessKey)
this.append_(buff, Msgs.getMsg('access_key', [node.accessKey]));
// Ancestry based hints.
if (uniqueAncestors.find(AutomationPredicate.table))
this.format_(node, '@hint_table', buff);
if (uniqueAncestors.find(
AutomationPredicate.roles([RoleType.MENU, RoleType.MENU_BAR])))
this.format_(node, '@hint_menu', buff);
},
/** /**
* Appends output to the |buff|. * Appends output to the |buff|.
* @param {!Array<Spannable>} buff * @param {!Array<Spannable>} buff
......
...@@ -111,7 +111,8 @@ TEST_F('OutputE2ETest', 'Links', function() { ...@@ -111,7 +111,8 @@ TEST_F('OutputE2ETest', 'Links', function() {
var el = root.firstChild.firstChild; var el = root.firstChild.firstChild;
var range = cursors.Range.fromNode(el); var range = cursors.Range.fromNode(el);
var o = new Output().withSpeechAndBraille(range, null, 'navigate'); var o = new Output().withSpeechAndBraille(range, null, 'navigate');
assertEqualsJSON({string_: 'Click here|Link', 'spans_': [ assertEqualsJSON({string_:
'Click here|Link|Press Search+Space to activate.', 'spans_': [
// Attributes. // Attributes.
{value: 'name', start: 0, end: 10}, {value: 'name', start: 0, end: 10},
...@@ -121,7 +122,7 @@ TEST_F('OutputE2ETest', 'Links', function() { ...@@ -121,7 +122,7 @@ TEST_F('OutputE2ETest', 'Links', function() {
{value: 'role', start: 11, end: 15} {value: 'role', start: 11, end: 15}
]}, o.speechOutputForTest); ]}, o.speechOutputForTest);
checkBrailleOutput( checkBrailleOutput(
'Click here lnk', 'Click here lnk Press Search+Space to activate.',
[{value: new Output.NodeSpan(el), start: 0, end: 14}], [{value: new Output.NodeSpan(el), start: 0, end: 14}],
o); o);
}); });
...@@ -133,14 +134,14 @@ TEST_F('OutputE2ETest', 'Checkbox', function() { ...@@ -133,14 +134,14 @@ TEST_F('OutputE2ETest', 'Checkbox', function() {
var el = root.firstChild.firstChild; var el = root.firstChild.firstChild;
var range = cursors.Range.fromNode(el); var range = cursors.Range.fromNode(el);
var o = new Output().withSpeechAndBraille(range, null, 'navigate'); var o = new Output().withSpeechAndBraille(range, null, 'navigate');
checkSpeechOutput('|Check box|Not checked', checkSpeechOutput('|Check box|Not checked|Press Search+Space to toggle.',
[ [
{value: new Output.EarconAction('CHECK_OFF'), start: 0, end: 0}, {value: new Output.EarconAction('CHECK_OFF'), start: 0, end: 0},
{value: 'role', start: 1, end: 10} {value: 'role', start: 1, end: 10}
], ],
o); o);
checkBrailleOutput( checkBrailleOutput(
'chk ( )', 'chk ( ) Press Search+Space to toggle.',
[{value: new Output.NodeSpan(el), start: 0, end: 7}], [{value: new Output.NodeSpan(el), start: 0, end: 7}],
o); o);
}); });
...@@ -225,7 +226,9 @@ TEST_F('OutputE2ETest', 'Audio', function() { ...@@ -225,7 +226,9 @@ TEST_F('OutputE2ETest', 'Audio', function() {
function(root) { function(root) {
var el = root.find({role: 'button'}); var el = root.find({role: 'button'});
var range = cursors.Range.fromNode(el); var range = cursors.Range.fromNode(el);
var o = new Output().withSpeechAndBraille(range, null, 'navigate'); var o = new Output()
.withoutHints()
.withSpeechAndBraille(range, null, 'navigate');
checkSpeechOutput('play|Button|begin playback|audio|Tool bar', checkSpeechOutput('play|Button|begin playback|audio|Tool bar',
[ [
...@@ -245,7 +248,9 @@ TEST_F('OutputE2ETest', 'Audio', function() { ...@@ -245,7 +248,9 @@ TEST_F('OutputE2ETest', 'Audio', function() {
el = el.nextSibling.nextSibling; el = el.nextSibling.nextSibling;
var prevRange = range; var prevRange = range;
range = cursors.Range.fromNode(el); range = cursors.Range.fromNode(el);
var o = new Output().withSpeechAndBraille(range, prevRange, 'navigate'); var o = new Output()
.withoutHints()
.withSpeechAndBraille(range, prevRange, 'navigate');
checkSpeechOutput('0|Min 0|Max 0||Slider|audio time scrubber', checkSpeechOutput('0|Min 0|Max 0||Slider|audio time scrubber',
[ [
{value: 'valueForRange', start: 0, end: 1}, {value: 'valueForRange', start: 0, end: 1},
...@@ -329,7 +334,9 @@ TEST_F('OutputE2ETest', 'Input', function() { ...@@ -329,7 +334,9 @@ TEST_F('OutputE2ETest', 'Input', function() {
var el = root.firstChild.firstChild; var el = root.firstChild.firstChild;
expectedSpeechValues.forEach(function(expectedValue) { expectedSpeechValues.forEach(function(expectedValue) {
var range = cursors.Range.fromNode(el); var range = cursors.Range.fromNode(el);
var o = new Output().withSpeechAndBraille(range, null, 'navigate'); var o = new Output()
.withoutHints()
.withSpeechAndBraille(range, null, 'navigate');
if (typeof expectedValue == 'object') { if (typeof expectedValue == 'object') {
checkSpeechOutput(expectedValue[0], expectedValue[1], o); checkSpeechOutput(expectedValue[0], expectedValue[1], o);
} else { } else {
...@@ -342,7 +349,9 @@ TEST_F('OutputE2ETest', 'Input', function() { ...@@ -342,7 +349,9 @@ TEST_F('OutputE2ETest', 'Input', function() {
el = root.firstChild.firstChild; el = root.firstChild.firstChild;
expectedBrailleValues.forEach(function(expectedValue) { expectedBrailleValues.forEach(function(expectedValue) {
var range = cursors.Range.fromNode(el); var range = cursors.Range.fromNode(el);
var o = new Output().withBraille(range, null, 'navigate'); var o = new Output()
.withoutHints()
.withBraille(range, null, 'navigate');
if (typeof expectedValue === 'string') { if (typeof expectedValue === 'string') {
checkBrailleOutput( checkBrailleOutput(
expectedValue, expectedValue,
...@@ -630,7 +639,8 @@ TEST_F('OutputE2ETest', 'ToggleButton', function() { ...@@ -630,7 +639,8 @@ TEST_F('OutputE2ETest', 'ToggleButton', function() {
function(root) { function(root) {
var el = root.firstChild; var el = root.firstChild;
var o = new Output().withSpeech(cursors.Range.fromNode(el)); var o = new Output().withSpeech(cursors.Range.fromNode(el));
assertEqualsJSON({string_: '|Subscribe|Button|Pressed', spans_: [ assertEqualsJSON({string_: '|Subscribe|Button|Pressed|Press Search+Space to activate.',
spans_: [
{value: {earconId: 'CHECK_ON'}, start: 0, end: 0}, {value: {earconId: 'CHECK_ON'}, start: 0, end: 0},
{value: 'name', start: 1, end:10}, {value: 'name', start: 1, end:10},
{value: 'role', start: 11, end: 17} {value: 'role', start: 11, end: 17}
...@@ -740,7 +750,9 @@ TEST_F('OutputE2ETest', 'RangeOutput', function() { ...@@ -740,7 +750,9 @@ TEST_F('OutputE2ETest', 'RangeOutput', function() {
*/}, function(root) { */}, function(root) {
var obj = root.find({role: RoleType.SLIDER}); var obj = root.find({role: RoleType.SLIDER});
var o = new Output().withSpeech(cursors.Range.fromNode(obj)); var o = new Output()
.withoutHints()
.withSpeech(cursors.Range.fromNode(obj));
checkSpeechOutput('2|Min 1|Max 10|volume|Slider', checkSpeechOutput('2|Min 1|Max 10|volume|Slider',
[ [
{value: 'valueForRange', start: 0, end: 1}, {value: 'valueForRange', start: 0, end: 1},
...@@ -751,7 +763,9 @@ TEST_F('OutputE2ETest', 'RangeOutput', function() { ...@@ -751,7 +763,9 @@ TEST_F('OutputE2ETest', 'RangeOutput', function() {
o); o);
obj = root.find({role: RoleType.PROGRESS_INDICATOR}); obj = root.find({role: RoleType.PROGRESS_INDICATOR});
o = new Output().withSpeech(cursors.Range.fromNode(obj)); o = new Output()
.withoutHints()
.withSpeech(cursors.Range.fromNode(obj));
checkSpeechOutput('2|Min 1|Max 10|volume|Progress indicator', checkSpeechOutput('2|Min 1|Max 10|volume|Progress indicator',
[ [
{value: 'valueForRange', start: 0, end: 1}, {value: 'valueForRange', start: 0, end: 1},
...@@ -761,7 +775,9 @@ TEST_F('OutputE2ETest', 'RangeOutput', function() { ...@@ -761,7 +775,9 @@ TEST_F('OutputE2ETest', 'RangeOutput', function() {
o); o);
obj = root.find({role: RoleType.METER}); obj = root.find({role: RoleType.METER});
o = new Output().withSpeech(cursors.Range.fromNode(obj)); o = new Output()
.withoutHints()
.withSpeech(cursors.Range.fromNode(obj));
checkSpeechOutput('2|Min 1|Max 10|volume|Meter', checkSpeechOutput('2|Min 1|Max 10|volume|Meter',
[ [
{value: 'valueForRange', start: 0, end: 1}, {value: 'valueForRange', start: 0, end: 1},
...@@ -771,7 +787,9 @@ TEST_F('OutputE2ETest', 'RangeOutput', function() { ...@@ -771,7 +787,9 @@ TEST_F('OutputE2ETest', 'RangeOutput', function() {
o); o);
obj = root.find({role: RoleType.SPIN_BUTTON}); obj = root.find({role: RoleType.SPIN_BUTTON});
o = new Output().withSpeech(cursors.Range.fromNode(obj)); o = new Output()
.withoutHints()
.withSpeech(cursors.Range.fromNode(obj));
checkSpeechOutput('2|Min 1|Max 10|volume|Spin button', checkSpeechOutput('2|Min 1|Max 10|volume|Spin button',
[ [
{value: 'valueForRange', start: 0, end: 1}, {value: 'valueForRange', start: 0, end: 1},
...@@ -788,7 +806,9 @@ TEST_F('OutputE2ETest', 'RoleDescription', function() { ...@@ -788,7 +806,9 @@ TEST_F('OutputE2ETest', 'RoleDescription', function() {
<div aria-label="hi" role="button" aria-roledescription="foo"></div> <div aria-label="hi" role="button" aria-roledescription="foo"></div>
*/}, function(root) { */}, function(root) {
var obj = root.find({role: RoleType.BUTTON}); var obj = root.find({role: RoleType.BUTTON});
var o = new Output().withSpeech(cursors.Range.fromNode(obj)); var o = new Output()
.withoutHints()
.withSpeech(cursors.Range.fromNode(obj));
checkSpeechOutput('hi|foo', checkSpeechOutput('hi|foo',
[ [
{value: 'name', start: 0, end: 2}, {value: 'name', start: 0, end: 2},
......
...@@ -316,6 +316,7 @@ PanelNodeMenu.prototype = { ...@@ -316,6 +316,7 @@ PanelNodeMenu.prototype = {
if (this.pred_(node)) { if (this.pred_(node)) {
var output = new Output(); var output = new Output();
var range = cursors.Range.fromNode(node); var range = cursors.Range.fromNode(node);
output.withoutHints();
output.withSpeech(range, range, Output.EventType.NAVIGATE); output.withSpeech(range, range, Output.EventType.NAVIGATE);
var label = output.toString(); var label = output.toString();
this.addMenuItem(label, '', '', (function() { this.addMenuItem(label, '', '', (function() {
......
...@@ -2783,6 +2783,27 @@ If you're done with the tutorial, use ChromeVox to navigate to the Close button ...@@ -2783,6 +2783,27 @@ If you're done with the tutorial, use ChromeVox to navigate to the Close button
<message desc="Shown to a user when they invoke the read current title command in a context without a title." name="IDS_CHROMEVOX_NO_TITLE"> <message desc="Shown to a user when they invoke the read current title command in a context without a title." name="IDS_CHROMEVOX_NO_TITLE">
No title No title
</message> </message>
<message desc="Spoken when a user issues a command when nothing is focused." name="IDS_CHROMEVOX_WARNING_NO_CURRENT_RANGE">
No focus. Press Ctrl+T to open a new tab.
</message>
<message desc="A hint to the user that the current control is checkable." name="IDS_CHROMEVOX_HINT_CHECKABLE">
Press Search+Space to toggle.
</message>
<message desc="A hint to the user that the current control is clickable." name="IDS_CHROMEVOX_HINT_CLICKABLE">
Press Search+Space to activate.
</message>
<message desc="A hint to the user that the current control has a list of auto completions." name="IDS_CHROMEVOX_HINT_AUTOCOMPLETE_LIST">
Press up or down arrow for auto completions.
</message>
<message desc="A hint to the user that the current control has inline auto completions." name="IDS_CHROMEVOX_HINT_AUTOCOMPLETE_INLINE">
Type to auto complete.
</message>
<message desc="A hint to the user for interacting with the table control." name="IDS_CHROMEVOX_HINT_TABLE">
Press Search+Ctrl+Alt with arrows to navigate by cell.
</message>
<message desc="A hint to the user for interacting with the menu control." name="IDS_CHROMEVOX_HINT_MENU">
Press up or down arrow to navigate; enter to activate.
</message>
</messages> </messages>
</release> </release>
</grit> </grit>
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