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); |
}, |
/** |