OLD | NEW |
1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * @fileoverview Processes events related to editing text and emits the | 6 * @fileoverview Processes events related to editing text and emits the |
7 * appropriate spoken and braille feedback. | 7 * appropriate spoken and braille feedback. |
8 */ | 8 */ |
9 | 9 |
10 goog.provide('editing.TextEditHandler'); | 10 goog.provide('editing.TextEditHandler'); |
(...skipping 197 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
208 * A |ChromeVoxEditableTextBase| that implements text editing feedback | 208 * A |ChromeVoxEditableTextBase| that implements text editing feedback |
209 * for automation tree text fields using anchor and focus selection. | 209 * for automation tree text fields using anchor and focus selection. |
210 * @constructor | 210 * @constructor |
211 * @param {!AutomationNode} node | 211 * @param {!AutomationNode} node |
212 * @extends {AutomationEditableText} | 212 * @extends {AutomationEditableText} |
213 */ | 213 */ |
214 function AutomationRichEditableText(node) { | 214 function AutomationRichEditableText(node) { |
215 AutomationEditableText.call(this, node); | 215 AutomationEditableText.call(this, node); |
216 | 216 |
217 var root = this.node_.root; | 217 var root = this.node_.root; |
218 if (!root || !root.anchorObject || !root.focusObject) | 218 if (!root || !root.anchorObject || !root.focusObject || |
| 219 root.anchorOffset === undefined || root.focusOffset === undefined) |
219 return; | 220 return; |
220 | 221 |
221 this.line_ = new editing.EditableLine( | 222 this.anchorLine_ = new editing.EditableLine( |
222 root.anchorObject, root.anchorOffset, root.focusObject, root.focusOffset); | 223 root.anchorObject, root.anchorOffset, root.anchorObject, |
| 224 root.anchorOffset); |
| 225 this.focusLine_ = new editing.EditableLine( |
| 226 root.focusObject, root.focusOffset, root.focusObject, root.focusOffset); |
223 } | 227 } |
224 | 228 |
225 AutomationRichEditableText.prototype = { | 229 AutomationRichEditableText.prototype = { |
226 __proto__: AutomationEditableText.prototype, | 230 __proto__: AutomationEditableText.prototype, |
227 | 231 |
228 /** @override */ | 232 /** @override */ |
229 onUpdate: function() { | 233 onUpdate: function() { |
230 var root = this.node_.root; | 234 var root = this.node_.root; |
231 if (!root.anchorObject || !root.focusObject) | 235 if (!root.anchorObject || !root.focusObject || |
| 236 root.anchorOffset === undefined || root.focusOffset === undefined) |
232 return; | 237 return; |
233 | 238 |
| 239 var anchorLine = new editing.EditableLine( |
| 240 root.anchorObject, root.anchorOffset, root.anchorObject, |
| 241 root.anchorOffset); |
| 242 var focusLine = new editing.EditableLine( |
| 243 root.focusObject, root.focusOffset, root.focusObject, root.focusOffset); |
| 244 |
| 245 var prevAnchorLine = this.anchorLine_; |
| 246 var prevFocusLine = this.focusLine_; |
| 247 this.anchorLine_ = anchorLine; |
| 248 this.focusLine_ = focusLine; |
| 249 |
| 250 // Compute the current line based upon whether the current selection was |
| 251 // extended from anchor or focus. The default behavior is to compute lines |
| 252 // via focus. |
| 253 var baseLineOnStart = prevFocusLine.isSameLineAndSelection(focusLine); |
| 254 |
234 var cur = new editing.EditableLine( | 255 var cur = new editing.EditableLine( |
235 root.anchorObject, root.anchorOffset || 0, root.focusObject, | 256 root.anchorObject, root.anchorOffset, root.focusObject, |
236 root.focusOffset || 0); | 257 root.focusOffset, baseLineOnStart); |
237 var prev = this.line_; | 258 var prev = this.line_; |
238 this.line_ = cur; | 259 this.line_ = cur; |
239 | 260 |
240 if (prev.equals(cur)) { | 261 // Selection stayed within the same line(s) and didn't cross into new lines. |
241 // Collapsed cursor. | 262 if (anchorLine.isSameLine(prevAnchorLine) && |
| 263 focusLine.isSameLine(prevFocusLine)) { |
| 264 // Intra-line changes. |
242 this.changed(new cvox.TextChangeEvent( | 265 this.changed(new cvox.TextChangeEvent( |
243 cur.text || '', cur.startOffset || 0, cur.endOffset || 0, true)); | 266 cur.text || '', cur.startOffset, cur.endOffset, true)); |
244 this.brailleCurrentRichLine_(); | 267 this.brailleCurrentRichLine_(); |
245 | 268 |
246 // Finally, queue up any text markers/styles at bounds. | 269 // Finally, queue up any text markers/styles at bounds. |
247 var container = cur.startContainer_; | 270 var container = cur.startContainer_; |
248 if (!container) | 271 if (!container) |
249 return; | 272 return; |
250 | 273 |
251 if (container.markerTypes) { | 274 if (container.markerTypes) { |
252 // Only consider markers that start or end at the selection bounds. | 275 // Only consider markers that start or end at the selection bounds. |
253 var markerStartIndex = -1, markerEndIndex = -1; | 276 var markerStartIndex = -1, markerEndIndex = -1; |
(...skipping 22 matching lines...) Expand all Loading... |
276 | 299 |
277 // Start of the container. | 300 // Start of the container. |
278 if (cur.containerStartOffset == cur.startOffset) | 301 if (cur.containerStartOffset == cur.startOffset) |
279 this.speakTextStyle_(container); | 302 this.speakTextStyle_(container); |
280 else if (cur.containerEndOffset == cur.endOffset) | 303 else if (cur.containerEndOffset == cur.endOffset) |
281 this.speakTextStyle_(container, true); | 304 this.speakTextStyle_(container, true); |
282 | 305 |
283 return; | 306 return; |
284 } | 307 } |
285 | 308 |
286 // Just output the current line. | 309 if (cur.text == '') { |
287 cvox.ChromeVox.tts.speak(cur.text, cvox.QueueMode.CATEGORY_FLUSH); | 310 // This line has no text content. Describe the DOM selection. |
288 this.brailleCurrentRichLine_(); | 311 new Output() |
| 312 .withRichSpeechAndBraille( |
| 313 new Range(cur.start_, cur.end_), |
| 314 new Range(prev.start_, prev.end_), Output.EventType.NAVIGATE) |
| 315 .go(); |
| 316 } else { |
| 317 // Describe the current line. This accounts for previous/current |
| 318 // selections and picking the line edge boundary that changed (as computed |
| 319 // above). This is also the code path for describing paste. |
| 320 cvox.ChromeVox.tts.speak(cur.text, cvox.QueueMode.CATEGORY_FLUSH); |
| 321 this.brailleCurrentRichLine_(); |
| 322 } |
289 | 323 |
290 // The state in EditableTextBase needs to get updated with the new line | 324 // The state in EditableTextBase needs to get updated with the new line |
291 // contents, so that subsequent intra-line changes get the right state | 325 // contents, so that subsequent intra-line changes get the right state |
292 // transitions. | 326 // transitions. |
293 this.value = cur.text; | 327 this.value = cur.text; |
294 this.start = cur.startOffset; | 328 this.start = cur.startOffset; |
295 this.end = cur.endOffset; | 329 this.end = cur.endOffset; |
296 }, | 330 }, |
297 | 331 |
298 /** | 332 /** |
(...skipping 135 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
434 }; | 468 }; |
435 | 469 |
436 /** | 470 /** |
437 * @private {ChromeVoxStateObserver} | 471 * @private {ChromeVoxStateObserver} |
438 */ | 472 */ |
439 editing.observer_ = new editing.EditingChromeVoxStateObserver(); | 473 editing.observer_ = new editing.EditingChromeVoxStateObserver(); |
440 | 474 |
441 /** | 475 /** |
442 * An EditableLine encapsulates all data concerning a line in the automation | 476 * An EditableLine encapsulates all data concerning a line in the automation |
443 * tree necessary to provide output. | 477 * tree necessary to provide output. |
| 478 * Editable: an editable selection (e.g. start/end offsets) get saved. |
| 479 * Line: nodes/offsets at the beginning/end of a line get saved. |
| 480 * @param {!AutomationNode} startNode |
| 481 * @param {number} startIndex |
| 482 * @param {!AutomationNode} endNode |
| 483 * @param {number} endIndex |
| 484 * @param {boolean=} opt_baseLineOnStart Controls whether to use anchor or |
| 485 * focus for Line computations as described above. Selections are automatically |
| 486 * truncated up to either the line start or end. |
444 * @constructor | 487 * @constructor |
445 */ | 488 */ |
446 editing.EditableLine = function(startNode, startIndex, endNode, endIndex) { | 489 editing.EditableLine = function( |
| 490 startNode, startIndex, endNode, endIndex, opt_baseLineOnStart) { |
447 /** @private {!Cursor} */ | 491 /** @private {!Cursor} */ |
448 this.start_ = new Cursor(startNode, startIndex); | 492 this.start_ = new Cursor(startNode, startIndex); |
449 this.start_ = this.start_.deepEquivalent || this.start_; | 493 this.start_ = this.start_.deepEquivalent || this.start_; |
450 | 494 |
451 /** @private {!Cursor} */ | 495 /** @private {!Cursor} */ |
452 this.end_ = new Cursor(endNode, endIndex); | 496 this.end_ = new Cursor(endNode, endIndex); |
453 this.end_ = this.end_.deepEquivalent || this.end_; | 497 this.end_ = this.end_.deepEquivalent || this.end_; |
454 /** @private {number} */ | 498 /** @private {number} */ |
455 this.localContainerStartOffset_ = startIndex; | 499 this.localContainerStartOffset_ = startIndex; |
456 | 500 |
457 // Computed members. | 501 // Computed members. |
458 /** @private {Spannable} */ | 502 /** @private {Spannable} */ |
459 this.value_; | 503 this.value_; |
460 /** @private {AutomationNode} */ | 504 /** @private {AutomationNode} */ |
461 this.lineStart_; | 505 this.lineStart_; |
462 /** @private {AutomationNode} */ | 506 /** @private {AutomationNode} */ |
463 this.lineEnd_; | 507 this.lineEnd_; |
464 /** @private {AutomationNode|undefined} */ | 508 /** @private {AutomationNode|undefined} */ |
465 this.startContainer_; | 509 this.startContainer_; |
466 /** @private {AutomationNode|undefined} */ | 510 /** @private {AutomationNode|undefined} */ |
467 this.lineStartContainer_; | 511 this.lineStartContainer_; |
468 /** @private {number} */ | 512 /** @private {number} */ |
469 this.localLineStartContainerOffset_ = 0; | 513 this.localLineStartContainerOffset_ = 0; |
| 514 /** @private {AutomationNode|undefined} */ |
| 515 this.lineEndContainer_; |
| 516 /** @private {number} */ |
| 517 this.localLineEndContainerOffset_ = 0; |
470 | 518 |
471 this.computeLineData_(); | 519 this.computeLineData_(opt_baseLineOnStart); |
472 }; | 520 }; |
473 | 521 |
474 editing.EditableLine.prototype = { | 522 editing.EditableLine.prototype = { |
475 /** @private */ | 523 /** @private */ |
476 computeLineData_: function() { | 524 computeLineData_: function(opt_baseLineOnStart) { |
| 525 // Note that we calculate the line based only upon anchor or focus even if |
| 526 // they do not fall on the same line. It is up to the caller to specify |
| 527 // which end to base this line upon since it requires reasoning about two |
| 528 // lines. |
477 var nameLen = 0; | 529 var nameLen = 0; |
478 if (this.start_.node.name) | 530 var lineBase = opt_baseLineOnStart ? this.start_ : this.end_; |
479 nameLen = this.start_.node.name.length; | 531 var lineExtend = opt_baseLineOnStart ? this.end_ : this.start_; |
480 | 532 |
481 this.value_ = new Spannable(this.start_.node.name || '', this.start_); | 533 if (lineBase.node.name) |
482 if (this.start_.node == this.end_.node) | 534 nameLen = lineBase.node.name.length; |
483 this.value_.setSpan(this.end_, 0, nameLen); | 535 |
| 536 this.value_ = new Spannable(lineBase.node.name || '', lineBase); |
| 537 if (lineBase.node == lineExtend.node) |
| 538 this.value_.setSpan(lineExtend, 0, nameLen); |
484 | 539 |
485 // Initialize defaults. | 540 // Initialize defaults. |
486 this.lineStart_ = this.start_.node; | 541 this.lineStart_ = lineBase.node; |
487 this.lineEnd_ = this.lineStart_; | 542 this.lineEnd_ = this.lineStart_; |
488 this.startContainer_ = this.lineStart_.parent; | 543 this.startContainer_ = this.lineStart_.parent; |
489 this.lineStartContainer_ = this.lineStart_.parent; | 544 this.lineStartContainer_ = this.lineStart_.parent; |
| 545 this.lineEndContainer_ = this.lineStart_.parent; |
490 | 546 |
491 // Annotate each chunk with its associated inline text box node. | 547 // Annotate each chunk with its associated inline text box node. |
492 this.value_.setSpan(this.lineStart_, 0, this.lineStart_.name.length); | 548 this.value_.setSpan(this.lineStart_, 0, nameLen); |
493 | 549 |
494 // If the current selection is not on an inline text box (e.g. an image), | 550 // If the current selection is not on an inline text box (e.g. an image), |
495 // return early here so that the line contents are just the node. This is | 551 // return early here so that the line contents are just the node. This is |
496 // pending the ability to show non-text leaf inline objects. | 552 // pending the ability to show non-text leaf inline objects. |
497 if (this.lineStart_.role != RoleType.INLINE_TEXT_BOX) | 553 if (this.lineStart_.role != RoleType.INLINE_TEXT_BOX) |
498 return; | 554 return; |
499 | 555 |
500 // Also, track their static text parents. | 556 // Also, track their static text parents. |
501 var parents = [this.startContainer_]; | 557 var parents = [this.startContainer_]; |
502 | 558 |
(...skipping 23 matching lines...) Expand all Loading... |
526 this.lineEnd_ = lineEnd; | 582 this.lineEnd_ = lineEnd; |
527 if (parents[parents.length - 1] != lineEnd.parent) | 583 if (parents[parents.length - 1] != lineEnd.parent) |
528 parents.push(this.lineEnd_.parent); | 584 parents.push(this.lineEnd_.parent); |
529 | 585 |
530 var annotation = lineEnd; | 586 var annotation = lineEnd; |
531 if (lineEnd == this.end_.node) | 587 if (lineEnd == this.end_.node) |
532 annotation = this.end_; | 588 annotation = this.end_; |
533 | 589 |
534 this.value_.append(new Spannable(lineEnd.name, annotation)); | 590 this.value_.append(new Spannable(lineEnd.name, annotation)); |
535 } | 591 } |
| 592 this.lineEndContainer_ = this.lineEnd_.parent; |
536 | 593 |
537 // Finally, annotate with all parent static texts as NodeSpan's so that | 594 // Finally, annotate with all parent static texts as NodeSpan's so that |
538 // braille routing can key properly into the node with an offset. | 595 // braille routing can key properly into the node with an offset. |
539 // Note that both line start and end needs to account for | 596 // Note that both line start and end needs to account for |
540 // potential offsets into the static texts as follows. | 597 // potential offsets into the static texts as follows. |
541 var textCountBeforeLineStart = 0, textCountAfterLineEnd = 0; | 598 var textCountBeforeLineStart = 0, textCountAfterLineEnd = 0; |
542 var finder = this.lineStart_; | 599 var finder = this.lineStart_; |
543 while (finder.previousSibling) { | 600 while (finder.previousSibling) { |
544 finder = finder.previousSibling; | 601 finder = finder.previousSibling; |
545 textCountBeforeLineStart += finder.name.length; | 602 textCountBeforeLineStart += finder.name.length; |
546 } | 603 } |
547 this.localLineStartContainerOffset_ = textCountBeforeLineStart; | 604 this.localLineStartContainerOffset_ = textCountBeforeLineStart; |
548 | 605 |
549 finder = this.lineEnd_; | 606 finder = this.lineEnd_; |
550 while (finder.nextSibling) { | 607 while (finder.nextSibling) { |
551 finder = finder.nextSibling; | 608 finder = finder.nextSibling; |
552 textCountAfterLineEnd += finder.name.length; | 609 textCountAfterLineEnd += finder.name.length; |
553 } | 610 } |
554 | 611 |
| 612 if (this.lineEndContainer_.name) { |
| 613 this.localLineEndContainerOffset_ = |
| 614 this.lineEndContainer_.name.length - textCountAfterLineEnd; |
| 615 } |
| 616 |
555 var len = 0; | 617 var len = 0; |
556 for (var i = 0; i < parents.length; i++) { | 618 for (var i = 0; i < parents.length; i++) { |
557 var parent = parents[i]; | 619 var parent = parents[i]; |
558 | 620 |
559 if (!parent.name) | 621 if (!parent.name) |
560 continue; | 622 continue; |
561 | 623 |
562 var prevLen = len; | 624 var prevLen = len; |
563 | 625 |
564 var currentLen = parent.name.length; | 626 var currentLen = parent.name.length; |
(...skipping 20 matching lines...) Expand all Loading... |
585 } catch (e) { | 647 } catch (e) { |
586 } | 648 } |
587 } | 649 } |
588 }, | 650 }, |
589 | 651 |
590 /** | 652 /** |
591 * Gets the selection offset based on the text content of this line. | 653 * Gets the selection offset based on the text content of this line. |
592 * @return {number} | 654 * @return {number} |
593 */ | 655 */ |
594 get startOffset() { | 656 get startOffset() { |
595 return this.value_.getSpanStart(this.start_) + this.start_.index; | 657 // It is possible that the start cursor points to content before this line |
| 658 // (e.g. in a multi-line selection). |
| 659 try { |
| 660 return this.value_.getSpanStart(this.start_) + this.start_.index; |
| 661 } catch (e) { |
| 662 // When that happens, fall back to the start of this line. |
| 663 return 0; |
| 664 } |
596 }, | 665 }, |
597 | 666 |
598 /** | 667 /** |
599 * Gets the selection offset based on the text content of this line. | 668 * Gets the selection offset based on the text content of this line. |
600 * @return {number} | 669 * @return {number} |
601 */ | 670 */ |
602 get endOffset() { | 671 get endOffset() { |
603 return this.value_.getSpanStart(this.end_) + this.end_.index; | 672 try { |
| 673 return this.value_.getSpanStart(this.end_) + this.end_.index; |
| 674 } catch (e) { |
| 675 return this.value_.length; |
| 676 } |
604 }, | 677 }, |
605 | 678 |
606 /** | 679 /** |
607 * Gets the selection offset based on the parent's text. | 680 * Gets the selection offset based on the parent's text. |
608 * The parent is expected to be static text. | 681 * The parent is expected to be static text. |
609 * @return {number} | 682 * @return {number} |
610 */ | 683 */ |
611 get localStartOffset() { | 684 get localStartOffset() { |
612 return this.startOffset - this.containerStartOffset; | 685 return this.startOffset - this.containerStartOffset; |
613 }, | 686 }, |
(...skipping 29 matching lines...) Expand all Loading... |
643 * The text content of this line. | 716 * The text content of this line. |
644 * @return {string} The text of this line. | 717 * @return {string} The text of this line. |
645 */ | 718 */ |
646 get text() { | 719 get text() { |
647 return this.value_.toString(); | 720 return this.value_.toString(); |
648 }, | 721 }, |
649 | 722 |
650 /** | 723 /** |
651 * Returns true if |otherLine| surrounds the same line as |this|. Note that | 724 * Returns true if |otherLine| surrounds the same line as |this|. Note that |
652 * the contents of the line might be different. | 725 * the contents of the line might be different. |
| 726 * @param {editing.EditableLine} otherLine |
653 * @return {boolean} | 727 * @return {boolean} |
654 */ | 728 */ |
655 equals: function(otherLine) { | 729 isSameLine: function(otherLine) { |
656 // Equality is intentionally loose here as any of the state nodes can be | 730 // Equality is intentionally loose here as any of the state nodes can be |
657 // invalidated at any time. We rely upon the start/anchor of the line | 731 // invalidated at any time. We rely upon the start/anchor of the line |
658 // staying the same. | 732 // staying the same. |
659 return otherLine.lineStartContainer_ == this.lineStartContainer_ && | 733 return (otherLine.lineStartContainer_ == this.lineStartContainer_ && |
660 otherLine.localLineStartContainerOffset_ == | 734 otherLine.localLineStartContainerOffset_ == |
661 this.localLineStartContainerOffset_; | 735 this.localLineStartContainerOffset_) || |
| 736 (otherLine.lineEndContainer_ == this.lineEndContainer_ && |
| 737 otherLine.localLineEndContainerOffset_ == |
| 738 this.localLineEndContainerOffset_); |
| 739 }, |
| 740 |
| 741 /** |
| 742 * Returns true if |otherLine| surrounds the same line as |this| and has the |
| 743 * same selection. |
| 744 * @param {editing.EditableLine} otherLine |
| 745 * @return {boolean} |
| 746 */ |
| 747 isSameLineAndSelection: function(otherLine) { |
| 748 return this.isSameLine(otherLine) && |
| 749 this.startOffset == otherLine.startOffset && |
| 750 this.endOffset == otherLine.endOffset; |
662 } | 751 } |
663 }; | 752 }; |
664 | 753 |
665 }); | 754 }); |
OLD | NEW |