1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 * @fileoverview The entry point for all ChromeVox2 related code for the
10 goog.provide('Background');
11 goog.provide('global');
13 goog.require('AutomationPredicate');
14 goog.require('AutomationUtil');
15 goog.require('ClassicCompatibility');
16 goog.require('Output');
17 goog.require('Output.EventType');
18 goog.require('cursors.Cursor');
19 goog.require('cvox.BrailleKeyCommand');
20 goog.require('cvox.ChromeVoxEditableTextBase');
21 goog.require('cvox.ExtensionBridge');
23 goog.scope(function() {
24 var AutomationNode = chrome.automation.AutomationNode;
25 var Dir = AutomationUtil.Dir;
26 var EventType = chrome.automation.EventType;
29 * All possible modes ChromeVox can run.
36 FORCE_NEXT: 'force_next'
40 * ChromeVox2 background page.
43 Background = function() {
45 * A list of site substring patterns to use with ChromeVox next. Keep these
46 * strings relatively specific.
47 * @type {!Array<string>}
50 this.whitelist_ = ['chromevox_next_test'];
53 * @type {cursors.Range}
56 this.currentRange_ = null;
59 * Which variant of ChromeVox is active.
60 * @type {ChromeVoxMode}
63 this.mode_ = ChromeVoxMode.CLASSIC;
65 /** @type {!ClassicCompatibility} @private */
66 this.compat_ = new ClassicCompatibility();
68 // Manually bind all functions to |this|.
69 for (var func in this) {
70 if (typeof(this[func]) == 'function')
71 this[func] = this[func].bind(this);
75 * Maps an automation event to its listener.
76 * @type {!Object<EventType, function(Object) : void>}
79 alert: this.onEventDefault,
81 hover: this.onEventDefault,
82 loadComplete: this.onLoadComplete,
83 menuStart: this.onEventDefault,
84 menuEnd: this.onEventDefault,
85 menuListValueChanged: this.onEventDefault,
86 textChanged: this.onTextOrTextSelectionChanged,
87 textSelectionChanged: this.onTextOrTextSelectionChanged,
88 valueChanged: this.onValueChanged
91 chrome.automation.getDesktop(this.onGotDesktop);
94 Background.prototype = {
95 /** Forces ChromeVox Next to be active for all tabs. */
96 forceChromeVoxNextActive: function() {
97 this.setChromeVoxMode(ChromeVoxMode.FORCE_NEXT);
101 * Handles all setup once a new automation tree appears.
102 * @param {chrome.automation.AutomationNode} desktop
104 onGotDesktop: function(desktop) {
105 // Register all automation event listeners.
106 for (var eventType in this.listeners_)
107 desktop.addEventListener(eventType, this.listeners_[eventType], true);
109 // Register a tree change observer.
110 chrome.automation.addTreeChangeObserver(this.onTreeChange);
112 // The focused state gets set on the containing webView node.
113 var webView = desktop.find({role: chrome.automation.RoleType.webView,
114 state: {focused: true}});
116 var root = webView.find({role: chrome.automation.RoleType.rootWebArea});
120 type: chrome.automation.EventType.loadComplete});
126 * Handles chrome.commands.onCommand.
127 * @param {string} command
128 * @param {boolean=} opt_skipCompat Whether to skip compatibility checks.
130 onGotCommand: function(command, opt_skipCompat) {
131 if (!this.currentRange_)
134 if (!opt_skipCompat && this.mode_ === ChromeVoxMode.COMPAT) {
135 if (this.compat_.onGotCommand(command))
139 var current = this.currentRange_;
140 var dir = Dir.FORWARD;
142 var predErrorMsg = undefined;
144 case 'nextCharacter':
145 current = current.move(cursors.Unit.CHARACTER, Dir.FORWARD);
147 case 'previousCharacter':
148 current = current.move(cursors.Unit.CHARACTER, Dir.BACKWARD);
151 current = current.move(cursors.Unit.WORD, Dir.FORWARD);
154 current = current.move(cursors.Unit.WORD, Dir.BACKWARD);
157 current = current.move(cursors.Unit.LINE, Dir.FORWARD);
160 current = current.move(cursors.Unit.LINE, Dir.BACKWARD);
164 pred = AutomationPredicate.button;
165 predErrorMsg = 'no_next_button';
167 case 'previousButton':
169 pred = AutomationPredicate.button;
170 predErrorMsg = 'no_previous_button';
174 pred = AutomationPredicate.checkBox;
175 predErrorMsg = 'no_next_checkbox';
177 case 'previousCheckBox':
179 pred = AutomationPredicate.checkBox;
180 predErrorMsg = 'no_previous_checkbox';
184 pred = AutomationPredicate.comboBox;
185 predErrorMsg = 'no_next_combo_box';
187 case 'previousComboBox':
189 pred = AutomationPredicate.comboBox;
190 predErrorMsg = 'no_previous_combo_box';
194 pred = AutomationPredicate.editText;
195 predErrorMsg = 'no_next_edit_text';
197 case 'previousEditText':
199 pred = AutomationPredicate.editText;
200 predErrorMsg = 'no_previous_edit_text';
202 case 'nextFormField':
204 pred = AutomationPredicate.formField;
205 predErrorMsg = 'no_next_form_field';
207 case 'previousFormField':
209 pred = AutomationPredicate.formField;
210 predErrorMsg = 'no_previous_form_field';
214 pred = AutomationPredicate.heading;
215 predErrorMsg = 'no_next_heading';
217 case 'previousHeading':
219 pred = AutomationPredicate.heading;
220 predErrorMsg = 'no_previous_heading';
224 pred = AutomationPredicate.link;
225 predErrorMsg = 'no_next_link';
229 pred = AutomationPredicate.link;
230 predErrorMsg = 'no_previous_link';
234 pred = AutomationPredicate.table;
235 predErrorMsg = 'no_next_table';
237 case 'previousTable':
239 pred = AutomationPredicate.table;
240 predErrorMsg = 'no_previous_table';
242 case 'nextVisitedLink':
244 pred = AutomationPredicate.visitedLink;
245 predErrorMsg = 'no_next_visited_link';
247 case 'previousVisitedLink':
249 pred = AutomationPredicate.visitedLink;
250 predErrorMsg = 'no_previous_visited_link';
253 current = current.move(cursors.Unit.NODE, Dir.FORWARD);
255 case 'previousElement':
256 current = current.move(cursors.Unit.NODE, Dir.BACKWARD);
258 case 'goToBeginning':
260 AutomationUtil.findNodePost(current.start.node.root,
262 AutomationPredicate.leaf);
264 current = cursors.Range.fromNode(node);
268 AutomationUtil.findNodePost(current.start.node.root,
270 AutomationPredicate.leaf);
272 current = cursors.Range.fromNode(node);
275 if (this.currentRange_) {
276 var actionNode = this.currentRange_.start.node;
277 if (actionNode.role == chrome.automation.RoleType.inlineTextBox)
278 actionNode = actionNode.parent;
279 actionNode.doDefault();
281 // Skip all other processing; if focus changes, we should get an event
284 case 'continuousRead':
285 global.isReadingContinuously = true;
286 var continueReading = function(prevRange) {
287 if (!global.isReadingContinuously || !this.currentRange_)
290 new Output().withSpeechAndBraille(
291 this.currentRange_, prevRange, Output.EventType.NAVIGATE)
292 .onSpeechEnd(function() { continueReading(prevRange); })
294 prevRange = this.currentRange_;
296 this.currentRange_.move(cursors.Unit.NODE, Dir.FORWARD);
298 if (!this.currentRange_ || this.currentRange_.equals(prevRange))
299 global.isReadingContinuously = false;
302 continueReading(null);
304 case 'showContextMenu':
305 if (this.currentRange_) {
306 var actionNode = this.currentRange_.start.node;
307 if (actionNode.role == chrome.automation.RoleType.inlineTextBox)
308 actionNode = actionNode.parent;
309 actionNode.showContextMenu();
313 case 'showOptionsPage':
314 var optionsPage = {url: 'chromevox/background/options.html'};
315 chrome.tabs.create(optionsPage);
320 var node = AutomationUtil.findNextNode(
321 current.getBound(dir).node, dir, pred);
324 current = cursors.Range.fromNode(node);
327 cvox.ChromeVox.tts.speak(cvox.ChromeVox.msgs.getMsg(predErrorMsg),
328 cvox.QueueMode.FLUSH);
335 // TODO(dtseng): Figure out what it means to focus a range.
336 var actionNode = current.start.node;
337 if (actionNode.role == chrome.automation.RoleType.inlineTextBox)
338 actionNode = actionNode.parent;
341 var prevRange = this.currentRange_;
342 this.currentRange_ = current;
344 new Output().withSpeechAndBraille(
345 this.currentRange_, prevRange, Output.EventType.NAVIGATE)
351 * Handles a braille command.
352 * @param {!cvox.BrailleKeyEvent} evt
353 * @return {boolean} True if evt was processed.
355 onGotBrailleCommand: function(evt) {
356 if (this.mode_ === ChromeVoxMode.CLASSIC)
359 switch (evt.command) {
360 case cvox.BrailleKeyCommand.PAN_LEFT:
361 this.onGotCommand('previousElement', true);
363 case cvox.BrailleKeyCommand.PAN_RIGHT:
364 this.onGotCommand('nextElement', true);
366 case cvox.BrailleKeyCommand.LINE_UP:
367 this.onGotCommand('previousLine', true);
369 case cvox.BrailleKeyCommand.LINE_DOWN:
370 this.onGotCommand('nextLine', true);
372 case cvox.BrailleKeyCommand.TOP:
373 this.onGotCommand('goToBeginning', true);
375 case cvox.BrailleKeyCommand.BOTTOM:
376 this.onGotCommand('goToEnd', true);
385 * Provides all feedback once ChromeVox's focus changes.
386 * @param {Object} evt
388 onEventDefault: function(evt) {
389 var node = evt.target;
394 var prevRange = this.currentRange_;
396 this.currentRange_ = cursors.Range.fromNode(node);
398 // Check to see if we've crossed roots. Continue if we've crossed roots or
399 // are not within web content.
400 if (node.root.role == 'desktop' ||
402 (prevRange.start.node.root != node.root &&
404 this.setupChromeVoxVariants_(node.root.docUrl || '');
406 // Don't process nodes inside of web content if ChromeVox Next is inactive.
407 if (node.root.role != chrome.automation.RoleType.desktop &&
408 this.mode_ === ChromeVoxMode.CLASSIC) {
409 chrome.accessibilityPrivate.setFocusRing([]);
413 // Don't output if focused node hasn't changed.
415 evt.type == 'focus' &&
416 this.currentRange_.equals(prevRange))
419 new Output().withSpeechAndBraille(
420 this.currentRange_, prevRange, evt.type)
425 * Provides all feedback once a focus event fires.
426 * @param {Object} evt
428 onFocus: function(evt) {
429 var node = evt.target;
431 // It doesn't make sense to focus the containing web area if a descendant
432 // has focused state.
433 if (node.role == 'rootWebArea') {
434 node = AutomationUtil.findNodePost(node,
436 AutomationPredicate.focused) || node;
438 this.onEventDefault({target: node, type: 'focus'});
442 * Provides all feedback once a load complete event fires.
443 * @param {Object} evt
445 onLoadComplete: function(evt) {
446 this.setupChromeVoxVariants_(evt.target.docUrl);
448 // Don't process nodes inside of web content if ChromeVox Next is inactive.
449 if (evt.target.root.role != chrome.automation.RoleType.desktop &&
450 this.mode_ === ChromeVoxMode.CLASSIC)
453 if (this.currentRange_)
456 var root = evt.target;
458 while (webView && webView.role != chrome.automation.RoleType.webView)
459 webView = webView.parent;
461 if (!webView || !webView.state.focused)
464 var node = AutomationUtil.findNodePost(root,
466 AutomationPredicate.leaf);
469 this.currentRange_ = cursors.Range.fromNode(node);
471 if (this.currentRange_)
472 new Output().withSpeechAndBraille(
473 this.currentRange_, null, evt.type)
478 * Provides all feedback once a text selection change event fires.
479 * @param {Object} evt
481 onTextOrTextSelectionChanged: function(evt) {
482 // Don't process nodes inside of web content if ChromeVox Next is inactive.
483 if (evt.target.root.role != chrome.automation.RoleType.desktop &&
484 this.mode_ === ChromeVoxMode.CLASSIC)
487 if (!evt.target.state.focused)
490 if (!this.currentRange_) {
491 this.onEventDefault(evt);
492 this.currentRange_ = cursors.Range.fromNode(evt.target);
495 var textChangeEvent = new cvox.TextChangeEvent(
497 evt.target.textSelStart,
498 evt.target.textSelEnd,
499 true); // triggered by user
500 if (!this.editableTextHandler ||
501 evt.target != this.currentRange_.start.node) {
502 this.editableTextHandler =
503 new cvox.ChromeVoxEditableTextBase(
504 textChangeEvent.value,
505 textChangeEvent.start,
507 evt.target.state['protected'],
511 this.editableTextHandler.changed(textChangeEvent);
512 new Output().withBraille(
513 this.currentRange_, null, evt.type)
518 * Provides all feedback once a value changed event fires.
519 * @param {Object} evt
521 onValueChanged: function(evt) {
522 // Don't process nodes inside of web content if ChromeVox Next is inactive.
523 if (evt.target.root.role != chrome.automation.RoleType.desktop &&
524 this.mode_ === ChromeVoxMode.CLASSIC)
527 if (!evt.target.state.focused)
530 // Value change events fire on web text fields and text areas when pressing
531 // enter; suppress them.
532 if (!this.currentRange_ ||
533 evt.target.role != chrome.automation.RoleType.textField) {
534 this.onEventDefault(evt);
535 this.currentRange_ = cursors.Range.fromNode(evt.target);
540 * Called when the automation tree is changed.
541 * @param {chrome.automation.TreeChange} treeChange
543 onTreeChange: function(treeChange) {
544 var node = treeChange.target;
545 if (!node.containerLiveStatus)
548 if (node.containerLiveRelevant.indexOf('additions') >= 0 &&
549 treeChange.type == 'nodeCreated')
550 this.outputLiveRegionChange_(node, null);
551 if (node.containerLiveRelevant.indexOf('text') >= 0 &&
552 treeChange.type == 'nodeChanged')
553 this.outputLiveRegionChange_(node, null);
554 if (node.containerLiveRelevant.indexOf('removals') >= 0 &&
555 treeChange.type == 'nodeRemoved')
556 this.outputLiveRegionChange_(node, '@live_regions_removed');
560 * Given a node that needs to be spoken as part of a live region
561 * change and an additional optional format string, output the
562 * live region description.
563 * @param {!chrome.automation.AutomationNode} node The changed node.
564 * @param {?string} opt_prependFormatStr If set, a format string for
565 * cvox2.Output to prepend to the output.
568 outputLiveRegionChange_: function(node, opt_prependFormatStr) {
569 var range = cursors.Range.fromNode(node);
570 var output = new Output();
571 if (opt_prependFormatStr) {
572 output.format(opt_prependFormatStr);
574 output.withSpeech(range, null, Output.EventType.NAVIGATE);
582 isWhitelistedForCompat_: function(url) {
583 return url.indexOf('chrome://md-settings') != -1 ||
584 url.indexOf('chrome://oobe/login') != -1 ||
586 'https://accounts.google.com/embedded/setup/chromeos') === 0 ||
592 * @param {string} url
593 * @return {boolean} Whether the given |url| is whitelisted.
595 isWhitelistedForNext_: function(url) {
596 return this.whitelist_.some(function(item) {
597 return url.indexOf(item) != -1;
602 * Setup ChromeVox variants.
603 * @param {string} url
606 setupChromeVoxVariants_: function(url) {
607 var mode = this.mode_;
608 if (mode != ChromeVoxMode.FORCE_NEXT) {
609 if (this.isWhitelistedForCompat_(url))
610 mode = ChromeVoxMode.COMPAT;
611 else if (this.isWhitelistedForNext_(url))
612 mode = ChromeVoxMode.NEXT;
614 mode = ChromeVoxMode.CLASSIC;
617 this.setChromeVoxMode(mode);
621 * Disables classic ChromeVox in current web content.
623 disableClassicChromeVox_: function() {
624 cvox.ExtensionBridge.send({
625 message: 'SYSTEM_COMMAND',
626 command: 'killChromeVox'
631 * Sets the current ChromeVox mode.
632 * @param {ChromeVoxMode} mode
634 setChromeVoxMode: function(mode) {
635 if (mode === ChromeVoxMode.NEXT ||
636 mode === ChromeVoxMode.COMPAT ||
637 mode === ChromeVoxMode.FORCE_NEXT) {
638 if (!chrome.commands.onCommand.hasListener(this.onGotCommand))
639 chrome.commands.onCommand.addListener(this.onGotCommand);
641 if (chrome.commands.onCommand.hasListener(this.onGotCommand))
642 chrome.commands.onCommand.removeListener(this.onGotCommand);
645 chrome.tabs.query({active: true}, function(tabs) {
646 if (mode === ChromeVoxMode.CLASSIC) {
647 // This case should do nothing because Classic gets injected by the
648 // extension system via our manifest. Once ChromeVox Next is enabled
649 // for tabs, re-enable.
650 // cvox.ChromeVox.injectChromeVoxIntoTabs(tabs);
652 // When in compat mode, if the focus is within the desktop tree proper,
653 // then do not disable content scripts.
654 if (this.currentRange_.start.node.root.role == 'desktop')
657 this.disableClassicChromeVox_();
665 /** @type {Background} */
666 global.backgroundObj = new Background();