[sql] Remove _HAS_EXCEPTIONS=0 from build info.
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / cvox2 / background / background.js
blob65e6664f08a38f23ab465867ad034691981a3543
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.
5 /**
6  * @fileoverview The entry point for all ChromeVox2 related code for the
7  * background page.
8  */
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;
28 /**
29  * All possible modes ChromeVox can run.
30  * @enum {string}
31  */
32 var ChromeVoxMode = {
33   CLASSIC: 'classic',
34   COMPAT: 'compat',
35   NEXT: 'next',
36   FORCE_NEXT: 'force_next'
39 /**
40  * ChromeVox2 background page.
41  * @constructor
42  */
43 Background = function() {
44   /**
45    * A list of site substring patterns to use with ChromeVox next. Keep these
46    * strings relatively specific.
47    * @type {!Array<string>}
48    * @private
49    */
50   this.whitelist_ = ['chromevox_next_test'];
52   /**
53    * @type {cursors.Range}
54    * @private
55    */
56   this.currentRange_ = null;
58   /**
59    * Which variant of ChromeVox is active.
60    * @type {ChromeVoxMode}
61    * @private
62    */
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);
72   }
74   /**
75    * Maps an automation event to its listener.
76    * @type {!Object<EventType, function(Object) : void>}
77    */
78   this.listeners_ = {
79     alert: this.onEventDefault,
80     focus: this.onFocus,
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
89   };
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);
98   },
100   /**
101    * Handles all setup once a new automation tree appears.
102    * @param {chrome.automation.AutomationNode} desktop
103    */
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}});
115     if (webView) {
116       var root = webView.find({role: chrome.automation.RoleType.rootWebArea});
117       if (root) {
118         this.onLoadComplete(
119             {target: root,
120              type: chrome.automation.EventType.loadComplete});
121       }
122     }
123   },
125   /**
126    * Handles chrome.commands.onCommand.
127    * @param {string} command
128    * @param {boolean=} opt_skipCompat Whether to skip compatibility checks.
129    */
130   onGotCommand: function(command, opt_skipCompat) {
131     if (!this.currentRange_)
132       return;
134     if (!opt_skipCompat && this.mode_ === ChromeVoxMode.COMPAT) {
135       if (this.compat_.onGotCommand(command))
136         return;
137     }
139     var current = this.currentRange_;
140     var dir = Dir.FORWARD;
141     var pred = null;
142     var predErrorMsg = undefined;
143     switch (command) {
144       case 'nextCharacter':
145         current = current.move(cursors.Unit.CHARACTER, Dir.FORWARD);
146         break;
147       case 'previousCharacter':
148         current = current.move(cursors.Unit.CHARACTER, Dir.BACKWARD);
149         break;
150       case 'nextWord':
151         current = current.move(cursors.Unit.WORD, Dir.FORWARD);
152         break;
153       case 'previousWord':
154         current = current.move(cursors.Unit.WORD, Dir.BACKWARD);
155         break;
156       case 'nextLine':
157         current = current.move(cursors.Unit.LINE, Dir.FORWARD);
158         break;
159       case 'previousLine':
160         current = current.move(cursors.Unit.LINE, Dir.BACKWARD);
161         break;
162       case 'nextButton':
163         dir = Dir.FORWARD;
164         pred = AutomationPredicate.button;
165         predErrorMsg = 'no_next_button';
166         break;
167       case 'previousButton':
168         dir = Dir.BACKWARD;
169         pred = AutomationPredicate.button;
170         predErrorMsg = 'no_previous_button';
171         break;
172       case 'nextCheckBox':
173         dir = Dir.FORWARD;
174         pred = AutomationPredicate.checkBox;
175         predErrorMsg = 'no_next_checkbox';
176         break;
177       case 'previousCheckBox':
178         dir = Dir.BACKWARD;
179         pred = AutomationPredicate.checkBox;
180         predErrorMsg = 'no_previous_checkbox';
181         break;
182       case 'nextComboBox':
183         dir = Dir.FORWARD;
184         pred = AutomationPredicate.comboBox;
185         predErrorMsg = 'no_next_combo_box';
186         break;
187       case 'previousComboBox':
188         dir = Dir.BACKWARD;
189         pred = AutomationPredicate.comboBox;
190         predErrorMsg = 'no_previous_combo_box';
191         break;
192       case 'nextEditText':
193         dir = Dir.FORWARD;
194         pred = AutomationPredicate.editText;
195         predErrorMsg = 'no_next_edit_text';
196         break;
197       case 'previousEditText':
198         dir = Dir.BACKWARD;
199         pred = AutomationPredicate.editText;
200         predErrorMsg = 'no_previous_edit_text';
201         break;
202       case 'nextFormField':
203         dir = Dir.FORWARD;
204         pred = AutomationPredicate.formField;
205         predErrorMsg = 'no_next_form_field';
206         break;
207       case 'previousFormField':
208         dir = Dir.BACKWARD;
209         pred = AutomationPredicate.formField;
210         predErrorMsg = 'no_previous_form_field';
211         break;
212       case 'nextHeading':
213         dir = Dir.FORWARD;
214         pred = AutomationPredicate.heading;
215         predErrorMsg = 'no_next_heading';
216         break;
217       case 'previousHeading':
218         dir = Dir.BACKWARD;
219         pred = AutomationPredicate.heading;
220         predErrorMsg = 'no_previous_heading';
221         break;
222       case 'nextLink':
223         dir = Dir.FORWARD;
224         pred = AutomationPredicate.link;
225         predErrorMsg = 'no_next_link';
226         break;
227       case 'previousLink':
228         dir = Dir.BACKWARD;
229         pred = AutomationPredicate.link;
230         predErrorMsg = 'no_previous_link';
231         break;
232       case 'nextTable':
233         dir = Dir.FORWARD;
234         pred = AutomationPredicate.table;
235         predErrorMsg = 'no_next_table';
236         break;
237       case 'previousTable':
238         dir = Dir.BACKWARD;
239         pred = AutomationPredicate.table;
240         predErrorMsg = 'no_previous_table';
241         break;
242       case 'nextVisitedLink':
243         dir = Dir.FORWARD;
244         pred = AutomationPredicate.visitedLink;
245         predErrorMsg = 'no_next_visited_link';
246         break;
247       case 'previousVisitedLink':
248         dir = Dir.BACKWARD;
249         pred = AutomationPredicate.visitedLink;
250         predErrorMsg = 'no_previous_visited_link';
251         break;
252       case 'nextElement':
253         current = current.move(cursors.Unit.NODE, Dir.FORWARD);
254         break;
255       case 'previousElement':
256         current = current.move(cursors.Unit.NODE, Dir.BACKWARD);
257         break;
258       case 'goToBeginning':
259       var node =
260           AutomationUtil.findNodePost(current.start.node.root,
261                                       Dir.FORWARD,
262                                       AutomationPredicate.leaf);
263         if (node)
264           current = cursors.Range.fromNode(node);
265         break;
266       case 'goToEnd':
267         var node =
268             AutomationUtil.findNodePost(current.start.node.root,
269                                         Dir.BACKWARD,
270                                         AutomationPredicate.leaf);
271         if (node)
272           current = cursors.Range.fromNode(node);
273         break;
274       case 'doDefault':
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();
280         }
281         // Skip all other processing; if focus changes, we should get an event
282         // for that.
283         return;
284       case 'continuousRead':
285         global.isReadingContinuously = true;
286         var continueReading = function(prevRange) {
287           if (!global.isReadingContinuously || !this.currentRange_)
288             return;
290           new Output().withSpeechAndBraille(
291                   this.currentRange_, prevRange, Output.EventType.NAVIGATE)
292               .onSpeechEnd(function() { continueReading(prevRange); })
293               .go();
294           prevRange = this.currentRange_;
295           this.currentRange_ =
296               this.currentRange_.move(cursors.Unit.NODE, Dir.FORWARD);
298           if (!this.currentRange_ || this.currentRange_.equals(prevRange))
299             global.isReadingContinuously = false;
300         }.bind(this);
302         continueReading(null);
303         return;
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();
310           return;
311         }
312         break;
313       case 'showOptionsPage':
314         var optionsPage = {url: 'chromevox/background/options.html'};
315         chrome.tabs.create(optionsPage);
316         break;
317     }
319     if (pred) {
320       var node = AutomationUtil.findNextNode(
321           current.getBound(dir).node, dir, pred);
323       if (node) {
324         current = cursors.Range.fromNode(node);
325       } else {
326         if (predErrorMsg) {
327           cvox.ChromeVox.tts.speak(cvox.ChromeVox.msgs.getMsg(predErrorMsg),
328                                    cvox.QueueMode.FLUSH);
329         }
330         return;
331       }
332     }
334     if (current) {
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;
339       actionNode.focus();
341       var prevRange = this.currentRange_;
342       this.currentRange_ = current;
344       new Output().withSpeechAndBraille(
345               this.currentRange_, prevRange, Output.EventType.NAVIGATE)
346           .go();
347     }
348   },
350   /**
351    * Handles a braille command.
352    * @param {!cvox.BrailleKeyEvent} evt
353    * @return {boolean} True if evt was processed.
354    */
355   onGotBrailleCommand: function(evt) {
356     if (this.mode_ === ChromeVoxMode.CLASSIC)
357       return false;
359     switch (evt.command) {
360       case cvox.BrailleKeyCommand.PAN_LEFT:
361         this.onGotCommand('previousElement', true);
362         break;
363       case cvox.BrailleKeyCommand.PAN_RIGHT:
364         this.onGotCommand('nextElement', true);
365         break;
366       case cvox.BrailleKeyCommand.LINE_UP:
367         this.onGotCommand('previousLine', true);
368         break;
369       case cvox.BrailleKeyCommand.LINE_DOWN:
370         this.onGotCommand('nextLine', true);
371         break;
372       case cvox.BrailleKeyCommand.TOP:
373         this.onGotCommand('goToBeginning', true);
374         break;
375       case cvox.BrailleKeyCommand.BOTTOM:
376         this.onGotCommand('goToEnd', true);
377         break;
378       default:
379         return false;
380     }
381     return true;
382   },
384   /**
385    * Provides all feedback once ChromeVox's focus changes.
386    * @param {Object} evt
387    */
388   onEventDefault: function(evt) {
389     var node = evt.target;
391     if (!node)
392       return;
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' ||
401         !prevRange ||
402         (prevRange.start.node.root != node.root &&
403         node.state.focused))
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([]);
410       return;
411     }
413     // Don't output if focused node hasn't changed.
414     if (prevRange &&
415         evt.type == 'focus' &&
416         this.currentRange_.equals(prevRange))
417       return;
419     new Output().withSpeechAndBraille(
420             this.currentRange_, prevRange, evt.type)
421         .go();
422   },
424   /**
425    * Provides all feedback once a focus event fires.
426    * @param {Object} evt
427    */
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,
435                                          Dir.FORWARD,
436                                          AutomationPredicate.focused) || node;
437     }
438     this.onEventDefault({target: node, type: 'focus'});
439   },
441   /**
442    * Provides all feedback once a load complete event fires.
443    * @param {Object} evt
444    */
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)
451       return;
453     if (this.currentRange_)
454       return;
456     var root = evt.target;
457     var webView = root;
458     while (webView && webView.role != chrome.automation.RoleType.webView)
459       webView = webView.parent;
461     if (!webView || !webView.state.focused)
462       return;
464     var node = AutomationUtil.findNodePost(root,
465         Dir.FORWARD,
466         AutomationPredicate.leaf);
468     if (node)
469       this.currentRange_ = cursors.Range.fromNode(node);
471     if (this.currentRange_)
472       new Output().withSpeechAndBraille(
473               this.currentRange_, null, evt.type)
474           .go();
475   },
477   /**
478    * Provides all feedback once a text selection change event fires.
479    * @param {Object} evt
480    */
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)
485       return;
487     if (!evt.target.state.focused)
488       return;
490     if (!this.currentRange_) {
491       this.onEventDefault(evt);
492       this.currentRange_ = cursors.Range.fromNode(evt.target);
493     }
495     var textChangeEvent = new cvox.TextChangeEvent(
496         evt.target.value,
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,
506               textChangeEvent.end,
507               evt.target.state['protected'],
508               cvox.ChromeVox.tts);
509     }
511     this.editableTextHandler.changed(textChangeEvent);
512     new Output().withBraille(
513             this.currentRange_, null, evt.type)
514         .go();
515   },
517   /**
518    * Provides all feedback once a value changed event fires.
519    * @param {Object} evt
520    */
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)
525       return;
527     if (!evt.target.state.focused)
528       return;
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);
536     }
537   },
539   /**
540    * Called when the automation tree is changed.
541    * @param {chrome.automation.TreeChange} treeChange
542    */
543   onTreeChange: function(treeChange) {
544     var node = treeChange.target;
545     if (!node.containerLiveStatus)
546       return;
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');
557   },
559   /**
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.
566    * @private
567    */
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);
573     }
574     output.withSpeech(range, null, Output.EventType.NAVIGATE);
575     output.go();
576   },
578   /**
579    * @return {boolean}
580    * @private
581    */
582   isWhitelistedForCompat_: function(url) {
583     return url.indexOf('chrome://md-settings') != -1 ||
584           url.indexOf('chrome://oobe/login') != -1 ||
585           url.indexOf(
586               'https://accounts.google.com/embedded/setup/chromeos') === 0 ||
587           url === '';
588   },
590   /**
591    * @private
592    * @param {string} url
593    * @return {boolean} Whether the given |url| is whitelisted.
594    */
595   isWhitelistedForNext_: function(url) {
596     return this.whitelist_.some(function(item) {
597       return url.indexOf(item) != -1;
598     }.bind(this));
599   },
601   /**
602    * Setup ChromeVox variants.
603    * @param {string} url
604    * @private
605    */
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;
613       else
614         mode = ChromeVoxMode.CLASSIC;
615     }
617     this.setChromeVoxMode(mode);
618   },
620   /**
621    * Disables classic ChromeVox in current web content.
622    */
623   disableClassicChromeVox_: function() {
624     cvox.ExtensionBridge.send({
625         message: 'SYSTEM_COMMAND',
626         command: 'killChromeVox'
627     });
628   },
630   /**
631    * Sets the current ChromeVox mode.
632    * @param {ChromeVoxMode} mode
633    */
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);
640     } else {
641       if (chrome.commands.onCommand.hasListener(this.onGotCommand))
642         chrome.commands.onCommand.removeListener(this.onGotCommand);
643     }
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);
651       } else {
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')
655           return;
657         this.disableClassicChromeVox_();
658       }
659     }.bind(this));
661     this.mode_ = mode;
662   }
665 /** @type {Background} */
666 global.backgroundObj = new Background();
668 });  // goog.scope