| 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 // Start of the container. |
| 297 if (cur.containerStartOffset == cur.startOffset) |
| 298 this.speakTextStyle_(container); |
| 299 else if (cur.containerEndOffset == cur.endOffset) |
| 300 this.speakTextStyle_(container, true); |
| 301 |
| 273 return; | 302 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 } | 303 } |
| 296 | 304 |
| 297 // Keep the other members in sync for any future editable text base state | 305 // Just output the current line. |
| 298 // machine changes. | 306 if (!cur.lineStart_ || !cur.lineEnd_) |
| 299 this.value = cur.start.node.name || ''; | 307 return; |
| 300 this.start = cur.start.index; | 308 var prevRange = null; |
| 301 this.end = cur.start.index; | 309 if (prev.lineStart_ && prev.lineEnd_) { |
| 310 prevRange = new Range( |
| 311 Cursor.fromNode(prev.lineStart_), Cursor.fromNode(prev.lineEnd_)); |
| 312 } |
| 313 |
| 314 new Output().withRichSpeechAndBraille(new Range( |
| 315 Cursor.fromNode(cur.lineStart_), Cursor.fromNode(cur.lineEnd_)), |
| 316 prevRange, |
| 317 Output.EventType.NAVIGATE).go(); |
| 318 }, |
| 319 |
| 320 /** |
| 321 * @param {number} markerType |
| 322 * @param {boolean=} opt_end |
| 323 * @private |
| 324 */ |
| 325 speakTextMarker_: function(markerType, opt_end) { |
| 326 // TODO(dtseng): Plumb through constants to automation. |
| 327 var msgs = []; |
| 328 if (markerType & 1) |
| 329 msgs.push(opt_end ? 'misspelling_end' : 'misspelling_start'); |
| 330 if (markerType & 2) |
| 331 msgs.push(opt_end ? 'grammar_end' : 'grammar_start'); |
| 332 if (markerType & 4) |
| 333 msgs.push(opt_end ? 'text_match_end' : 'text_match_start'); |
| 334 |
| 335 if (msgs.length) { |
| 336 msgs.forEach(function(msg) { |
| 337 cvox.ChromeVox.tts.speak(Msgs.getMsg(msg), |
| 338 cvox.QueueMode.QUEUE, |
| 339 cvox.AbstractTts.PERSONALITY_ANNOTATION); |
| 340 }); |
| 341 } |
| 342 }, |
| 343 |
| 344 /** |
| 345 * @param {!AutomationNode} style |
| 346 * @param {boolean=} opt_end |
| 347 * @private |
| 348 */ |
| 349 speakTextStyle_: function(style, opt_end) { |
| 350 var msgs = []; |
| 351 if (style.bold) |
| 352 msgs.push(opt_end ? 'bold_end' : 'bold_start'); |
| 353 if (style.italic) |
| 354 msgs.push(opt_end ? 'italic_end' : 'italic_start'); |
| 355 if (style.underline) |
| 356 msgs.push(opt_end ? 'underline_end' : 'underline_start'); |
| 357 if (style.lineThrough) |
| 358 msgs.push(opt_end ? 'line_through_end' : 'line_through_start'); |
| 359 |
| 360 if (msgs.length) { |
| 361 msgs.forEach(function(msg) { |
| 362 cvox.ChromeVox.tts.speak(Msgs.getMsg(msg), |
| 363 cvox.QueueMode.QUEUE, |
| 364 cvox.AbstractTts.PERSONALITY_ANNOTATION); |
| 365 }); |
| 366 } |
| 302 }, | 367 }, |
| 303 | 368 |
| 304 /** @override */ | 369 /** @override */ |
| 305 describeSelectionChanged: function(evt) { | 370 describeSelectionChanged: function(evt) { |
| 306 // Ignore end of text announcements. | 371 // Ignore end of text announcements. |
| 307 if ((this.start + 1) == evt.start && evt.start == this.value.length) | 372 if ((this.start + 1) == evt.start && evt.start == this.value.length) |
| 308 return; | 373 return; |
| 309 | 374 |
| 310 cvox.ChromeVoxEditableTextBase.prototype.describeSelectionChanged.call( | 375 cvox.ChromeVoxEditableTextBase.prototype.describeSelectionChanged.call( |
| 311 this, evt); | 376 this, evt); |
| 312 }, | 377 }, |
| 313 | 378 |
| 314 /** @override */ | 379 /** @override */ |
| 315 getLineIndex: function(charIndex) { | 380 getLineIndex: function(charIndex) { |
| 316 var breaks = this.getLineBreaks_(); | 381 return 0; |
| 317 var index = 0; | |
| 318 while (index < breaks.length && breaks[index] <= charIndex) | |
| 319 ++index; | |
| 320 return index; | |
| 321 }, | 382 }, |
| 322 | 383 |
| 323 /** @override */ | 384 /** @override */ |
| 324 getLineStart: function(lineIndex) { | 385 getLineStart: function(lineIndex) { |
| 325 if (lineIndex == 0) | 386 return 0; |
| 326 return 0; | |
| 327 var breaks = this.getLineBreaks_(); | |
| 328 return breaks[lineIndex - 1] || | |
| 329 this.node_.root.focusObject.value.length; | |
| 330 }, | 387 }, |
| 331 | 388 |
| 332 /** @override */ | 389 /** @override */ |
| 333 getLineEnd: function(lineIndex) { | 390 getLineEnd: function(lineIndex) { |
| 334 var breaks = this.getLineBreaks_(); | 391 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 }, | 392 }, |
| 340 | 393 |
| 341 /** @override */ | 394 /** @override */ |
| 342 getLineBreaks_: function() { | 395 getLineBreaks_: function() { |
| 343 return this.node_.root.focusObject.lineStartOffsets || []; | 396 return []; |
| 344 } | 397 } |
| 345 }; | 398 }; |
| 346 | 399 |
| 347 /** | 400 /** |
| 348 * @param {!AutomationNode} node The root editable node, i.e. the root of a | 401 * @param {!AutomationNode} node The root editable node, i.e. the root of a |
| 349 * contenteditable subtree or a text field. | 402 * contenteditable subtree or a text field. |
| 350 * @return {editing.TextEditHandler} | 403 * @return {editing.TextEditHandler} |
| 351 */ | 404 */ |
| 352 editing.TextEditHandler.createForNode = function(node) { | 405 editing.TextEditHandler.createForNode = function(node) { |
| 353 var rootFocusedEditable = null; | 406 var rootFocusedEditable = null; |
| (...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 389 cvox.BrailleBackground.getInstance().getTranslatorManager().refresh( | 442 cvox.BrailleBackground.getInstance().getTranslatorManager().refresh( |
| 390 localStorage['brailleTable']); | 443 localStorage['brailleTable']); |
| 391 } | 444 } |
| 392 }; | 445 }; |
| 393 | 446 |
| 394 /** | 447 /** |
| 395 * @private {ChromeVoxStateObserver} | 448 * @private {ChromeVoxStateObserver} |
| 396 */ | 449 */ |
| 397 editing.observer_ = new editing.EditingChromeVoxStateObserver(); | 450 editing.observer_ = new editing.EditingChromeVoxStateObserver(); |
| 398 | 451 |
| 452 /** |
| 453 * An EditableLine encapsulates all data concerning a line in the automation |
| 454 * tree necessary to provide output. |
| 455 * @constructor |
| 456 */ |
| 457 editing.EditableLine = function(startNode, startIndex, endNode, endIndex) { |
| 458 /** @private {!Cursor} */ |
| 459 this.start_ = new Cursor(startNode, startIndex); |
| 460 this.start_ = this.start_.deepEquivalent || this.start_; |
| 461 |
| 462 /** @private {!Cursor} */ |
| 463 this.end_ = new Cursor(endNode, endIndex); |
| 464 this.end_ = this.end_.deepEquivalent || this.end_; |
| 465 /** @private {number} */ |
| 466 this.localContainerStartOffset_ = startIndex; |
| 467 |
| 468 // Computed members. |
| 469 /** @private {Spannable} */ |
| 470 this.value_; |
| 471 /** @private {AutomationNode} */ |
| 472 this.lineStart_; |
| 473 /** @private {AutomationNode} */ |
| 474 this.lineEnd_; |
| 475 /** @private {AutomationNode|undefined} */ |
| 476 this.lineContainer_; |
| 477 |
| 478 this.computeLineData_(); |
| 479 }; |
| 480 |
| 481 editing.EditableLine.prototype = { |
| 482 /** @private */ |
| 483 computeLineData_: function() { |
| 484 this.value_ = new Spannable(this.start_.node.name, this.start_); |
| 485 if (this.start_.node == this.end_.node) |
| 486 this.value_.setSpan(this.end_, 0, this.start_.node.name.length); |
| 487 |
| 488 this.lineStart_ = this.start_.node; |
| 489 this.lineEnd_ = this.lineStart_; |
| 490 this.lineContainer_ = this.lineStart_.parent; |
| 491 |
| 492 // Annotate each chunk with its associated node. |
| 493 this.value_.setSpan(this.lineStart_, 0, this.lineStart_.name.length); |
| 494 |
| 495 while (this.lineStart_.previousOnLine) { |
| 496 this.lineStart_ = this.lineStart_.previousOnLine; |
| 497 var prepend = new Spannable(this.lineStart_.name, this.lineStart_); |
| 498 prepend.append(this.value_); |
| 499 this.value_ = prepend; |
| 500 } |
| 501 |
| 502 while (this.lineEnd_.nextOnLine) { |
| 503 this.lineEnd_ = this.lineEnd_.nextOnLine; |
| 504 |
| 505 var annotation = this.lineEnd_; |
| 506 if (this.lineEnd_ == this.end_.node) |
| 507 annotation = this.end_; |
| 508 |
| 509 this.value_.append(new Spannable(this.lineEnd_.name, annotation)); |
| 510 } |
| 511 }, |
| 512 |
| 513 /** |
| 514 * Gets the selection offset based on the text content of this line. |
| 515 * @return {number} |
| 516 */ |
| 517 get startOffset() { |
| 518 return this.value_.getSpanStart(this.start_) + this.start_.index; |
| 519 }, |
| 520 |
| 521 /** |
| 522 * Gets the selection offset based on the text content of this line. |
| 523 * @return {number} |
| 524 */ |
| 525 get endOffset() { |
| 526 return this.value_.getSpanStart(this.end_) + this.end_.index; |
| 527 }, |
| 528 |
| 529 /** |
| 530 * Gets the selection offset based on the parent's text. |
| 531 * @return {number} |
| 532 */ |
| 533 get localStartOffset() { |
| 534 return this.startOffset - this.containerStartOffset; |
| 535 }, |
| 536 |
| 537 /** |
| 538 * Gets the selection offset based on the parent's text. |
| 539 * @return {number} |
| 540 */ |
| 541 get localEndOffset() { |
| 542 return this.endOffset - this.containerStartOffset; |
| 543 }, |
| 544 |
| 545 /** |
| 546 * Gets the start offset of the line, relative to the container's text. |
| 547 * @return {number} |
| 548 */ |
| 549 get containerLineStartOffset() { |
| 550 // When the container start offset is larger, the start offset is usually |
| 551 // part of a line wrap, so the two offsets differ. |
| 552 // When the container start offset is smaller, there is usually more line |
| 553 // content before the container accounted for in start offset. |
| 554 // Taking the difference either way will give us the offset at which the |
| 555 // line begins. |
| 556 return Math.abs(this.localContainerStartOffset_ - this.startOffset); |
| 557 }, |
| 558 |
| 559 /** |
| 560 * Gets the start offset of the container, relative to the line text content. |
| 561 * @return {number} |
| 562 */ |
| 563 get containerStartOffset() { |
| 564 return this.value_.getSpanStart(this.lineContainer_.firstChild); |
| 565 }, |
| 566 |
| 567 /** |
| 568 * Gets the end offset of the container, relative to the line text content. |
| 569 * @return {number} |
| 570 */ |
| 571 get containerEndOffset() { |
| 572 return this.value_.getSpanEnd(this.lineContainer_.lastChild) - 1; |
| 573 }, |
| 574 |
| 575 /** |
| 576 * @return {string} The text of this line. |
| 577 */ |
| 578 get text() { |
| 579 return this.value_.toString(); |
| 580 }, |
| 581 |
| 582 /** |
| 583 * Returns true if |otherLine| surrounds the same line as |this|. Note that |
| 584 * the contents of the line might be different. |
| 585 * @return {boolean} |
| 586 */ |
| 587 equals: function(otherLine) { |
| 588 // Equality is intentionally loose here as any of the state nodes can be |
| 589 // invalidated at any time. |
| 590 return (otherLine.lineStart_ == this.lineStart_ && |
| 591 otherLine.lineEnd_ == this.lineEnd_) || |
| 592 (otherLine.lineContainer_ == this.lineContainer_ && |
| 593 otherLine.containerLineStartOffset == this.containerLineStartOffset); |
| 594 } |
| 595 }; |
| 596 |
| 399 }); | 597 }); |
| OLD | NEW |