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 235 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
246 | 246 |
247 var prevAnchorLine = this.anchorLine_; | 247 var prevAnchorLine = this.anchorLine_; |
248 var prevFocusLine = this.focusLine_; | 248 var prevFocusLine = this.focusLine_; |
249 this.anchorLine_ = anchorLine; | 249 this.anchorLine_ = anchorLine; |
250 this.focusLine_ = focusLine; | 250 this.focusLine_ = focusLine; |
251 | 251 |
252 // Compute the current line based upon whether the current selection was | 252 // Compute the current line based upon whether the current selection was |
253 // extended from anchor or focus. The default behavior is to compute lines | 253 // extended from anchor or focus. The default behavior is to compute lines |
254 // via focus. | 254 // via focus. |
255 var baseLineOnStart = prevFocusLine.isSameLineAndSelection(focusLine); | 255 var baseLineOnStart = prevFocusLine.isSameLineAndSelection(focusLine); |
| 256 var isSameSelection = |
| 257 baseLineOnStart && prevAnchorLine.isSameLineAndSelection(anchorLine); |
256 | 258 |
257 var cur = new editing.EditableLine( | 259 var cur; |
258 root.anchorObject, root.anchorOffset, root.focusObject, | 260 if (isSameSelection && this.line_) { |
259 root.focusOffset, baseLineOnStart); | 261 // Nothing changed, return. |
| 262 return; |
| 263 } else { |
| 264 cur = new editing.EditableLine( |
| 265 root.anchorObject, root.anchorOffset, root.focusObject, |
| 266 root.focusOffset, baseLineOnStart); |
| 267 } |
260 var prev = this.line_; | 268 var prev = this.line_; |
261 this.line_ = cur; | 269 this.line_ = cur; |
262 | 270 |
263 // Selection stayed within the same line(s) and didn't cross into new lines. | 271 // Selection stayed within the same line(s) and didn't cross into new lines. |
264 if (anchorLine.isSameLine(prevAnchorLine) && | 272 if (anchorLine.isSameLine(prevAnchorLine) && |
265 focusLine.isSameLine(prevFocusLine)) { | 273 focusLine.isSameLine(prevFocusLine)) { |
266 // Intra-line changes. | 274 // Intra-line changes. |
267 this.changed(new cvox.TextChangeEvent( | 275 this.changed(new cvox.TextChangeEvent( |
268 cur.text || '', cur.startOffset, cur.endOffset, true)); | 276 cur.text || '', cur.startOffset, cur.endOffset, true)); |
269 this.brailleCurrentRichLine_(); | 277 this.brailleCurrentRichLine_(); |
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
310 | 318 |
311 if (cur.text == '') { | 319 if (cur.text == '') { |
312 // This line has no text content. Describe the DOM selection. | 320 // This line has no text content. Describe the DOM selection. |
313 new Output() | 321 new Output() |
314 .withRichSpeechAndBraille( | 322 .withRichSpeechAndBraille( |
315 new Range(cur.start_, cur.end_), | 323 new Range(cur.start_, cur.end_), |
316 new Range(prev.start_, prev.end_), Output.EventType.NAVIGATE) | 324 new Range(prev.start_, prev.end_), Output.EventType.NAVIGATE) |
317 .go(); | 325 .go(); |
318 } else if (!cur.hasCollapsedSelection()) { | 326 } else if (!cur.hasCollapsedSelection()) { |
319 // This is a selection. | 327 // This is a selection. |
320 cvox.ChromeVox.tts.speak(cur.selectedText, cvox.QueueMode.CATEGORY_FLUSH); | 328 |
321 cvox.ChromeVox.tts.speak(Msgs.getMsg('selected'), cvox.QueueMode.QUEUE); | 329 // Speech requires many more states than braille. |
| 330 // TODO(dtseng): base/extent and anchor/focus are mismatched in AX. Swap |
| 331 // once this works. |
| 332 var curBase = baseLineOnStart ? focusLine : anchorLine; |
| 333 var curExtent = baseLineOnStart ? anchorLine : focusLine; |
| 334 var text = ''; |
| 335 var suffixMsg = ''; |
| 336 if (curBase.isBeforeLine(curExtent)) { |
| 337 // Forward selection. |
| 338 if (prev.isBeforeLine(curBase)) { |
| 339 // Wrapped across the baseline. Read out the new selection. |
| 340 suffixMsg = 'selected'; |
| 341 text = this.getTextSelection_( |
| 342 curBase.startContainer_, curBase.localStartOffset, |
| 343 curExtent.endContainer_, curExtent.localEndOffset); |
| 344 } else { |
| 345 if (prev.isBeforeLine(curExtent)) { |
| 346 // Grew. |
| 347 suffixMsg = 'selected'; |
| 348 text = this.getTextSelection_( |
| 349 prev.endContainer_, prev.localEndOffset, |
| 350 curExtent.endContainer_, curExtent.localEndOffset); |
| 351 } else { |
| 352 // Shrank. |
| 353 suffixMsg = 'unselected'; |
| 354 text = this.getTextSelection_( |
| 355 curExtent.endContainer_, curExtent.localEndOffset, |
| 356 prev.endContainer_, prev.localEndOffset); |
| 357 } |
| 358 } |
| 359 } else { |
| 360 // Backward selection. |
| 361 if (curBase.isBeforeLine(prev)) { |
| 362 // Wrapped across the baseline. Read out the new selection. |
| 363 suffixMsg = 'selected'; |
| 364 text = this.getTextSelection_( |
| 365 curExtent.startContainer_, curExtent.localStartOffset, |
| 366 curBase.endContainer_, curBase.localEndOffset); |
| 367 } else { |
| 368 if (curExtent.isBeforeLine(prev)) { |
| 369 // Grew. |
| 370 suffixMsg = 'selected'; |
| 371 text = this.getTextSelection_( |
| 372 curExtent.startContainer_, curExtent.localStartOffset, |
| 373 prev.startContainer_, prev.localStartOffset); |
| 374 } else { |
| 375 // Shrank. |
| 376 suffixMsg = 'unselected'; |
| 377 text = this.getTextSelection_( |
| 378 prev.startContainer_, prev.localStartOffset, |
| 379 curExtent.startContainer_, curExtent.localStartOffset); |
| 380 } |
| 381 } |
| 382 } |
| 383 |
| 384 cvox.ChromeVox.tts.speak(text, cvox.QueueMode.CATEGORY_FLUSH); |
| 385 cvox.ChromeVox.tts.speak(Msgs.getMsg(suffixMsg), cvox.QueueMode.QUEUE); |
322 this.brailleCurrentRichLine_(); | 386 this.brailleCurrentRichLine_(); |
323 } else { | 387 } else { |
324 // Describe the current line. This accounts for previous/current | 388 // Describe the current line. This accounts for previous/current |
325 // selections and picking the line edge boundary that changed (as computed | 389 // selections and picking the line edge boundary that changed (as computed |
326 // above). This is also the code path for describing paste. | 390 // above). This is also the code path for describing paste. |
327 this.speakCurrentRichLine_(prev); | 391 this.speakCurrentRichLine_(prev); |
328 this.brailleCurrentRichLine_(); | 392 this.brailleCurrentRichLine_(); |
329 } | 393 } |
330 | 394 |
331 // The state in EditableTextBase needs to get updated with the new line | 395 // The state in EditableTextBase needs to get updated with the new line |
332 // contents, so that subsequent intra-line changes get the right state | 396 // contents, so that subsequent intra-line changes get the right state |
333 // transitions. | 397 // transitions. |
334 this.value = cur.text; | 398 this.value = cur.text; |
335 this.start = cur.startOffset; | 399 this.start = cur.startOffset; |
336 this.end = cur.endOffset; | 400 this.end = cur.endOffset; |
337 }, | 401 }, |
338 | 402 |
339 /** | 403 /** |
| 404 * @param {AutomationNode|undefined} startNode |
| 405 * @param {number} startOffset |
| 406 * @param {AutomationNode|undefined} endNode |
| 407 * @param {number} endOffset |
| 408 * @return {string} |
| 409 */ |
| 410 getTextSelection_: function(startNode, startOffset, endNode, endOffset) { |
| 411 if (!startNode || !endNode) |
| 412 return ''; |
| 413 |
| 414 if (startNode == endNode) { |
| 415 return startNode.name ? startNode.name.substring(startOffset, endOffset) : |
| 416 ''; |
| 417 } |
| 418 |
| 419 var text = ''; |
| 420 if (startNode.name) |
| 421 text = startNode.name.substring(startOffset); |
| 422 |
| 423 for (var node = startNode; |
| 424 (node = AutomationUtil.findNextNode( |
| 425 node, Dir.FORWARD, AutomationPredicate.leafOrStaticText)) && |
| 426 node != endNode;) { |
| 427 // Padding needs to get added to break up speech utterances. |
| 428 if (node.name) |
| 429 text += ' ' + node.name; |
| 430 } |
| 431 |
| 432 if (endNode.name) |
| 433 text += ' ' + endNode.name.substring(0, endOffset); |
| 434 return text; |
| 435 }, |
| 436 |
| 437 /** |
340 * @param {number} markerType | 438 * @param {number} markerType |
341 * @param {boolean=} opt_end | 439 * @param {boolean=} opt_end |
342 * @private | 440 * @private |
343 */ | 441 */ |
344 speakTextMarker_: function(markerType, opt_end) { | 442 speakTextMarker_: function(markerType, opt_end) { |
345 // TODO(dtseng): Plumb through constants to automation. | 443 // TODO(dtseng): Plumb through constants to automation. |
346 var msgs = []; | 444 var msgs = []; |
347 if (markerType & 1) | 445 if (markerType & 1) |
348 msgs.push(opt_end ? 'misspelling_end' : 'misspelling_start'); | 446 msgs.push(opt_end ? 'misspelling_end' : 'misspelling_start'); |
349 if (markerType & 2) | 447 if (markerType & 2) |
(...skipping 186 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
536 startNode, startIndex, endNode, endIndex, opt_baseLineOnStart) { | 634 startNode, startIndex, endNode, endIndex, opt_baseLineOnStart) { |
537 /** @private {!Cursor} */ | 635 /** @private {!Cursor} */ |
538 this.start_ = new Cursor(startNode, startIndex); | 636 this.start_ = new Cursor(startNode, startIndex); |
539 this.start_ = this.start_.deepEquivalent || this.start_; | 637 this.start_ = this.start_.deepEquivalent || this.start_; |
540 | 638 |
541 /** @private {!Cursor} */ | 639 /** @private {!Cursor} */ |
542 this.end_ = new Cursor(endNode, endIndex); | 640 this.end_ = new Cursor(endNode, endIndex); |
543 this.end_ = this.end_.deepEquivalent || this.end_; | 641 this.end_ = this.end_.deepEquivalent || this.end_; |
544 /** @private {number} */ | 642 /** @private {number} */ |
545 this.localContainerStartOffset_ = startIndex; | 643 this.localContainerStartOffset_ = startIndex; |
| 644 /** @private {number} */ |
| 645 this.localContainerEndOffset_ = endIndex; |
546 | 646 |
547 // Computed members. | 647 // Computed members. |
548 /** @private {Spannable} */ | 648 /** @private {Spannable} */ |
549 this.value_; | 649 this.value_; |
550 /** @private {AutomationNode|undefined} */ | 650 /** @private {AutomationNode|undefined} */ |
551 this.lineStart_; | 651 this.lineStart_; |
552 /** @private {AutomationNode|undefined} */ | 652 /** @private {AutomationNode|undefined} */ |
553 this.lineEnd_; | 653 this.lineEnd_; |
554 /** @private {AutomationNode|undefined} */ | 654 /** @private {AutomationNode|undefined} */ |
555 this.startContainer_; | 655 this.startContainer_; |
(...skipping 20 matching lines...) Expand all Loading... |
576 var lineBase = opt_baseLineOnStart ? this.start_ : this.end_; | 676 var lineBase = opt_baseLineOnStart ? this.start_ : this.end_; |
577 var lineExtend = opt_baseLineOnStart ? this.end_ : this.start_; | 677 var lineExtend = opt_baseLineOnStart ? this.end_ : this.start_; |
578 | 678 |
579 if (lineBase.node.name) | 679 if (lineBase.node.name) |
580 nameLen = lineBase.node.name.length; | 680 nameLen = lineBase.node.name.length; |
581 | 681 |
582 this.value_ = new Spannable(lineBase.node.name || '', lineBase); | 682 this.value_ = new Spannable(lineBase.node.name || '', lineBase); |
583 if (lineBase.node == lineExtend.node) | 683 if (lineBase.node == lineExtend.node) |
584 this.value_.setSpan(lineExtend, 0, nameLen); | 684 this.value_.setSpan(lineExtend, 0, nameLen); |
585 | 685 |
| 686 this.startContainer_ = this.start_.node; |
| 687 if (this.startContainer_.role == RoleType.INLINE_TEXT_BOX) |
| 688 this.startContainer_ = this.startContainer_.parent; |
| 689 this.endContainer_ = this.end_.node; |
| 690 if (this.endContainer_.role == RoleType.INLINE_TEXT_BOX) |
| 691 this.endContainer_ = this.endContainer_.parent; |
| 692 |
586 // Initialize defaults. | 693 // Initialize defaults. |
587 this.lineStart_ = lineBase.node; | 694 this.lineStart_ = lineBase.node; |
588 this.lineEnd_ = this.lineStart_; | 695 this.lineEnd_ = this.lineStart_; |
589 this.startContainer_ = this.lineStart_.parent; | |
590 this.lineStartContainer_ = this.lineStart_.parent; | 696 this.lineStartContainer_ = this.lineStart_.parent; |
591 this.lineEndContainer_ = this.lineStart_.parent; | 697 this.lineEndContainer_ = this.lineStart_.parent; |
592 | 698 |
593 // Annotate each chunk with its associated inline text box node. | 699 // Annotate each chunk with its associated inline text box node. |
594 this.value_.setSpan(this.lineStart_, 0, nameLen); | 700 this.value_.setSpan(this.lineStart_, 0, nameLen); |
595 | 701 |
596 // Also, track the nodes necessary for selection (either their parents, in | 702 // Also, track the nodes necessary for selection (either their parents, in |
597 // the case of inline text boxes, or the node itself). | 703 // the case of inline text boxes, or the node itself). |
598 var parents = [this.startContainer_]; | 704 var parents = [this.startContainer_]; |
599 | 705 |
(...skipping 132 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
732 return this.value_.length; | 838 return this.value_.length; |
733 } | 839 } |
734 }, | 840 }, |
735 | 841 |
736 /** | 842 /** |
737 * Gets the selection offset based on the parent's text. | 843 * Gets the selection offset based on the parent's text. |
738 * The parent is expected to be static text. | 844 * The parent is expected to be static text. |
739 * @return {number} | 845 * @return {number} |
740 */ | 846 */ |
741 get localStartOffset() { | 847 get localStartOffset() { |
742 return this.startOffset - this.containerStartOffset; | 848 return this.localContainerStartOffset_; |
743 }, | 849 }, |
744 | 850 |
745 /** | 851 /** |
746 * Gets the selection offset based on the parent's text. | 852 * Gets the selection offset based on the parent's text. |
747 * The parent is expected to be static text. | 853 * The parent is expected to be static text. |
748 * @return {number} | 854 * @return {number} |
749 */ | 855 */ |
750 get localEndOffset() { | 856 get localEndOffset() { |
751 return this.endOffset - this.containerStartOffset; | 857 return this.localContainerEndOffset_; |
752 }, | 858 }, |
753 | 859 |
754 /** | 860 /** |
755 * Gets the start offset of the container, relative to the line text content. | 861 * Gets the start offset of the container, relative to the line text content. |
756 * The container refers to the static text parenting the inline text box. | 862 * The container refers to the static text parenting the inline text box. |
757 * @return {number} | 863 * @return {number} |
758 */ | 864 */ |
759 get containerStartOffset() { | 865 get containerStartOffset() { |
760 return this.value_.getSpanStart(this.startContainer_); | 866 return this.value_.getSpanStart(this.startContainer_); |
761 }, | 867 }, |
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
808 /** | 914 /** |
809 * Returns true if |otherLine| surrounds the same line as |this| and has the | 915 * Returns true if |otherLine| surrounds the same line as |this| and has the |
810 * same selection. | 916 * same selection. |
811 * @param {editing.EditableLine} otherLine | 917 * @param {editing.EditableLine} otherLine |
812 * @return {boolean} | 918 * @return {boolean} |
813 */ | 919 */ |
814 isSameLineAndSelection: function(otherLine) { | 920 isSameLineAndSelection: function(otherLine) { |
815 return this.isSameLine(otherLine) && | 921 return this.isSameLine(otherLine) && |
816 this.startOffset == otherLine.startOffset && | 922 this.startOffset == otherLine.startOffset && |
817 this.endOffset == otherLine.endOffset; | 923 this.endOffset == otherLine.endOffset; |
| 924 }, |
| 925 |
| 926 /** |
| 927 * Returns whether this line comes before |otherLine| in document order. |
| 928 * @return {boolean} |
| 929 */ |
| 930 isBeforeLine: function(otherLine) { |
| 931 if (this.isSameLine(otherLine) || !this.lineStartContainer_ || |
| 932 !otherLine.lineStartContainer_) |
| 933 return false; |
| 934 return AutomationUtil.getDirection( |
| 935 this.lineStartContainer_, otherLine.lineStartContainer_) == |
| 936 Dir.FORWARD; |
818 } | 937 } |
819 }; | 938 }; |
820 | 939 |
821 }); | 940 }); |
OLD | NEW |