Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1625)

Unified Diff: chrome/browser/resources/chromeos/chromevox/cvox2/background/output.js

Issue 1094463003: Reland: Split spoken feedback up on a per format rule basis. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Fix tests Created 5 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/browser/resources/chromeos/chromevox/cvox2/background/output.js
diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/output.js b/chrome/browser/resources/chromeos/chromevox/cvox2/background/output.js
index 1d90039527692cff43650cd42f81342703a7d353..1b509f03daa3a1a090e179932b0ef00868695ffa 100644
--- a/chrome/browser/resources/chromeos/chromevox/cvox2/background/output.js
+++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/output.js
@@ -46,10 +46,10 @@ var Dir = AutomationUtil.Dir;
*/
Output = function() {
// TODO(dtseng): Include braille specific rules.
- /** @type {!cvox.Spannable} */
- this.buffer_ = new cvox.Spannable();
- /** @type {!cvox.Spannable} */
- this.brailleBuffer_ = new cvox.Spannable();
+ /** @type {!Array<cvox.Spannable>} */
+ this.buffer_ = [];
+ /** @type {!Array<cvox.Spannable>} */
+ this.brailleBuffer_ = [];
/** @type {!Array<Object>} */
this.locations_ = [];
/** @type {function()} */
@@ -90,9 +90,12 @@ Output.ROLE_INFO_ = {
msgId: 'tag_button',
earcon: 'BUTTON'
},
- checkbox: {
+ checkBox: {
msgId: 'input_type_checkbox'
},
+ dialog: {
+ msgId: 'dialog'
+ },
heading: {
msgId: 'aria_role_heading',
},
@@ -127,6 +130,26 @@ Output.ROLE_INFO_ = {
};
/**
+ * Metadata about supported automation states.
+ * @const {!Object<string,
+ * {on: {msgId: string, earconId: string},
+ * off: {msgId: string, earconId: string}}>}
+ * @private
+ */
+Output.STATE_INFO_ = {
+ checked: {
+ on: {
+ earconId: 'CHECK_ON',
+ msgId: 'checkbox_checked_state'
+ },
+ off: {
+ earconId: 'CHECK_OFF',
+ msgId: 'checkbox_unchecked_state'
+ }
+ }
+};
+
+/**
* Rules specifying format of AutomationNodes for output.
* @type {!Object<string, Object<string, Object<string, string>>>}
*/
@@ -137,15 +160,10 @@ Output.RULES = {
braille: ''
},
alert: {
- speak: '!doNotInterrupt ' +
- '@aria_role_alert $name $earcon(ALERT_NONMODAL) $descendants'
+ speak: '!doNotInterrupt $role $descendants'
},
checkBox: {
- speak: '$if($checked, @describe_checkbox_checked($name), ' +
- '@describe_checkbox_unchecked($name)) ' +
- '$if($checked, ' +
- '$earcon(CHECK_ON, @input_type_checkbox), ' +
- '$earcon(CHECK_OFF, @input_type_checkbox))'
+ speak: '$name $role $checked'
},
dialog: {
enter: '$name $role'
@@ -158,9 +176,9 @@ Output.RULES = {
speak: '$value='
},
link: {
- enter: '$name= $visited $earcon(LINK, @tag_link)=',
- stay: '$name= $visited @tag_link',
- speak: '$name= $visited $earcon(LINK, @tag_link)='
+ enter: '$name $visited $role',
+ stay: '$name= $visited $role',
+ speak: '$name= $visited $role'
},
list: {
enter: '@aria_role_list @list_with_items($parentChildCount)'
@@ -181,15 +199,12 @@ Output.RULES = {
speak: '$value'
},
popUpButton: {
- speak: '$value $name @tag_button @aria_has_popup $earcon(LISTBOX) ' +
+ speak: '$value $name $role @aria_has_popup ' +
'$if($collapsed, @aria_expanded_false, @aria_expanded_true)'
},
radioButton: {
speak: '$if($checked, @describe_radio_selected($name), ' +
- '@describe_radio_unselected($name)) ' +
- '$if($checked, ' +
- '$earcon(CHECK_ON, @input_type_radio), ' +
- '$earcon(CHECK_OFF, @input_type_radio))'
+ '@describe_radio_unselected($name))'
},
slider: {
speak: '@describe_slider($value, $name)'
@@ -210,12 +225,12 @@ Output.RULES = {
},
menuStart: {
'default': {
- speak: '@chrome_menu_opened($name) $role $earcon(OBJECT_OPEN)'
+ speak: '@chrome_menu_opened($name) $earcon(OBJECT_OPEN)'
}
},
menuEnd: {
'default': {
- speak: '$earcon(OBJECT_CLOSE)'
+ speak: '@chrome_menu_closed $earcon(OBJECT_CLOSE)'
}
},
menuListValueChanged: {
@@ -234,15 +249,6 @@ Output.RULES = {
};
/**
- * Alias equivalent attributes.
- * @type {!Object<string, string>}
- */
-Output.ATTRIBUTE_ALIAS = {
- name: 'value',
- value: 'name'
-};
-
-/**
* Custom actions performed while rendering an output string.
* @param {function()} action
* @constructor
@@ -258,6 +264,23 @@ Output.Action.prototype = {
};
/**
+ * Action to play a earcon.
+ * @param {string} earconId
+ * @constructor
+ * @extends {Output.Action}
+ */
+Output.EarconAction = function(earconId) {
+ Output.Action.call(this, function() {
+ cvox.ChromeVox.earcons.playEarcon(
+ cvox.AbstractEarcons[earconId]);
+ });
+};
+
+Output.EarconAction.prototype = {
+ __proto__: Output.Action.prototype
+};
+
+/**
* Annotation for selection.
* @param {number} startIndex
* @param {number} endIndex
@@ -282,8 +305,11 @@ Output.prototype = {
* Gets the output buffer for speech.
* @return {!cvox.Spannable}
*/
- getBuffer: function() {
- return this.buffer_;
+ toSpannable: function() {
+ return this.buffer_.reduce(function(prev, cur) {
+ prev.append(cur);
+ return prev;
+ }, new cvox.Spannable());
},
/**
@@ -364,45 +390,56 @@ Output.prototype = {
*/
go: function() {
// Speech.
- var buff = this.buffer_;
- if (buff.toString()) {
- if (this.speechStartCallback_)
- this.speechProperties_['startCallback'] = this.speechStartCallback_;
- if (this.speechEndCallback_) {
- this.speechProperties_['endCallback'] = this.speechEndCallback_;
+ var queueMode = cvox.QueueMode.FLUSH;
+ this.buffer_.forEach(function(buff, i, a) {
+ if (buff.toString()) {
+ if (this.speechStartCallback_ && i == 0)
+ this.speechProperties_['startCallback'] = this.speechStartCallback_;
+ else
+ this.speechProperties_['startCallback'] = null;
+ if (this.speechEndCallback_ && i == a.length - 1)
+ this.speechProperties_['endCallback'] = this.speechEndCallback_;
+ else
+ this.speechProperties_['endCallback'] = null;
+ cvox.ChromeVox.tts.speak(
+ buff.toString(), queueMode, this.speechProperties_);
+ queueMode = cvox.QueueMode.QUEUE;
}
-
- cvox.ChromeVox.tts.speak(
- buff.toString(), cvox.QueueMode.FLUSH, this.speechProperties_);
- }
-
- var actions = buff.getSpansInstanceOf(Output.Action);
- if (actions) {
- actions.forEach(function(a) {
- a.run();
- });
- }
+ var actions = buff.getSpansInstanceOf(Output.Action);
+ if (actions) {
+ actions.forEach(function(a) {
+ a.run();
+ });
+ }
+ }.bind(this));
// Braille.
+ var buff = this.brailleBuffer_.reduce(function(prev, cur) {
+ if (prev.getLength() > 0 && cur.getLength() > 0)
+ prev.append(Output.SPACE);
+ prev.append(cur);
+ return prev;
+ }, new cvox.Spannable());
+
var selSpan =
- this.brailleBuffer_.getSpanInstanceOf(Output.SelectionSpan);
+ buff.getSpanInstanceOf(Output.SelectionSpan);
var startIndex = -1, endIndex = -1;
if (selSpan) {
// Casts ok, since the span is known to be in the spannable.
var valueStart =
- /** @type {number} */ (this.brailleBuffer_.getSpanStart(selSpan));
+ /** @type {number} */ (buff.getSpanStart(selSpan));
var valueEnd =
- /** @type {number} */ (this.brailleBuffer_.getSpanEnd(selSpan));
+ /** @type {number} */ (buff.getSpanEnd(selSpan));
startIndex = valueStart + selSpan.startIndex;
endIndex = valueStart + selSpan.endIndex;
- this.brailleBuffer_.setSpan(new cvox.ValueSpan(0),
+ buff.setSpan(new cvox.ValueSpan(0),
valueStart, valueEnd);
- this.brailleBuffer_.setSpan(new cvox.ValueSelectionSpan(),
+ buff.setSpan(new cvox.ValueSelectionSpan(),
startIndex, endIndex);
}
var output = new cvox.NavBraille({
- text: this.brailleBuffer_,
+ text: buff,
startIndex: startIndex,
endIndex: endIndex
});
@@ -420,7 +457,7 @@ Output.prototype = {
* @param {!cursors.Range} range
* @param {cursors.Range} prevRange
* @param {chrome.automation.EventType|string} type
- * @param {!cvox.Spannable} buff Buffer to receive rendered output.
+ * @param {!Array<cvox.Spannable>} buff Buffer to receive rendered output.
* @private
*/
render_: function(range, prevRange, type, buff) {
@@ -435,7 +472,7 @@ Output.prototype = {
* @param {chrome.automation.AutomationNode} node
* @param {string|!Object} format The output format either specified as an
* output template string or a parsed output format tree.
- * @param {!cvox.Spannable} buff Buffer to receive rendered output.
+ * @param {!Array<cvox.Spannable>} buff Buffer to receive rendered output.
* @param {!Object=} opt_exclude A set of attributes to exclude.
* @private
*/
@@ -469,8 +506,9 @@ Output.prototype = {
// Set suffix options.
var options = {};
- options.ifEmpty = token[token.length - 1] == '=';
- if (options.ifEmpty)
+ options.annotation = [];
+ options.isUnique = token[token.length - 1] == '=';
+ if (options.isUnique)
token = token.substring(0, token.length - 1);
// Process token based on prefix.
@@ -482,30 +520,31 @@ Output.prototype = {
// All possible tokens based on prefix.
if (prefix == '$') {
- options.annotation = token;
if (token == 'value') {
var text = node.attributes.value;
if (text !== undefined) {
- var offset = buff.getLength();
if (node.attributes.textSelStart !== undefined) {
- options.annotation = new Output.SelectionSpan(
+ options.annotation.push(new Output.SelectionSpan(
node.attributes.textSelStart,
- node.attributes.textSelEnd);
+ node.attributes.textSelEnd));
}
- } else if (node.role == chrome.automation.RoleType.staticText) {
- // TODO(dtseng): Remove once Blink treats staticText values as
- // names.
- text = node.attributes.name;
}
- this.addToSpannable_(buff, text, options);
+ // Annotate this as a name so we don't duplicate names from ancestors.
+ if (node.role == chrome.automation.RoleType.inlineTextBox)
+ token = 'name';
+ options.annotation.push(token);
+ this.append_(buff, text, options);
} else if (token == 'indexInParent') {
- this.addToSpannable_(buff, node.indexInParent + 1);
+ options.annotation.push(token);
+ this.append_(buff, node.indexInParent + 1);
} else if (token == 'parentChildCount') {
+ options.annotation.push(token);
if (node.parent)
- this.addToSpannable_(buff, node.parent.children.length);
+ this.append_(buff, node.parent.children.length);
} else if (token == 'state') {
+ options.annotation.push(token);
Object.getOwnPropertyNames(node.state).forEach(function(s) {
- this.addToSpannable_(buff, s, options);
+ this.append_(buff, s, options);
}.bind(this));
} else if (token == 'find') {
// Find takes two arguments: JSON query string and format string.
@@ -534,6 +573,7 @@ Output.prototype = {
new cursors.Cursor(rightmost, 0));
this.range_(subrange, null, 'navigate', buff);
} else if (token == 'role') {
+ options.annotation.push(token);
var msg = node.role;
var earconId = null;
var info = Output.ROLE_INFO_[node.role];
@@ -546,17 +586,23 @@ Output.prototype = {
} else {
console.error('Missing role info for ' + node.role);
}
- if (earconId) {
- options.annotation = new Output.Action(function() {
- cvox.ChromeVox.earcons.playEarcon(
- cvox.AbstractEarcons[earconId]);
- });
- }
- this.addToSpannable_(buff, msg, options);
- } else if (node.attributes[token]) {
- this.addToSpannable_(buff, node.attributes[token], options);
- } else if (node.state[token]) {
- this.addToSpannable_(buff, token, options);
+ if (earconId)
+ options.annotation.push(new Output.EarconAction(earconId));
+ this.append_(buff, msg, options);
+ } else if (node.attributes[token] !== undefined) {
+ options.annotation.push(token);
+ this.append_(buff, node.attributes[token], options);
+ } else if (Output.STATE_INFO_[token]) {
+ options.annotation.push('state');
+ var stateInfo = Output.STATE_INFO_[token];
+ var resolvedInfo = node.state[token] ? stateInfo.on : stateInfo.off;
+ options.annotation.push(
+ new Output.EarconAction(resolvedInfo.earconId));
+ var msgId =
+ this.formatOptions_.braille ? resolvedInfo.msgId + '_brl' :
+ resolvedInfo.msgId;
+ var msg = cvox.ChromeVox.msgs.getMsg(msgId);
+ this.append_(buff, msg, options);
} else if (tree.firstChild) {
// Custom functions.
if (token == 'if') {
@@ -567,14 +613,13 @@ Output.prototype = {
else
this.format_(node, cond.nextSibling.nextSibling, buff);
} else if (token == 'earcon') {
- var contentBuff = new cvox.Spannable();
- if (tree.firstChild.nextSibling)
- this.format_(node, tree.firstChild.nextSibling, contentBuff);
- options.annotation = new Output.Action(function() {
- cvox.ChromeVox.earcons.playEarcon(
- cvox.AbstractEarcons[tree.firstChild.value]);
- });
- this.addToSpannable_(buff, contentBuff, options);
+ // Assumes there's existing output in our buffer.
+ var lastBuff = buff[buff.length - 1];
+ if (!lastBuff)
+ return;
+
+ lastBuff.setSpan(
+ new Output.EarconAction(tree.firstChild.value), 0, 0);
}
}
} else if (prefix == '@') {
@@ -588,9 +633,9 @@ Output.prototype = {
console.error('Unexpected value: ' + arg);
return;
}
- var msgBuff = new cvox.Spannable();
+ var msgBuff = [];
this.format_(node, arg, msgBuff);
- msgArgs.push(msgBuff.toString());
+ msgArgs = msgArgs.concat(msgBuff);
curMsg = curMsg.nextSibling;
}
var msg = cvox.ChromeVox.msgs.getMsg(msgId, msgArgs);
@@ -600,7 +645,7 @@ Output.prototype = {
} catch(e) {}
if (msg) {
- this.addToSpannable_(buff, msg, options);
+ this.append_(buff, msg, options);
}
} else if (prefix == '!') {
this.speechProperties_[token] = true;
@@ -612,7 +657,7 @@ Output.prototype = {
* @param {!cursors.Range} range
* @param {cursors.Range} prevRange
* @param {chrome.automation.EventType|string} type
- * @param {!cvox.Spannable} rangeBuff
+ * @param {!Array<cvox.Spannable>} rangeBuff
* @private
*/
range_: function(range, prevRange, type, rangeBuff) {
@@ -623,7 +668,7 @@ Output.prototype = {
var prevNode = prevRange.getStart().getNode();
var formatNodeAndAncestors = function(node, prevNode) {
- var buff = new cvox.Spannable();
+ var buff = [];
this.ancestry_(node, prevNode, type, buff);
this.node_(node, prevNode, type, buff);
if (this.formatOptions_.location)
@@ -633,22 +678,21 @@ Output.prototype = {
while (cursor.getNode() != range.getEnd().getNode()) {
var node = cursor.getNode();
- this.addToSpannable_(
- rangeBuff, formatNodeAndAncestors(node, prevNode));
+ rangeBuff.push.apply(rangeBuff, formatNodeAndAncestors(node, prevNode));
prevNode = node;
cursor = cursor.move(cursors.Unit.NODE,
cursors.Movement.DIRECTIONAL,
Dir.FORWARD);
}
var lastNode = range.getEnd().getNode();
- this.addToSpannable_(rangeBuff, formatNodeAndAncestors(lastNode, prevNode));
+ rangeBuff.push.apply(rangeBuff, formatNodeAndAncestors(lastNode, prevNode));
},
/**
* @param {!chrome.automation.AutomationNode} node
* @param {!chrome.automation.AutomationNode} prevNode
* @param {chrome.automation.EventType|string} type
- * @param {!cvox.Spannable} buff
+ * @param {!Array<cvox.Spannable>} buff
* @param {!Object=} opt_exclude A list of attributes to exclude from
* processing.
* @private
@@ -671,7 +715,7 @@ Output.prototype = {
this.format_(formatPrevNode, roleBlock.leave, buff, opt_exclude);
}
- var enterOutput = [];
+ var enterOutputs = [];
var enterRole = {};
for (var j = uniqueAncestors.length - 2, formatNode;
(formatNode = uniqueAncestors[j]);
@@ -681,14 +725,16 @@ Output.prototype = {
if (enterRole[formatNode.role])
continue;
enterRole[formatNode.role] = true;
- var tempBuff = new cvox.Spannable('');
+ var tempBuff = [];
this.format_(formatNode, roleBlock.enter, tempBuff, opt_exclude);
- enterOutput.unshift(tempBuff);
+ enterOutputs.unshift(tempBuff);
}
+ if (formatNode.role == 'window')
+ break;
}
- enterOutput.forEach(function(c) {
- this.addToSpannable_(buff, c);
- }.bind(this));
+ enterOutputs.forEach(function(b) {
+ buff.push.apply(buff, b);
+ });
if (!opt_exclude.stay) {
var commonFormatNode = uniqueAncestors[0];
@@ -706,7 +752,7 @@ Output.prototype = {
* @param {!chrome.automation.AutomationNode} node
* @param {!chrome.automation.AutomationNode} prevNode
* @param {chrome.automation.EventType|string} type
- * @param {!cvox.Spannable} buff
+ * @param {!Array<cvox.Spannable>} buff
* @private
*/
node_: function(node, prevNode, type, buff) {
@@ -721,7 +767,7 @@ Output.prototype = {
* @param {!cursors.Range} range
* @param {cursors.Range} prevRange
* @param {chrome.automation.EventType|string} type
- * @param {!cvox.Spannable} buff
+ * @param {!Array<cvox.Spannable>} buff
* @private
*/
subNode_: function(range, prevRange, type, buff) {
@@ -736,38 +782,47 @@ Output.prototype = {
var endIndex = range.getEnd().getIndex();
if (startIndex === endIndex)
endIndex++;
- this.addToSpannable_(
+ this.append_(
buff, range.getStart().getText().substring(startIndex, endIndex));
},
/**
- * Adds to the given buffer with proper delimiters added.
- * @param {!cvox.Spannable} spannable
+ * Appends output to the |buff|.
+ * @param {!Array<cvox.Spannable>} buff
* @param {string|!cvox.Spannable} value
- * @param {{ifEmpty: boolean,
- * annotation: (string|Output.Action|undefined)}=} opt_options
+ * @param {{isUnique: (boolean|undefined),
+ * annotation: !Array<*>}=} opt_options
*/
- addToSpannable_: function(spannable, value, opt_options) {
- opt_options = opt_options || {ifEmpty: false, annotation: undefined};
- if ((!value || value.length == 0) && !opt_options.annotation)
+ append_: function(buff, value, opt_options) {
+ opt_options = opt_options || {isUnique: false, annotation: []};
+
+ // Reject empty values without annotations.
+ if ((!value || value.length == 0) && opt_options.annotation.length == 0)
return;
- var spannableToAdd = new cvox.Spannable(value, opt_options.annotation);
- if (spannable.getLength() == 0) {
- spannable.append(spannableToAdd);
+ var spannableToAdd = new cvox.Spannable(value);
+ opt_options.annotation.forEach(function(a) {
+ spannableToAdd.setSpan(a, 0, spannableToAdd.getLength());
+ });
+
+ // Early return if the buffer is empty.
+ if (buff.length == 0) {
+ buff.push(spannableToAdd);
return;
}
- if (opt_options.ifEmpty &&
- opt_options.annotation &&
- (spannable.getSpanStart(opt_options.annotation) != undefined ||
- spannable.getSpanStart(
- Output.ATTRIBUTE_ALIAS[opt_options.annotation]) != undefined))
- return;
+ // |isUnique| specifies an annotation that cannot be duplicated.
+ if (opt_options.isUnique) {
+ var alreadyAnnotated = buff.some(function(s) {
+ return opt_options.annotation.some(function(annotation) {
+ return s.getSpanStart(annotation) != undefined;
+ });
+ });
+ if (alreadyAnnotated)
+ return;
+ }
- var prefixed = new cvox.Spannable(Output.SPACE);
- prefixed.append(spannableToAdd);
- spannable.append(prefixed);
+ buff.push(spannableToAdd);
},
/**

Powered by Google App Engine
This is Rietveld 408576698