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