Index: chrome/browser/resources/chromeos/chromevox/cvox2/background/editing.js |
diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/editing.js b/chrome/browser/resources/chromeos/chromevox/cvox2/background/editing.js |
index c18ebb4fc4d148cf2ac9e7dacb6c445495e47f46..4a729cbc796c57e6e57fc24d73607204c2235fb2 100644 |
--- a/chrome/browser/resources/chromeos/chromevox/cvox2/background/editing.js |
+++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/editing.js |
@@ -215,11 +215,15 @@ function AutomationRichEditableText(node) { |
AutomationEditableText.call(this, node); |
var root = this.node_.root; |
- if (!root || !root.anchorObject || !root.focusObject) |
+ if (!root || !root.anchorObject || !root.focusObject || |
+ root.anchorOffset === undefined || root.focusOffset === undefined) |
return; |
- this.line_ = new editing.EditableLine( |
- root.anchorObject, root.anchorOffset, root.focusObject, root.focusOffset); |
+ this.anchorLine_ = new editing.EditableLine( |
+ root.anchorObject, root.anchorOffset, root.anchorObject, |
+ root.anchorOffset); |
+ this.focusLine_ = new editing.EditableLine( |
+ root.focusObject, root.focusOffset, root.focusObject, root.focusOffset); |
} |
AutomationRichEditableText.prototype = { |
@@ -228,19 +232,38 @@ AutomationRichEditableText.prototype = { |
/** @override */ |
onUpdate: function() { |
var root = this.node_.root; |
- if (!root.anchorObject || !root.focusObject) |
+ if (!root.anchorObject || !root.focusObject || |
+ root.anchorOffset === undefined || root.focusOffset === undefined) |
return; |
+ var anchorLine = new editing.EditableLine( |
+ root.anchorObject, root.anchorOffset, root.anchorObject, |
+ root.anchorOffset); |
+ var focusLine = new editing.EditableLine( |
+ root.focusObject, root.focusOffset, root.focusObject, root.focusOffset); |
+ |
+ var prevAnchorLine = this.anchorLine_; |
+ var prevFocusLine = this.focusLine_; |
+ this.anchorLine_ = anchorLine; |
+ this.focusLine_ = focusLine; |
+ |
+ // Compute the current line based upon whether the current selection was |
+ // extended from anchor or focus. The default behavior is to compute lines |
+ // via focus. |
+ var baseLineOnStart = prevFocusLine.isSameLineAndSelection(focusLine); |
+ |
var cur = new editing.EditableLine( |
- root.anchorObject, root.anchorOffset || 0, root.focusObject, |
- root.focusOffset || 0); |
+ root.anchorObject, root.anchorOffset, root.focusObject, |
+ root.focusOffset, baseLineOnStart); |
var prev = this.line_; |
this.line_ = cur; |
- if (prev.equals(cur)) { |
- // Collapsed cursor. |
+ // Selection stayed within the same line(s) and didn't cross into new lines. |
+ if (anchorLine.isSameLine(prevAnchorLine) && |
+ focusLine.isSameLine(prevFocusLine)) { |
+ // Intra-line changes. |
this.changed(new cvox.TextChangeEvent( |
- cur.text || '', cur.startOffset || 0, cur.endOffset || 0, true)); |
+ cur.text || '', cur.startOffset, cur.endOffset, true)); |
this.brailleCurrentRichLine_(); |
// Finally, queue up any text markers/styles at bounds. |
@@ -283,9 +306,20 @@ AutomationRichEditableText.prototype = { |
return; |
} |
- // Just output the current line. |
- cvox.ChromeVox.tts.speak(cur.text, cvox.QueueMode.CATEGORY_FLUSH); |
- this.brailleCurrentRichLine_(); |
+ if (cur.text == '') { |
+ // This line has no text content. Describe the DOM selection. |
+ new Output() |
+ .withRichSpeechAndBraille( |
+ new Range(cur.start_, cur.end_), |
+ new Range(prev.start_, prev.end_), Output.EventType.NAVIGATE) |
+ .go(); |
+ } else { |
+ // Describe the current line. This accounts for previous/current |
+ // selections and picking the line edge boundary that changed (as computed |
+ // above). This is also the code path for describing paste. |
+ cvox.ChromeVox.tts.speak(cur.text, cvox.QueueMode.CATEGORY_FLUSH); |
+ this.brailleCurrentRichLine_(); |
+ } |
// The state in EditableTextBase needs to get updated with the new line |
// contents, so that subsequent intra-line changes get the right state |
@@ -441,9 +475,19 @@ editing.observer_ = new editing.EditingChromeVoxStateObserver(); |
/** |
* An EditableLine encapsulates all data concerning a line in the automation |
* tree necessary to provide output. |
+ * Editable: an editable selection (e.g. start/end offsets) get saved. |
+ * Line: nodes/offsets at the beginning/end of a line get saved. |
+ * @param {!AutomationNode} startNode |
+ * @param {number} startIndex |
+ * @param {!AutomationNode} endNode |
+ * @param {number} endIndex |
+ * @param {boolean=} opt_baseLineOnStart Controls whether to use anchor or |
+ * focus for Line computations as described above. Selections are automatically |
+ * truncated up to either the line start or end. |
* @constructor |
*/ |
-editing.EditableLine = function(startNode, startIndex, endNode, endIndex) { |
+editing.EditableLine = function( |
+ startNode, startIndex, endNode, endIndex, opt_baseLineOnStart) { |
/** @private {!Cursor} */ |
this.start_ = new Cursor(startNode, startIndex); |
this.start_ = this.start_.deepEquivalent || this.start_; |
@@ -467,29 +511,41 @@ editing.EditableLine = function(startNode, startIndex, endNode, endIndex) { |
this.lineStartContainer_; |
/** @private {number} */ |
this.localLineStartContainerOffset_ = 0; |
+ /** @private {AutomationNode|undefined} */ |
+ this.lineEndContainer_; |
+ /** @private {number} */ |
+ this.localLineEndContainerOffset_ = 0; |
- this.computeLineData_(); |
+ this.computeLineData_(opt_baseLineOnStart); |
}; |
editing.EditableLine.prototype = { |
/** @private */ |
- computeLineData_: function() { |
+ computeLineData_: function(opt_baseLineOnStart) { |
+ // Note that we calculate the line based only upon anchor or focus even if |
+ // they do not fall on the same line. It is up to the caller to specify |
+ // which end to base this line upon since it requires reasoning about two |
+ // lines. |
var nameLen = 0; |
- if (this.start_.node.name) |
- nameLen = this.start_.node.name.length; |
+ var lineBase = opt_baseLineOnStart ? this.start_ : this.end_; |
+ var lineExtend = opt_baseLineOnStart ? this.end_ : this.start_; |
- this.value_ = new Spannable(this.start_.node.name || '', this.start_); |
- if (this.start_.node == this.end_.node) |
- this.value_.setSpan(this.end_, 0, nameLen); |
+ if (lineBase.node.name) |
+ nameLen = lineBase.node.name.length; |
+ |
+ this.value_ = new Spannable(lineBase.node.name || '', lineBase); |
+ if (lineBase.node == lineExtend.node) |
+ this.value_.setSpan(lineExtend, 0, nameLen); |
// Initialize defaults. |
- this.lineStart_ = this.start_.node; |
+ this.lineStart_ = lineBase.node; |
this.lineEnd_ = this.lineStart_; |
this.startContainer_ = this.lineStart_.parent; |
this.lineStartContainer_ = this.lineStart_.parent; |
+ this.lineEndContainer_ = this.lineStart_.parent; |
// Annotate each chunk with its associated inline text box node. |
- this.value_.setSpan(this.lineStart_, 0, this.lineStart_.name.length); |
+ this.value_.setSpan(this.lineStart_, 0, nameLen); |
// If the current selection is not on an inline text box (e.g. an image), |
// return early here so that the line contents are just the node. This is |
@@ -533,6 +589,7 @@ editing.EditableLine.prototype = { |
this.value_.append(new Spannable(lineEnd.name, annotation)); |
} |
+ this.lineEndContainer_ = this.lineEnd_.parent; |
// Finally, annotate with all parent static texts as NodeSpan's so that |
// braille routing can key properly into the node with an offset. |
@@ -552,6 +609,11 @@ editing.EditableLine.prototype = { |
textCountAfterLineEnd += finder.name.length; |
} |
+ if (this.lineEndContainer_.name) { |
+ this.localLineEndContainerOffset_ = |
+ this.lineEndContainer_.name.length - textCountAfterLineEnd; |
+ } |
+ |
var len = 0; |
for (var i = 0; i < parents.length; i++) { |
var parent = parents[i]; |
@@ -592,7 +654,14 @@ editing.EditableLine.prototype = { |
* @return {number} |
*/ |
get startOffset() { |
- return this.value_.getSpanStart(this.start_) + this.start_.index; |
+ // It is possible that the start cursor points to content before this line |
+ // (e.g. in a multi-line selection). |
+ try { |
+ return this.value_.getSpanStart(this.start_) + this.start_.index; |
+ } catch (e) { |
+ // When that happens, fall back to the start of this line. |
+ return 0; |
+ } |
}, |
/** |
@@ -600,7 +669,11 @@ editing.EditableLine.prototype = { |
* @return {number} |
*/ |
get endOffset() { |
- return this.value_.getSpanStart(this.end_) + this.end_.index; |
+ try { |
+ return this.value_.getSpanStart(this.end_) + this.end_.index; |
+ } catch (e) { |
+ return this.value_.length; |
+ } |
}, |
/** |
@@ -650,15 +723,31 @@ editing.EditableLine.prototype = { |
/** |
* Returns true if |otherLine| surrounds the same line as |this|. Note that |
* the contents of the line might be different. |
+ * @param {editing.EditableLine} otherLine |
* @return {boolean} |
*/ |
- equals: function(otherLine) { |
+ isSameLine: function(otherLine) { |
// Equality is intentionally loose here as any of the state nodes can be |
// invalidated at any time. We rely upon the start/anchor of the line |
// staying the same. |
- return otherLine.lineStartContainer_ == this.lineStartContainer_ && |
- otherLine.localLineStartContainerOffset_ == |
- this.localLineStartContainerOffset_; |
+ return (otherLine.lineStartContainer_ == this.lineStartContainer_ && |
+ otherLine.localLineStartContainerOffset_ == |
+ this.localLineStartContainerOffset_) || |
+ (otherLine.lineEndContainer_ == this.lineEndContainer_ && |
+ otherLine.localLineEndContainerOffset_ == |
+ this.localLineEndContainerOffset_); |
+ }, |
+ |
+ /** |
+ * Returns true if |otherLine| surrounds the same line as |this| and has the |
+ * same selection. |
+ * @param {editing.EditableLine} otherLine |
+ * @return {boolean} |
+ */ |
+ isSameLineAndSelection: function(otherLine) { |
+ return this.isSameLine(otherLine) && |
+ this.startOffset == otherLine.startOffset && |
+ this.endOffset == otherLine.endOffset; |
} |
}; |