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'); |
11 | 11 |
12 goog.require('AutomationTreeWalker'); | 12 goog.require('AutomationTreeWalker'); |
13 goog.require('AutomationUtil'); | 13 goog.require('AutomationUtil'); |
14 goog.require('Output'); | 14 goog.require('Output'); |
15 goog.require('Output.EventType'); | 15 goog.require('Output.EventType'); |
16 goog.require('cursors.Cursor'); | 16 goog.require('cursors.Cursor'); |
17 goog.require('cursors.Range'); | 17 goog.require('cursors.Range'); |
18 goog.require('cvox.BrailleBackground'); | 18 goog.require('cvox.BrailleBackground'); |
19 goog.require('cvox.ChromeVoxEditableTextBase'); | 19 goog.require('cvox.ChromeVoxEditableTextBase'); |
20 | 20 |
21 goog.scope(function() { | 21 goog.scope(function() { |
22 var AutomationEvent = chrome.automation.AutomationEvent; | 22 var AutomationEvent = chrome.automation.AutomationEvent; |
23 var AutomationNode = chrome.automation.AutomationNode; | 23 var AutomationNode = chrome.automation.AutomationNode; |
24 var Cursor = cursors.Cursor; | 24 var Cursor = cursors.Cursor; |
25 var Dir = AutomationUtil.Dir; | 25 var Dir = constants.Dir; |
26 var EventType = chrome.automation.EventType; | 26 var EventType = chrome.automation.EventType; |
27 var Range = cursors.Range; | 27 var Range = cursors.Range; |
28 var RoleType = chrome.automation.RoleType; | 28 var RoleType = chrome.automation.RoleType; |
29 var StateType = chrome.automation.StateType; | 29 var StateType = chrome.automation.StateType; |
30 var Movement = cursors.Movement; | 30 var Movement = cursors.Movement; |
31 var Unit = cursors.Unit; | 31 var Unit = cursors.Unit; |
32 | 32 |
33 /** | 33 /** |
34 * A handler for automation events in a focused text field or editable root | 34 * A handler for automation events in a focused text field or editable root |
35 * such as a |contenteditable| subtree. | 35 * such as a |contenteditable| subtree. |
(...skipping 23 matching lines...) Expand all Loading... | |
59 }; | 59 }; |
60 | 60 |
61 /** | 61 /** |
62 * A |TextEditHandler| suitable for text fields. | 62 * A |TextEditHandler| suitable for text fields. |
63 * @constructor | 63 * @constructor |
64 * @param {!AutomationNode} node A node with the role of |textField| | 64 * @param {!AutomationNode} node A node with the role of |textField| |
65 * @extends {editing.TextEditHandler} | 65 * @extends {editing.TextEditHandler} |
66 */ | 66 */ |
67 function TextFieldTextEditHandler(node) { | 67 function TextFieldTextEditHandler(node) { |
68 editing.TextEditHandler.call(this, node); | 68 editing.TextEditHandler.call(this, node); |
69 /** @type {AutomationEditableText} @private */ | 69 |
70 this.editableText_ = new AutomationEditableText(node); | 70 chrome.automation.getDesktop(function(desktop) { |
71 // Chrome channel is empty when building from source. | |
72 var useExperimentalRichText = desktop.chromeChannel == '' && | |
73 node.state.richlyEditable; | |
74 | |
75 /** @private {!AutomationEditableText} */ | |
76 this.editableText_ = useExperimentalRichText ? | |
77 new AutomationRichEditableText(node) : new AutomationEditableText(node); | |
78 }.bind(this)); | |
71 } | 79 } |
72 | 80 |
73 TextFieldTextEditHandler.prototype = { | 81 TextFieldTextEditHandler.prototype = { |
74 __proto__: editing.TextEditHandler.prototype, | 82 __proto__: editing.TextEditHandler.prototype, |
75 | 83 |
76 /** @override */ | 84 /** @override */ |
77 onEvent: function(evt) { | 85 onEvent: function(evt) { |
78 if (evt.type !== EventType.TEXT_CHANGED && | 86 if (evt.type !== EventType.TEXT_CHANGED && |
79 evt.type !== EventType.TEXT_SELECTION_CHANGED && | 87 evt.type !== EventType.TEXT_SELECTION_CHANGED && |
80 evt.type !== EventType.VALUE_CHANGED && | 88 evt.type !== EventType.VALUE_CHANGED && |
(...skipping 111 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
192 } else { | 200 } else { |
193 range = Range.fromNode(this.node_); | 201 range = Range.fromNode(this.node_); |
194 } | 202 } |
195 output.withBraille(range, null, Output.EventType.NAVIGATE); | 203 output.withBraille(range, null, Output.EventType.NAVIGATE); |
196 if (isFirstLine) | 204 if (isFirstLine) |
197 output.formatForBraille('@tag_textarea'); | 205 output.formatForBraille('@tag_textarea'); |
198 output.go(); | 206 output.go(); |
199 } | 207 } |
200 }; | 208 }; |
201 | 209 |
210 | |
211 /** | |
212 * A |ChromeVoxEditableTextBase| that implements text editing feedback | |
213 * for automation tree text fields using anchor and focus selection. | |
214 * @constructor | |
215 * @param {!AutomationNode} node | |
216 * @extends {AutomationEditableText} | |
217 */ | |
218 function AutomationRichEditableText(node) { | |
219 AutomationEditableText.call(this, node); | |
220 | |
221 var root = this.node_.root; | |
222 if (!root || !root.anchorObject || !root.focusObject) | |
223 return; | |
224 | |
225 this.range = new cursors.Range( | |
226 new cursors.Cursor(root.anchorObject, root.anchorOffset || 0), | |
227 new cursors.Cursor(root.focusObject, root.focusOffset || 0)); | |
228 } | |
229 | |
230 AutomationRichEditableText.prototype = { | |
231 __proto__: AutomationEditableText.prototype, | |
232 | |
233 /** @override */ | |
234 onUpdate: function() { | |
235 var root = this.node_.root; | |
236 if (!root.anchorObject || !root.focusObject) | |
237 return; | |
238 | |
239 var cur = new cursors.Range( | |
240 new cursors.Cursor(root.anchorObject, root.anchorOffset || 0), | |
241 new cursors.Cursor(root.focusObject, root.focusOffset || 0)); | |
242 var prev = this.range; | |
243 | |
244 if (prev.equals(cur)) | |
dmazzoni
2017/05/09 04:49:47
What if the range stays the same but the contents
David Tseng
2017/05/09 23:13:50
Done.
| |
245 return; | |
246 | |
247 this.range = cur; | |
248 | |
249 if (prev.start.node == cur.start.node && | |
250 prev.end.node == cur.end.node && | |
251 cur.start.node == cur.end.node) { | |
252 // Plain text: diff the two positions. | |
253 this.changed(new cvox.TextChangeEvent( | |
254 root.anchorObject.name || '', | |
255 root.anchorOffset || 0, | |
256 root.focusOffset || 0, | |
257 true)); | |
258 | |
259 var lineIndex = this.getLineIndex(this.start); | |
260 var brailleLineStart = this.getLineStart(lineIndex); | |
261 var buff = new Spannable(this.value); | |
262 buff.setSpan(new cvox.ValueSpan(0), 0, this.value.length); | |
263 | |
264 var selStart = this.start - brailleLineStart; | |
265 var selEnd = this.end - brailleLineStart; | |
266 buff.setSpan(new cvox.ValueSelectionSpan(), selStart, selEnd); | |
267 cvox.ChromeVox.braille.write(new cvox.NavBraille({text: buff, | |
268 startIndex: selStart, | |
269 endIndex: selEnd})); | |
270 return; | |
271 } else if (cur.start.equals(prev.start)) { | |
dmazzoni
2017/05/09 04:49:47
Do you need a braille output rule for this case?
David Tseng
2017/05/09 23:13:50
Removed.
| |
272 // Selection. | |
273 var dir = prev.end.compare(cur.end); | |
274 var msg = dir == Dir.FORWARD ? '@selected' : '@unselected'; | |
275 new Output().withSpeech( | |
276 new cursors.Range(cur.end, prev.end), prev, Output.EventType.NAVIGATE) | |
277 .formatForSpeech(msg) | |
278 .go(); | |
279 } else if (cur.end.equals(prev.end)) { | |
dmazzoni
2017/05/09 04:49:46
Same, this does this case need braille?
David Tseng
2017/05/09 23:13:50
Removed.
| |
280 // Selection. | |
281 var dir = prev.start.compare(cur.start); | |
282 var msg = dir == Dir.FORWARD ? '@unselected' : '@selected'; | |
283 new Output().withSpeech(new cursors.Range( | |
284 cur.start, prev.start), prev, Output.EventType.NAVIGATE) | |
285 .formatForSpeech(msg) | |
286 .go(); | |
287 } else { | |
288 // Rich text: | |
289 // If the position is collapsed, expand to the current line. | |
290 var start = cur.start; | |
291 var end = cur.end; | |
292 if (start.equals(end)) { | |
293 start = start.move(Unit.LINE, Movement.BOUND, Dir.BACKWARD); | |
294 end = end.move(Unit.LINE, Movement.BOUND, Dir.FORWARD); | |
295 } | |
296 var range = new cursors.Range(start, end); | |
297 var output = new Output().withRichSpeechAndBraille( | |
298 range, prev, Output.EventType.NAVIGATE); | |
299 | |
300 // This position is not describable. | |
301 if (!output.hasSpeech) { | |
302 cvox.ChromeVox.tts.speak('blank', cvox.QueueMode.CATEGORY_FLUSH); | |
303 cvox.ChromeVox.braille.write( | |
304 new cvox.NavBraille({text: '', startIndex: 0, endIndex: 0})); | |
305 } else { | |
306 output.go(); | |
307 } | |
308 } | |
309 | |
310 // Keep the other members in sync for any future editable text base state | |
311 // machine changes. | |
312 this.value = cur.start.node.name || ''; | |
313 this.start = cur.start.index; | |
314 this.end = cur.start.index; | |
315 }, | |
316 | |
317 /** @override */ | |
318 describeSelectionChanged: function(evt) { | |
319 // Ignore end of text announcements. | |
320 if ((this.start + 1) == evt.start && evt.start == this.value.length) | |
321 return; | |
322 | |
323 cvox.ChromeVoxEditableTextBase.prototype.describeSelectionChanged.call( | |
324 this, evt); | |
325 }, | |
326 | |
327 /** @override */ | |
328 getLineIndex: function(charIndex) { | |
329 var breaks = this.getLineBreaks_(); | |
330 var index = 0; | |
331 while (index < breaks.length && breaks[index] <= charIndex) | |
332 ++index; | |
333 return index; | |
334 }, | |
335 | |
336 /** @override */ | |
337 getLineStart: function(lineIndex) { | |
338 if (lineIndex == 0) | |
339 return 0; | |
340 var breaks = this.getLineBreaks_(); | |
341 return breaks[lineIndex - 1] || | |
342 this.node_.root.focusObject.value.length; | |
343 }, | |
344 | |
345 /** @override */ | |
346 getLineEnd: function(lineIndex) { | |
347 var breaks = this.getLineBreaks_(); | |
348 var value = this.node_.root.focusObject.name; | |
349 if (lineIndex >= breaks.length) | |
350 return value.length; | |
351 return breaks[lineIndex]; | |
352 }, | |
353 | |
354 /** @override */ | |
355 getLineBreaks_: function() { | |
356 return this.node_.root.focusObject.lineStartOffsets || []; | |
357 } | |
358 }; | |
359 | |
202 /** | 360 /** |
203 * @param {!AutomationNode} node The root editable node, i.e. the root of a | 361 * @param {!AutomationNode} node The root editable node, i.e. the root of a |
204 * contenteditable subtree or a text field. | 362 * contenteditable subtree or a text field. |
205 * @return {editing.TextEditHandler} | 363 * @return {editing.TextEditHandler} |
206 */ | 364 */ |
207 editing.TextEditHandler.createForNode = function(node) { | 365 editing.TextEditHandler.createForNode = function(node) { |
208 var rootFocusedEditable = null; | 366 var rootFocusedEditable = null; |
209 var testNode = node; | 367 var testNode = node; |
210 | 368 |
211 do { | 369 do { |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
245 localStorage['brailleTable']); | 403 localStorage['brailleTable']); |
246 } | 404 } |
247 }; | 405 }; |
248 | 406 |
249 /** | 407 /** |
250 * @private {ChromeVoxStateObserver} | 408 * @private {ChromeVoxStateObserver} |
251 */ | 409 */ |
252 editing.observer_ = new editing.EditingChromeVoxStateObserver(); | 410 editing.observer_ = new editing.EditingChromeVoxStateObserver(); |
253 | 411 |
254 }); | 412 }); |
OLD | NEW |