Chromium Code Reviews| 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.equalsStrict(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.equals(prevAnchorLine) && focusLine.equals(prevFocusLine)) { |
| 263 // Intra-line changes. | |
| 242 this.changed(new cvox.TextChangeEvent( | 264 this.changed(new cvox.TextChangeEvent( |
| 243 cur.text || '', cur.startOffset || 0, cur.endOffset || 0, true)); | 265 cur.text || '', cur.startOffset, cur.endOffset, true)); |
| 244 this.brailleCurrentRichLine_(); | 266 this.brailleCurrentRichLine_(); |
| 245 | 267 |
| 246 // Finally, queue up any text markers/styles at bounds. | 268 // Finally, queue up any text markers/styles at bounds. |
| 247 var container = cur.startContainer_; | 269 var container = cur.startContainer_; |
| 248 if (!container) | 270 if (!container) |
| 249 return; | 271 return; |
| 250 | 272 |
| 251 if (container.markerTypes) { | 273 if (container.markerTypes) { |
| 252 // Only consider markers that start or end at the selection bounds. | 274 // Only consider markers that start or end at the selection bounds. |
| 253 var markerStartIndex = -1, markerEndIndex = -1; | 275 var markerStartIndex = -1, markerEndIndex = -1; |
| (...skipping 22 matching lines...) Expand all Loading... | |
| 276 | 298 |
| 277 // Start of the container. | 299 // Start of the container. |
| 278 if (cur.containerStartOffset == cur.startOffset) | 300 if (cur.containerStartOffset == cur.startOffset) |
| 279 this.speakTextStyle_(container); | 301 this.speakTextStyle_(container); |
| 280 else if (cur.containerEndOffset == cur.endOffset) | 302 else if (cur.containerEndOffset == cur.endOffset) |
| 281 this.speakTextStyle_(container, true); | 303 this.speakTextStyle_(container, true); |
| 282 | 304 |
| 283 return; | 305 return; |
| 284 } | 306 } |
| 285 | 307 |
| 286 // Just output the current line. | 308 if (cur.text == '') { |
| 287 cvox.ChromeVox.tts.speak(cur.text, cvox.QueueMode.CATEGORY_FLUSH); | 309 // This line has no text content. Describe the DOM selection. |
| 288 this.brailleCurrentRichLine_(); | 310 new Output() |
| 311 .withRichSpeechAndBraille( | |
| 312 new Range(cur.start_, cur.end_), | |
| 313 new Range(prev.start_, prev.end_), Output.EventType.NAVIGATE) | |
| 314 .go(); | |
| 315 } else { | |
| 316 // Describe the current line. This accounts for previous/current | |
| 317 // selections and picking the line edge boundary that changed (as computed | |
| 318 // above). This is also the code path for describing paste. | |
| 319 cvox.ChromeVox.tts.speak(cur.text, cvox.QueueMode.CATEGORY_FLUSH); | |
| 320 this.brailleCurrentRichLine_(); | |
| 321 } | |
| 289 | 322 |
| 290 // The state in EditableTextBase needs to get updated with the new line | 323 // The state in EditableTextBase needs to get updated with the new line |
| 291 // contents, so that subsequent intra-line changes get the right state | 324 // contents, so that subsequent intra-line changes get the right state |
| 292 // transitions. | 325 // transitions. |
| 293 this.value = cur.text; | 326 this.value = cur.text; |
| 294 this.start = cur.startOffset; | 327 this.start = cur.startOffset; |
| 295 this.end = cur.endOffset; | 328 this.end = cur.endOffset; |
| 296 }, | 329 }, |
| 297 | 330 |
| 298 /** | 331 /** |
| (...skipping 135 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 434 }; | 467 }; |
| 435 | 468 |
| 436 /** | 469 /** |
| 437 * @private {ChromeVoxStateObserver} | 470 * @private {ChromeVoxStateObserver} |
| 438 */ | 471 */ |
| 439 editing.observer_ = new editing.EditingChromeVoxStateObserver(); | 472 editing.observer_ = new editing.EditingChromeVoxStateObserver(); |
| 440 | 473 |
| 441 /** | 474 /** |
| 442 * An EditableLine encapsulates all data concerning a line in the automation | 475 * An EditableLine encapsulates all data concerning a line in the automation |
| 443 * tree necessary to provide output. | 476 * tree necessary to provide output. |
| 477 * Editable: an editable selection (e.g. start/end offsets) get saved. | |
| 478 * Line: nodes/offsets at the beginning/end of a line get saved. | |
| 479 * @param {!AutomationNode} startNode | |
| 480 * @param {number} startIndex | |
| 481 * @param {!AutomationNode} endNode | |
| 482 * @param {number} endIndex | |
| 483 * @param {boolean=} opt_baseLineOnStart Controls whether to use anchor or | |
| 484 * focus for Line computations as described above. Selections are automatically | |
| 485 * truncated up to either the line start or end. | |
| 444 * @constructor | 486 * @constructor |
| 445 */ | 487 */ |
| 446 editing.EditableLine = function(startNode, startIndex, endNode, endIndex) { | 488 editing.EditableLine = function( |
| 489 startNode, startIndex, endNode, endIndex, opt_baseLineOnStart) { | |
| 447 /** @private {!Cursor} */ | 490 /** @private {!Cursor} */ |
| 448 this.start_ = new Cursor(startNode, startIndex); | 491 this.start_ = new Cursor(startNode, startIndex); |
| 449 this.start_ = this.start_.deepEquivalent || this.start_; | 492 this.start_ = this.start_.deepEquivalent || this.start_; |
| 450 | 493 |
| 451 /** @private {!Cursor} */ | 494 /** @private {!Cursor} */ |
| 452 this.end_ = new Cursor(endNode, endIndex); | 495 this.end_ = new Cursor(endNode, endIndex); |
| 453 this.end_ = this.end_.deepEquivalent || this.end_; | 496 this.end_ = this.end_.deepEquivalent || this.end_; |
| 454 /** @private {number} */ | 497 /** @private {number} */ |
| 455 this.localContainerStartOffset_ = startIndex; | 498 this.localContainerStartOffset_ = startIndex; |
| 456 | 499 |
| 457 // Computed members. | 500 // Computed members. |
| 458 /** @private {Spannable} */ | 501 /** @private {Spannable} */ |
| 459 this.value_; | 502 this.value_; |
| 460 /** @private {AutomationNode} */ | 503 /** @private {AutomationNode} */ |
| 461 this.lineStart_; | 504 this.lineStart_; |
| 462 /** @private {AutomationNode} */ | 505 /** @private {AutomationNode} */ |
| 463 this.lineEnd_; | 506 this.lineEnd_; |
| 464 /** @private {AutomationNode|undefined} */ | 507 /** @private {AutomationNode|undefined} */ |
| 465 this.startContainer_; | 508 this.startContainer_; |
| 466 /** @private {AutomationNode|undefined} */ | 509 /** @private {AutomationNode|undefined} */ |
| 467 this.lineStartContainer_; | 510 this.lineStartContainer_; |
| 468 /** @private {number} */ | 511 /** @private {number} */ |
| 469 this.localLineStartContainerOffset_ = 0; | 512 this.localLineStartContainerOffset_ = 0; |
| 513 /** @private {AutomationNode|undefined} */ | |
| 514 this.lineEndContainer_; | |
| 515 /** @private {number} */ | |
| 516 this.localLineEndContainerOffset_ = 0; | |
| 470 | 517 |
| 471 this.computeLineData_(); | 518 this.computeLineData_(opt_baseLineOnStart); |
| 472 }; | 519 }; |
| 473 | 520 |
| 474 editing.EditableLine.prototype = { | 521 editing.EditableLine.prototype = { |
| 475 /** @private */ | 522 /** @private */ |
| 476 computeLineData_: function() { | 523 computeLineData_: function(opt_baseLineOnStart) { |
| 524 // Note that we calculate the line based only upon anchor or focus even if | |
| 525 // they do not fall on the same line. It is up to the caller to specify | |
| 526 // which end to base this line upon since it requires reasoning about two | |
| 527 // lines. | |
| 477 var nameLen = 0; | 528 var nameLen = 0; |
| 478 if (this.start_.node.name) | 529 var lineBase = opt_baseLineOnStart ? this.start_ : this.end_; |
| 479 nameLen = this.start_.node.name.length; | 530 var lineExtend = opt_baseLineOnStart ? this.end_ : this.start_; |
| 480 | 531 |
| 481 this.value_ = new Spannable(this.start_.node.name || '', this.start_); | 532 if (lineBase.node.name) |
| 482 if (this.start_.node == this.end_.node) | 533 nameLen = lineBase.node.name.length; |
| 483 this.value_.setSpan(this.end_, 0, nameLen); | 534 |
| 535 this.value_ = new Spannable(lineBase.node.name || '', lineBase); | |
| 536 if (lineBase.node == lineExtend.node) | |
| 537 this.value_.setSpan(lineExtend, 0, nameLen); | |
| 484 | 538 |
| 485 // Initialize defaults. | 539 // Initialize defaults. |
| 486 this.lineStart_ = this.start_.node; | 540 this.lineStart_ = lineBase.node; |
| 487 this.lineEnd_ = this.lineStart_; | 541 this.lineEnd_ = this.lineStart_; |
| 488 this.startContainer_ = this.lineStart_.parent; | 542 this.startContainer_ = this.lineStart_.parent; |
| 489 this.lineStartContainer_ = this.lineStart_.parent; | 543 this.lineStartContainer_ = this.lineStart_.parent; |
| 544 this.lineEndContainer_ = this.lineStart_.parent; | |
| 490 | 545 |
| 491 // Annotate each chunk with its associated inline text box node. | 546 // Annotate each chunk with its associated inline text box node. |
| 492 this.value_.setSpan(this.lineStart_, 0, this.lineStart_.name.length); | 547 this.value_.setSpan(this.lineStart_, 0, nameLen); |
| 493 | 548 |
| 494 // If the current selection is not on an inline text box (e.g. an image), | 549 // 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 | 550 // 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. | 551 // pending the ability to show non-text leaf inline objects. |
| 497 if (this.lineStart_.role != RoleType.INLINE_TEXT_BOX) | 552 if (this.lineStart_.role != RoleType.INLINE_TEXT_BOX) |
| 498 return; | 553 return; |
| 499 | 554 |
| 500 // Also, track their static text parents. | 555 // Also, track their static text parents. |
| 501 var parents = [this.startContainer_]; | 556 var parents = [this.startContainer_]; |
| 502 | 557 |
| (...skipping 23 matching lines...) Expand all Loading... | |
| 526 this.lineEnd_ = lineEnd; | 581 this.lineEnd_ = lineEnd; |
| 527 if (parents[parents.length - 1] != lineEnd.parent) | 582 if (parents[parents.length - 1] != lineEnd.parent) |
| 528 parents.push(this.lineEnd_.parent); | 583 parents.push(this.lineEnd_.parent); |
| 529 | 584 |
| 530 var annotation = lineEnd; | 585 var annotation = lineEnd; |
| 531 if (lineEnd == this.end_.node) | 586 if (lineEnd == this.end_.node) |
| 532 annotation = this.end_; | 587 annotation = this.end_; |
| 533 | 588 |
| 534 this.value_.append(new Spannable(lineEnd.name, annotation)); | 589 this.value_.append(new Spannable(lineEnd.name, annotation)); |
| 535 } | 590 } |
| 591 this.lineEndContainer_ = this.lineEnd_.parent; | |
| 536 | 592 |
| 537 // Finally, annotate with all parent static texts as NodeSpan's so that | 593 // Finally, annotate with all parent static texts as NodeSpan's so that |
| 538 // braille routing can key properly into the node with an offset. | 594 // braille routing can key properly into the node with an offset. |
| 539 // Note that both line start and end needs to account for | 595 // Note that both line start and end needs to account for |
| 540 // potential offsets into the static texts as follows. | 596 // potential offsets into the static texts as follows. |
| 541 var textCountBeforeLineStart = 0, textCountAfterLineEnd = 0; | 597 var textCountBeforeLineStart = 0, textCountAfterLineEnd = 0; |
| 542 var finder = this.lineStart_; | 598 var finder = this.lineStart_; |
| 543 while (finder.previousSibling) { | 599 while (finder.previousSibling) { |
| 544 finder = finder.previousSibling; | 600 finder = finder.previousSibling; |
| 545 textCountBeforeLineStart += finder.name.length; | 601 textCountBeforeLineStart += finder.name.length; |
| 546 } | 602 } |
| 547 this.localLineStartContainerOffset_ = textCountBeforeLineStart; | 603 this.localLineStartContainerOffset_ = textCountBeforeLineStart; |
| 548 | 604 |
| 549 finder = this.lineEnd_; | 605 finder = this.lineEnd_; |
| 550 while (finder.nextSibling) { | 606 while (finder.nextSibling) { |
| 551 finder = finder.nextSibling; | 607 finder = finder.nextSibling; |
| 552 textCountAfterLineEnd += finder.name.length; | 608 textCountAfterLineEnd += finder.name.length; |
| 553 } | 609 } |
| 554 | 610 |
| 611 if (this.lineEndContainer_.name) { | |
| 612 this.localLineEndContainerOffset_ = | |
| 613 this.lineEndContainer_.name.length - textCountAfterLineEnd; | |
| 614 } | |
| 615 | |
| 555 var len = 0; | 616 var len = 0; |
| 556 for (var i = 0; i < parents.length; i++) { | 617 for (var i = 0; i < parents.length; i++) { |
| 557 var parent = parents[i]; | 618 var parent = parents[i]; |
| 558 | 619 |
| 559 if (!parent.name) | 620 if (!parent.name) |
| 560 continue; | 621 continue; |
| 561 | 622 |
| 562 var prevLen = len; | 623 var prevLen = len; |
| 563 | 624 |
| 564 var currentLen = parent.name.length; | 625 var currentLen = parent.name.length; |
| (...skipping 20 matching lines...) Expand all Loading... | |
| 585 } catch (e) { | 646 } catch (e) { |
| 586 } | 647 } |
| 587 } | 648 } |
| 588 }, | 649 }, |
| 589 | 650 |
| 590 /** | 651 /** |
| 591 * Gets the selection offset based on the text content of this line. | 652 * Gets the selection offset based on the text content of this line. |
| 592 * @return {number} | 653 * @return {number} |
| 593 */ | 654 */ |
| 594 get startOffset() { | 655 get startOffset() { |
| 595 return this.value_.getSpanStart(this.start_) + this.start_.index; | 656 // It is possible that the start cursor points to content before this line |
| 657 // (e.g. in a multi-line selection). | |
| 658 try { | |
| 659 return this.value_.getSpanStart(this.start_) + this.start_.index; | |
| 660 } catch (e) { | |
| 661 // When that happens, fall back to the start of this line. | |
| 662 return 0; | |
| 663 } | |
| 596 }, | 664 }, |
| 597 | 665 |
| 598 /** | 666 /** |
| 599 * Gets the selection offset based on the text content of this line. | 667 * Gets the selection offset based on the text content of this line. |
| 600 * @return {number} | 668 * @return {number} |
| 601 */ | 669 */ |
| 602 get endOffset() { | 670 get endOffset() { |
| 603 return this.value_.getSpanStart(this.end_) + this.end_.index; | 671 try { |
| 672 return this.value_.getSpanStart(this.end_) + this.end_.index; | |
| 673 } catch (e) { | |
| 674 return this.value_.length; | |
| 675 } | |
| 604 }, | 676 }, |
| 605 | 677 |
| 606 /** | 678 /** |
| 607 * Gets the selection offset based on the parent's text. | 679 * Gets the selection offset based on the parent's text. |
| 608 * The parent is expected to be static text. | 680 * The parent is expected to be static text. |
| 609 * @return {number} | 681 * @return {number} |
| 610 */ | 682 */ |
| 611 get localStartOffset() { | 683 get localStartOffset() { |
| 612 return this.startOffset - this.containerStartOffset; | 684 return this.startOffset - this.containerStartOffset; |
| 613 }, | 685 }, |
| (...skipping 27 matching lines...) Expand all Loading... | |
| 641 | 713 |
| 642 /** | 714 /** |
| 643 * The text content of this line. | 715 * The text content of this line. |
| 644 * @return {string} The text of this line. | 716 * @return {string} The text of this line. |
| 645 */ | 717 */ |
| 646 get text() { | 718 get text() { |
| 647 return this.value_.toString(); | 719 return this.value_.toString(); |
| 648 }, | 720 }, |
| 649 | 721 |
| 650 /** | 722 /** |
| 723 * Returns true if |otherLine| shares its start position with this line. | |
| 724 * @return {boolean} | |
|
dmazzoni
2017/06/21 06:22:07
Add @param {editing.EditableLine}, I'm assuming a
David Tseng
2017/06/21 16:58:28
Acknowledged and done elsewhere for isSameLine and
| |
| 725 */ | |
| 726 equalsStart: function(otherLine) { | |
|
dmazzoni
2017/06/21 06:22:08
how about hasSameStart?
David Tseng
2017/06/21 16:58:28
Start is really overloaded in all of the editing c
| |
| 727 return otherLine.lineStartContainer_ == this.lineStartContainer_ && | |
| 728 otherLine.localLineStartContainerOffset_ == | |
| 729 this.localLineStartContainerOffset_; | |
| 730 }, | |
| 731 | |
| 732 /** | |
| 733 * Returns true if |otherLine| shares its end position with this line. | |
| 734 * @return {boolean} | |
| 735 */ | |
| 736 equalsEnd: function(otherLine) { | |
| 737 return otherLine.lineEndContainer_ == this.lineEndContainer_ && | |
| 738 otherLine.localLineEndContainerOffset_ == | |
| 739 this.localLineEndContainerOffset_; | |
| 740 }, | |
| 741 | |
| 742 /** | |
| 651 * Returns true if |otherLine| surrounds the same line as |this|. Note that | 743 * Returns true if |otherLine| surrounds the same line as |this|. Note that |
| 652 * the contents of the line might be different. | 744 * the contents of the line might be different. |
| 653 * @return {boolean} | 745 * @return {boolean} |
| 654 */ | 746 */ |
| 655 equals: function(otherLine) { | 747 equals: function(otherLine) { |
|
dmazzoni
2017/06/21 06:22:07
I think it'd be more clear to rename equalsStrict
| |
| 656 // Equality is intentionally loose here as any of the state nodes can be | 748 // 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 | 749 // invalidated at any time. We rely upon the start/anchor of the line |
| 658 // staying the same. | 750 // staying the same. |
| 659 return otherLine.lineStartContainer_ == this.lineStartContainer_ && | 751 return this.equalsStart(otherLine) || this.equalsEnd(otherLine); |
| 660 otherLine.localLineStartContainerOffset_ == | 752 }, |
| 661 this.localLineStartContainerOffset_; | 753 |
| 754 /** | |
| 755 * Returns true if |otherLine| surrounds the same line as |this| and has the | |
| 756 * same selection. | |
| 757 * @return {boolean} | |
| 758 */ | |
| 759 equalsStrict: function(otherLine) { | |
| 760 return this.equals(otherLine) && | |
| 761 this.startOffset == otherLine.startOffset && | |
| 762 this.endOffset == otherLine.endOffset; | |
| 662 } | 763 } |
| 663 }; | 764 }; |
| 664 | 765 |
| 665 }); | 766 }); |
| OLD | NEW |