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('Output');
16 goog.require('Output.EventType');
17 goog.require('cursors.Cursor');
18 goog.require('cvox.ChromeVoxEditableTextBase');
20 goog.scope(function() {
21 var AutomationNode = chrome.automation.AutomationNode;
22 var Dir = AutomationUtil.Dir;
23 var EventType = chrome.automation.EventType;
26 * ChromeVox2 background page.
29 Background = function() {
31 * A list of site substring patterns to use with ChromeVox next. Keep these
32 * strings relatively specific.
33 * @type {!Array<string>}
36 this.whitelist_ = ['chromevox_next_test'];
39 * @type {cursors.Range}
42 this.currentRange_ = null;
45 * Whether ChromeVox Next is active.
51 // Manually bind all functions to |this|.
52 for (var func in this) {
53 if (typeof(this[func]) == 'function')
54 this[func] = this[func].bind(this);
58 * Maps an automation event to its listener.
59 * @type {!Object<EventType, function(Object) : void>}
62 alert: this.onEventDefault,
63 focus: this.onEventDefault,
64 hover: this.onEventDefault,
65 menuStart: this.onEventDefault,
66 menuEnd: this.onEventDefault,
67 menuListValueChanged: this.onEventDefault,
68 loadComplete: this.onLoadComplete,
69 textChanged: this.onTextOrTextSelectionChanged,
70 textSelectionChanged: this.onTextOrTextSelectionChanged,
71 valueChanged: this.onEventDefault
74 // Register listeners for ...
76 chrome.automation.getDesktop(this.onGotDesktop);
79 Background.prototype = {
81 * Handles all setup once a new automation tree appears.
82 * @param {chrome.automation.AutomationNode} desktop
84 onGotDesktop: function(desktop) {
85 // Register all automation event listeners.
86 for (var eventType in this.listeners_)
87 desktop.addEventListener(eventType, this.listeners_[eventType], true);
89 // The focused state gets set on the containing webView node.
90 var webView = desktop.find({role: chrome.automation.RoleType.webView,
91 state: {focused: true}});
93 var root = webView.find({role: chrome.automation.RoleType.rootWebArea});
97 type: chrome.automation.EventType.loadComplete});
103 * Handles chrome.commands.onCommand.
104 * @param {string} command
106 onGotCommand: function(command) {
107 if (command == 'toggleChromeVoxVersion') {
108 this.toggleChromeVoxVersion();
112 if (!this.active_ || !this.currentRange_)
115 var current = this.currentRange_;
116 var dir = Dir.FORWARD;
121 pred = AutomationPredicate.heading;
123 case 'previousHeading':
125 pred = AutomationPredicate.heading;
127 case 'nextCharacter':
128 current = current.move(cursors.Unit.CHARACTER, Dir.FORWARD);
130 case 'previousCharacter':
131 current = current.move(cursors.Unit.CHARACTER, Dir.BACKWARD);
134 current = current.move(cursors.Unit.WORD, Dir.FORWARD);
137 current = current.move(cursors.Unit.WORD, Dir.BACKWARD);
140 current = current.move(cursors.Unit.LINE, Dir.FORWARD);
143 current = current.move(cursors.Unit.LINE, Dir.BACKWARD);
147 pred = AutomationPredicate.link;
151 pred = AutomationPredicate.link;
154 current = current.move(cursors.Unit.NODE, Dir.FORWARD);
156 case 'previousElement':
157 current = current.move(cursors.Unit.NODE, Dir.BACKWARD);
159 case 'goToBeginning':
161 AutomationUtil.findNodePost(current.getStart().getNode().root,
163 AutomationPredicate.leaf);
165 current = cursors.Range.fromNode(node);
169 AutomationUtil.findNodePost(current.getStart().getNode().root,
171 AutomationPredicate.leaf);
173 current = cursors.Range.fromNode(node);
176 if (this.currentRange_)
177 this.currentRange_.getStart().getNode().doDefault();
179 case 'continuousRead':
180 global.isReadingContinuously = true;
181 var continueReading = function(prevRange) {
182 if (!global.isReadingContinuously)
185 new Output().withSpeechAndBraille(
186 this.currentRange_, prevRange, Output.EventType.NAVIGATE)
187 .onSpeechEnd(function() { continueReading(prevRange); })
189 prevRange = this.currentRange_;
191 this.currentRange_.move(cursors.Unit.NODE, Dir.FORWARD);
193 if (this.currentRange_.equals(prevRange))
194 global.isReadingContinuously = false;
197 continueReading(null);
202 var node = AutomationUtil.findNextNode(
203 current.getBound(dir).getNode(), dir, pred);
206 current = cursors.Range.fromNode(node);
210 // TODO(dtseng): Figure out what it means to focus a range.
211 current.getStart().getNode().focus();
213 var prevRange = this.currentRange_;
214 this.currentRange_ = current;
215 new Output().withSpeechAndBraille(
216 this.currentRange_, prevRange, Output.EventType.NAVIGATE)
222 * Provides all feedback once ChromeVox's focus changes.
223 * @param {Object} evt
225 onEventDefault: function(evt) {
226 var node = evt.target;
231 var prevRange = this.currentRange_;
232 this.currentRange_ = cursors.Range.fromNode(node);
234 // Don't process nodes inside of web content if ChromeVox Next is inactive.
235 if (node.root.role != chrome.automation.RoleType.desktop && !this.active_)
238 new Output().withSpeechAndBraille(
239 this.currentRange_, prevRange, evt.type)
244 * Provides all feedback once a load complete event fires.
245 * @param {Object} evt
247 onLoadComplete: function(evt) {
248 var next = this.isWhitelisted_(evt.target.attributes.url);
249 this.toggleChromeVoxVersion({next: next, classic: !next});
250 // Don't process nodes inside of web content if ChromeVox Next is inactive.
251 if (evt.target.root.role != chrome.automation.RoleType.desktop &&
255 if (this.currentRange_)
258 var root = evt.target;
260 while (webView && webView.role != chrome.automation.RoleType.webView)
261 webView = webView.parent;
263 if (!webView || !webView.state.focused)
266 var node = AutomationUtil.findNodePost(root,
268 AutomationPredicate.leaf);
271 this.currentRange_ = cursors.Range.fromNode(node);
273 if (this.currentRange_)
274 new Output().withSpeechAndBraille(
275 this.currentRange_, null, evt.type)
280 * Provides all feedback once a text selection change event fires.
281 * @param {Object} evt
283 onTextOrTextSelectionChanged: function(evt) {
284 // Don't process nodes inside of web content if ChromeVox Next is inactive.
285 if (evt.target.root.role != chrome.automation.RoleType.desktop &&
289 if (!evt.target.state.focused)
292 if (!this.currentRange_) {
293 this.onEventDefault(evt);
294 this.currentRange_ = cursors.Range.fromNode(evt.target);
297 var textChangeEvent = new cvox.TextChangeEvent(
298 evt.target.attributes.value,
299 evt.target.attributes.textSelStart,
300 evt.target.attributes.textSelEnd,
301 true); // triggered by user
302 if (!this.editableTextHandler ||
303 evt.target != this.currentRange_.getStart().getNode()) {
304 this.editableTextHandler =
305 new cvox.ChromeVoxEditableTextBase(
306 textChangeEvent.value,
307 textChangeEvent.start,
309 evt.target.state['protected'],
313 this.editableTextHandler.changed(textChangeEvent);
314 new Output().withBraille(
315 this.currentRange_, null, evt.type)
321 * @param {string} url
322 * @return {boolean} Whether the given |url| is whitelisted.
324 isWhitelisted_: function(url) {
325 return this.whitelist_.some(function(item) {
326 return url.indexOf(item) != -1;
331 * Disables classic ChromeVox.
332 * @param {number} tabId The tab where ChromeVox classic is running in.
334 disableClassicChromeVox_: function(tabId) {
335 chrome.tabs.executeScript(
337 {'code': 'try { window.disableChromeVox(); } catch(e) { }\n',
342 * Toggles between ChromeVox Next and Classic.
343 * @param {{classic: boolean, next: boolean}=} opt_options Forceably set.
345 toggleChromeVoxVersion: function(opt_options) {
348 opt_options.next = !this.active_;
349 opt_options.classic = !opt_options.next;
352 if (opt_options.next) {
353 if (!chrome.commands.onCommand.hasListener(this.onGotCommand))
354 chrome.commands.onCommand.addListener(this.onGotCommand);
357 if (chrome.commands.onCommand.hasListener(this.onGotCommand))
358 chrome.commands.onCommand.removeListener(this.onGotCommand);
359 this.active_ = false;
362 chrome.tabs.query({active: true}, function(tabs) {
363 if (opt_options.classic) {
364 // This case should do nothing because Classic gets injected by the
365 // extension system via our manifest. Once ChromeVox Next is enabled
366 // for tabs, re-enable.
367 // cvox.ChromeVox.injectChromeVoxIntoTabs(tabs);
369 tabs.forEach(function(tab) {
370 this.disableClassicChromeVox_(tab.id);
377 /** @type {Background} */
378 global.backgroundObj = new Background();