| 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 209 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 220 * @param {!AutomationNode} node | 220 * @param {!AutomationNode} node |
| 221 * @extends {AutomationEditableText} | 221 * @extends {AutomationEditableText} |
| 222 */ | 222 */ |
| 223 function AutomationRichEditableText(node) { | 223 function AutomationRichEditableText(node) { |
| 224 AutomationEditableText.call(this, node); | 224 AutomationEditableText.call(this, node); |
| 225 | 225 |
| 226 var root = this.node_.root; | 226 var root = this.node_.root; |
| 227 if (!root || !root.anchorObject || !root.focusObject) | 227 if (!root || !root.anchorObject || !root.focusObject) |
| 228 return; | 228 return; |
| 229 | 229 |
| 230 this.range = new cursors.Range( | 230 this.line_ = new editing.EditableLine( |
| 231 new cursors.Cursor(root.anchorObject, root.anchorOffset || 0), | 231 root.anchorObject, root.anchorOffset, root.focusObject, root.focusOffset); |
| 232 new cursors.Cursor(root.focusObject, root.focusOffset || 0)); | |
| 233 } | 232 } |
| 234 | 233 |
| 235 AutomationRichEditableText.prototype = { | 234 AutomationRichEditableText.prototype = { |
| 236 __proto__: AutomationEditableText.prototype, | 235 __proto__: AutomationEditableText.prototype, |
| 237 | 236 |
| 238 /** @override */ | 237 /** @override */ |
| 239 onUpdate: function() { | 238 onUpdate: function() { |
| 240 var root = this.node_.root; | 239 var root = this.node_.root; |
| 241 if (!root.anchorObject || !root.focusObject) | 240 if (!root.anchorObject || !root.focusObject) |
| 242 return; | 241 return; |
| 243 | 242 |
| 244 var cur = new cursors.Range( | 243 var cur = new editing.EditableLine( |
| 245 new cursors.Cursor(root.anchorObject, root.anchorOffset || 0), | 244 root.anchorObject, root.anchorOffset || 0, |
| 246 new cursors.Cursor(root.focusObject, root.focusOffset || 0)); | 245 root.focusObject, root.focusOffset || 0); |
| 247 var prev = this.range; | 246 var prev = this.line_; |
| 247 this.line_ = cur; |
| 248 | 248 |
| 249 this.range = cur; | 249 if (prev.equals(cur)) { |
| 250 | 250 // Collapsed cursor. |
| 251 if (prev.start.node == cur.start.node && | |
| 252 prev.end.node == cur.end.node && | |
| 253 cur.start.node == cur.end.node) { | |
| 254 // Plain text: diff the two positions. | |
| 255 this.changed(new cvox.TextChangeEvent( | 251 this.changed(new cvox.TextChangeEvent( |
| 256 root.anchorObject.name || '', | 252 cur.text || '', |
| 257 root.anchorOffset || 0, | 253 cur.startOffset || 0, |
| 258 root.focusOffset || 0, | 254 cur.endOffset || 0, |
| 259 true)); | 255 true)); |
| 260 | 256 |
| 261 var lineIndex = this.getLineIndex(this.start); | 257 var value = cur.value_; |
| 262 var brailleLineStart = this.getLineStart(lineIndex); | 258 value.setSpan(new cvox.ValueSpan(0), 0, cur.value_.length); |
| 263 var brailleLineEnd = this.getLineEnd(lineIndex); | 259 value.setSpan( |
| 264 var buff = new Spannable(this.value); | 260 new cvox.ValueSelectionSpan(), cur.startOffset, cur.endOffset); |
| 265 buff.setSpan(new cvox.ValueSpan(0), brailleLineStart, brailleLineEnd); | 261 cvox.ChromeVox.braille.write(new cvox.NavBraille({text: value, |
| 262 startIndex: cur.startOffset, |
| 263 endIndex: cur.endOffset})); |
| 266 | 264 |
| 267 var selStart = this.start - brailleLineStart; | 265 // Finally, queue up any text markers/styles at bounds. |
| 268 var selEnd = this.end - brailleLineStart; | 266 var container = cur.lineContainer_; |
| 269 buff.setSpan(new cvox.ValueSelectionSpan(), selStart, selEnd); | 267 if (!container) |
| 270 cvox.ChromeVox.braille.write(new cvox.NavBraille({text: buff, | 268 return; |
| 271 startIndex: selStart, | 269 |
| 272 endIndex: selEnd})); | 270 if (container.markerTypes) { |
| 271 // Only consider markers that start or end at the selection bounds. |
| 272 var markerStartIndex = -1, markerEndIndex = -1; |
| 273 var localStartOffset = cur.localStartOffset; |
| 274 for (var i = 0; i < container.markerStarts.length; i++) { |
| 275 if (container.markerStarts[i] == localStartOffset) { |
| 276 markerStartIndex = i; |
| 277 break; |
| 278 } |
| 279 } |
| 280 |
| 281 var localEndOffset = cur.localEndOffset; |
| 282 for (var i = 0; i < container.markerEnds.length; i++) { |
| 283 if (container.markerEnds[i] == localEndOffset) { |
| 284 markerEndIndex = i; |
| 285 break; |
| 286 } |
| 287 } |
| 288 |
| 289 if (markerStartIndex > -1) |
| 290 this.speakTextMarker_(container.markerTypes[markerStartIndex]); |
| 291 |
| 292 if (markerEndIndex > -1) |
| 293 this.speakTextMarker_(container.markerTypes[markerEndIndex], true); |
| 294 } |
| 295 |
| 296 if (!container.textStyle) |
| 297 return; |
| 298 |
| 299 // Start of the container. |
| 300 if (cur.containerStartOffset == cur.startOffset) |
| 301 this.speakTextStyle_(container.textStyle); |
| 302 else if (cur.containerEndOffset == cur.endOffset) |
| 303 this.speakTextStyle_(container.textStyle, true); |
| 304 |
| 273 return; | 305 return; |
| 274 } else { | |
| 275 // Rich text: | |
| 276 // If the position is collapsed, expand to the current line. | |
| 277 var start = cur.start; | |
| 278 var end = cur.end; | |
| 279 if (start.equals(end)) { | |
| 280 start = start.move(Unit.LINE, Movement.BOUND, Dir.BACKWARD); | |
| 281 end = end.move(Unit.LINE, Movement.BOUND, Dir.FORWARD); | |
| 282 } | |
| 283 var range = new cursors.Range(start, end); | |
| 284 var output = new Output().withRichSpeechAndBraille( | |
| 285 range, prev, Output.EventType.NAVIGATE); | |
| 286 | |
| 287 // This position is not describable. | |
| 288 if (!output.hasSpeech) { | |
| 289 cvox.ChromeVox.tts.speak('blank', cvox.QueueMode.CATEGORY_FLUSH); | |
| 290 cvox.ChromeVox.braille.write( | |
| 291 new cvox.NavBraille({text: '', startIndex: 0, endIndex: 0})); | |
| 292 } else { | |
| 293 output.go(); | |
| 294 } | |
| 295 } | 306 } |
| 296 | 307 |
| 297 // Keep the other members in sync for any future editable text base state | 308 // Just output the current line. |
| 298 // machine changes. | 309 if (!cur.lineStart_ || !cur.lineEnd_) |
| 299 this.value = cur.start.node.name || ''; | 310 return; |
| 300 this.start = cur.start.index; | 311 var prevRange = null; |
| 301 this.end = cur.start.index; | 312 if (prev.lineStart_ && prev.lineEnd_) { |
| 313 prevRange = new Range( |
| 314 Cursor.fromNode(prev.lineStart_), Cursor.fromNode(prev.lineEnd_)); |
| 315 } |
| 316 |
| 317 new Output().withRichSpeechAndBraille(new Range( |
| 318 Cursor.fromNode(cur.lineStart_), Cursor.fromNode(cur.lineEnd_)), |
| 319 prevRange, |
| 320 Output.EventType.NAVIGATE).go(); |
| 321 }, |
| 322 |
| 323 /** |
| 324 * @param {number} markerType |
| 325 * @param {boolean=} opt_end |
| 326 * @private |
| 327 */ |
| 328 speakTextMarker_: function(markerType, opt_end) { |
| 329 // TODO(dtseng): Plumb through constants to automation. |
| 330 var msgs = []; |
| 331 if (markerType & 1) |
| 332 msgs.push(opt_end ? 'misspelling_end' : 'misspelling_start'); |
| 333 if (markerType & 2) |
| 334 msgs.push(opt_end ? 'grammar_end' : 'grammar_start'); |
| 335 if (markerType & 4) |
| 336 msgs.push(opt_end ? 'text_match_end' : 'text_match_start'); |
| 337 |
| 338 if (msgs.length) { |
| 339 msgs.forEach(function(msg) { |
| 340 cvox.ChromeVox.tts.speak(Msgs.getMsg(msg), |
| 341 cvox.QueueMode.QUEUE, |
| 342 cvox.AbstractTts.PERSONALITY_ANNOTATION); |
| 343 }); |
| 344 } |
| 345 }, |
| 346 |
| 347 /** |
| 348 * @param {number} style |
| 349 * @param {boolean=} opt_end |
| 350 * @private |
| 351 */ |
| 352 speakTextStyle_: function(style, opt_end) { |
| 353 var msgs = []; |
| 354 if (style & 1) |
| 355 msgs.push(opt_end ? 'bold_end' : 'bold_start'); |
| 356 if (style & 2) |
| 357 msgs.push(opt_end ? 'italic_end' : 'italic_start'); |
| 358 if (style & 4) |
| 359 msgs.push(opt_end ? 'underline_end' : 'underline_start'); |
| 360 if (style & 8) |
| 361 msgs.push(opt_end ? 'line_through_end' : 'line_through_start'); |
| 362 |
| 363 if (msgs.length) { |
| 364 msgs.forEach(function(msg) { |
| 365 cvox.ChromeVox.tts.speak(Msgs.getMsg(msg), |
| 366 cvox.QueueMode.QUEUE, |
| 367 cvox.AbstractTts.PERSONALITY_ANNOTATION); |
| 368 }); |
| 369 } |
| 302 }, | 370 }, |
| 303 | 371 |
| 304 /** @override */ | 372 /** @override */ |
| 305 describeSelectionChanged: function(evt) { | 373 describeSelectionChanged: function(evt) { |
| 306 // Ignore end of text announcements. | 374 // Ignore end of text announcements. |
| 307 if ((this.start + 1) == evt.start && evt.start == this.value.length) | 375 if ((this.start + 1) == evt.start && evt.start == this.value.length) |
| 308 return; | 376 return; |
| 309 | 377 |
| 310 cvox.ChromeVoxEditableTextBase.prototype.describeSelectionChanged.call( | 378 cvox.ChromeVoxEditableTextBase.prototype.describeSelectionChanged.call( |
| 311 this, evt); | 379 this, evt); |
| 312 }, | 380 }, |
| 313 | 381 |
| 314 /** @override */ | 382 /** @override */ |
| 315 getLineIndex: function(charIndex) { | 383 getLineIndex: function(charIndex) { |
| 316 var breaks = this.getLineBreaks_(); | 384 return 0; |
| 317 var index = 0; | |
| 318 while (index < breaks.length && breaks[index] <= charIndex) | |
| 319 ++index; | |
| 320 return index; | |
| 321 }, | 385 }, |
| 322 | 386 |
| 323 /** @override */ | 387 /** @override */ |
| 324 getLineStart: function(lineIndex) { | 388 getLineStart: function(lineIndex) { |
| 325 if (lineIndex == 0) | 389 return 0; |
| 326 return 0; | |
| 327 var breaks = this.getLineBreaks_(); | |
| 328 return breaks[lineIndex - 1] || | |
| 329 this.node_.root.focusObject.value.length; | |
| 330 }, | 390 }, |
| 331 | 391 |
| 332 /** @override */ | 392 /** @override */ |
| 333 getLineEnd: function(lineIndex) { | 393 getLineEnd: function(lineIndex) { |
| 334 var breaks = this.getLineBreaks_(); | 394 return this.value.length; |
| 335 var value = this.node_.root.focusObject.name; | |
| 336 if (lineIndex >= breaks.length) | |
| 337 return value.length; | |
| 338 return breaks[lineIndex]; | |
| 339 }, | 395 }, |
| 340 | 396 |
| 341 /** @override */ | 397 /** @override */ |
| 342 getLineBreaks_: function() { | 398 getLineBreaks_: function() { |
| 343 return this.node_.root.focusObject.lineStartOffsets || []; | 399 return []; |
| 344 } | 400 } |
| 345 }; | 401 }; |
| 346 | 402 |
| 347 /** | 403 /** |
| 348 * @param {!AutomationNode} node The root editable node, i.e. the root of a | 404 * @param {!AutomationNode} node The root editable node, i.e. the root of a |
| 349 * contenteditable subtree or a text field. | 405 * contenteditable subtree or a text field. |
| 350 * @return {editing.TextEditHandler} | 406 * @return {editing.TextEditHandler} |
| 351 */ | 407 */ |
| 352 editing.TextEditHandler.createForNode = function(node) { | 408 editing.TextEditHandler.createForNode = function(node) { |
| 353 var rootFocusedEditable = null; | 409 var rootFocusedEditable = null; |
| (...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 389 cvox.BrailleBackground.getInstance().getTranslatorManager().refresh( | 445 cvox.BrailleBackground.getInstance().getTranslatorManager().refresh( |
| 390 localStorage['brailleTable']); | 446 localStorage['brailleTable']); |
| 391 } | 447 } |
| 392 }; | 448 }; |
| 393 | 449 |
| 394 /** | 450 /** |
| 395 * @private {ChromeVoxStateObserver} | 451 * @private {ChromeVoxStateObserver} |
| 396 */ | 452 */ |
| 397 editing.observer_ = new editing.EditingChromeVoxStateObserver(); | 453 editing.observer_ = new editing.EditingChromeVoxStateObserver(); |
| 398 | 454 |
| 455 /** |
| 456 * An EditableLine encapsulates all data concerning a line in the automation |
| 457 * tree necessary to provide output. |
| 458 * @constructor |
| 459 */ |
| 460 editing.EditableLine = function(startNode, startIndex, endNode, endIndex) { |
| 461 /** @private {!Cursor} */ |
| 462 this.start_ = new Cursor(startNode, startIndex); |
| 463 this.start_ = this.start_.deepEquivalent || this.start_; |
| 464 |
| 465 /** @private {!Cursor} */ |
| 466 this.end_ = new Cursor(endNode, endIndex); |
| 467 this.end_ = this.end_.deepEquivalent || this.end_; |
| 468 /** @private {number} */ |
| 469 this.localContainerStartOffset_ = startIndex; |
| 470 |
| 471 // Computed members. |
| 472 /** @private {Spannable} */ |
| 473 this.value_; |
| 474 /** @private {AutomationNode} */ |
| 475 this.lineStart_; |
| 476 /** @private {AutomationNode} */ |
| 477 this.lineEnd_; |
| 478 /** @private {AutomationNode|undefined} */ |
| 479 this.lineContainer_; |
| 480 |
| 481 this.computeLineData_(); |
| 482 }; |
| 483 |
| 484 editing.EditableLine.prototype = { |
| 485 /** @private */ |
| 486 computeLineData_: function() { |
| 487 this.value_ = new Spannable(this.start_.node.name, this.start_); |
| 488 if (this.start_.node == this.end_.node) |
| 489 this.value_.setSpan(this.end_, 0, this.start_.node.name.length); |
| 490 |
| 491 this.lineStart_ = this.start_.node; |
| 492 this.lineEnd_ = this.lineStart_; |
| 493 this.lineContainer_ = this.lineStart_.parent; |
| 494 |
| 495 // Annotate each chunk with its associated node. |
| 496 this.value_.setSpan(this.lineStart_, 0, this.lineStart_.name.length); |
| 497 |
| 498 while (this.lineStart_.previousOnLine) { |
| 499 this.lineStart_ = this.lineStart_.previousOnLine; |
| 500 var prepend = new Spannable(this.lineStart_.name, this.lineStart_); |
| 501 prepend.append(this.value_); |
| 502 this.value_ = prepend; |
| 503 } |
| 504 |
| 505 while (this.lineEnd_.nextOnLine) { |
| 506 this.lineEnd_ = this.lineEnd_.nextOnLine; |
| 507 |
| 508 var annotation = this.lineEnd_; |
| 509 if (this.lineEnd_ == this.end_.node) |
| 510 annotation = this.end_; |
| 511 |
| 512 this.value_.append(new Spannable(this.lineEnd_.name, annotation)); |
| 513 } |
| 514 }, |
| 515 |
| 516 /** |
| 517 * Gets the selection offset based on the text content of this line. |
| 518 * @return {number} |
| 519 */ |
| 520 get startOffset() { |
| 521 return this.value_.getSpanStart(this.start_) + this.start_.index; |
| 522 }, |
| 523 |
| 524 /** |
| 525 * Gets the selection offset based on the text content of this line. |
| 526 * @return {number} |
| 527 */ |
| 528 get endOffset() { |
| 529 return this.value_.getSpanStart(this.end_) + this.end_.index; |
| 530 }, |
| 531 |
| 532 /** |
| 533 * Gets the selection offset based on the parent's text. |
| 534 * @return {number} |
| 535 */ |
| 536 get localStartOffset() { |
| 537 return this.startOffset - this.containerStartOffset; |
| 538 }, |
| 539 |
| 540 /** |
| 541 * Gets the selection offset based on the parent's text. |
| 542 * @return {number} |
| 543 */ |
| 544 get localEndOffset() { |
| 545 return this.endOffset - this.containerStartOffset; |
| 546 }, |
| 547 |
| 548 /** |
| 549 * Gets the start offset of the line, relative to the container's text. |
| 550 * @return {number} |
| 551 */ |
| 552 get containerLineStartOffset() { |
| 553 // When the container start offset is larger, the start offset is usually |
| 554 // part of a line wrap, so the two offsets differ. |
| 555 // When the container start offset is smaller, there is usually more line |
| 556 // content before the container accounted for in start offset. |
| 557 // Taking the difference either way will give us the offset at which the |
| 558 // line begins. |
| 559 return Math.abs(this.localContainerStartOffset_ - this.startOffset); |
| 560 }, |
| 561 |
| 562 /** |
| 563 * Gets the start offset of the container, relative to the line text content. |
| 564 * @return {number} |
| 565 */ |
| 566 get containerStartOffset() { |
| 567 return this.value_.getSpanStart(this.lineContainer_.firstChild); |
| 568 }, |
| 569 |
| 570 /** |
| 571 * Gets the end offset of the container, relative to the line text content. |
| 572 * @return {number} |
| 573 */ |
| 574 get containerEndOffset() { |
| 575 return this.value_.getSpanEnd(this.lineContainer_.lastChild) - 1; |
| 576 }, |
| 577 |
| 578 /** |
| 579 * @return {string} The text of this line. |
| 580 */ |
| 581 get text() { |
| 582 return this.value_.toString(); |
| 583 }, |
| 584 |
| 585 /** |
| 586 * Returns true if |otherLine| surrounds the same line as |this|. Note that |
| 587 * the contents of the line might be different. |
| 588 * @return {boolean} |
| 589 */ |
| 590 equals: function(otherLine) { |
| 591 // Equality is intentionally loose here as any of the state nodes can be |
| 592 // invalidated at any time. |
| 593 return (otherLine.lineStart_ == this.lineStart_ && |
| 594 otherLine.lineEnd_ == this.lineEnd_) || |
| 595 (otherLine.lineContainer_ == this.lineContainer_ && |
| 596 otherLine.containerLineStartOffset == this.containerLineStartOffset); |
| 597 } |
| 598 }; |
| 599 |
| 399 }); | 600 }); |
| OLD | NEW |