user_commands.js revision 46d4c2bc3267f3f028f39e7e311b0f89aba2e4fd
145afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.org// Copyright 2014 The Chromium Authors. All rights reserved.
245afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.org// Use of this source code is governed by a BSD-style license that can be
345afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.org// found in the LICENSE file.
445afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.org
545afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.org/**
645afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.org * @fileoverview High level commands that the user can invoke using hotkeys.
745afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.org *
845afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.org * Usage:
945afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.org * If you are here, you probably want to add a new user command. Here are some
1045afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.org * general steps to get you started.
1145afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.org * - Go to command_store.js, where all static data about a command lives. Follow
1245afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.org * the instructions there.
1345afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.org * - Add the logic of the command to doCommand_ below. Try to reuse or group
1445afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.org * your command with related commands.
1545afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.org */
1645afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.org
1745afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.org
1845afe016bed87b9c6946184709058b39ede3f77ajwong@chromium.orggoog.provide('cvox.ChromeVoxUserCommands');
19
20goog.require('cvox.BrailleKeyCommand');
21goog.require('cvox.BrailleOverlayWidget');
22goog.require('cvox.ChromeVox');
23goog.require('cvox.CommandStore');
24goog.require('cvox.ConsoleTts');
25goog.require('cvox.ContextMenuWidget');
26goog.require('cvox.DomPredicates');
27goog.require('cvox.DomUtil');
28goog.require('cvox.FocusUtil');
29goog.require('cvox.KeyboardHelpWidget');
30goog.require('cvox.NodeSearchWidget');
31goog.require('cvox.PlatformUtil');
32goog.require('cvox.SearchWidget');
33goog.require('cvox.SelectWidget');
34goog.require('cvox.TypingEcho');
35goog.require('cvox.UserEventDetail');
36goog.require('goog.object');
37
38
39/**
40 * Initializes commands map.
41 * Initializes global members.
42 * @private
43 */
44cvox.ChromeVoxUserCommands.init_ = function() {
45  if (cvox.ChromeVoxUserCommands.commands) {
46    return;
47  } else {
48    cvox.ChromeVoxUserCommands.commands = {};
49  }
50  for (var cmd in cvox.CommandStore.CMD_WHITELIST) {
51    cvox.ChromeVoxUserCommands.commands[cmd] =
52        cvox.ChromeVoxUserCommands.createCommand_(cmd);
53  }
54};
55
56
57/**
58 * @type {!Object.<string, function(Object=): boolean>}
59 */
60cvox.ChromeVoxUserCommands.commands;
61
62
63/**
64 * @type {boolean}
65 * TODO (clchen, dmazzoni): Implement syncing on click to avoid needing this.
66 */
67cvox.ChromeVoxUserCommands.wasMouseClicked = false;
68
69
70/**
71 * @type {boolean} Flag to set whether or not certain user commands will be
72 * first dispatched to the underlying web page. Some commands (such as finding
73 * the next/prev structural element) may be better implemented by the web app
74 * than by ChromeVox.
75 *
76 * By default, this is enabled; however, for testing, we usually disable this to
77 * reduce flakiness caused by event timing issues.
78 *
79 * TODO (clchen, dtseng): Fix testing framework so that we don't need to turn
80 * this feature off at all.
81 */
82cvox.ChromeVoxUserCommands.enableCommandDispatchingToPage = true;
83
84
85/**
86 * Handles any tab navigation by putting focus at the user's position.
87 * This function will create dummy nodes if there is nothing that is focusable
88 * at the current position.
89 * TODO (adu): This function is too long. We need to break it up into smaller
90 * helper functions.
91 * @return {boolean} True if default action should be taken.
92 * @private
93 */
94cvox.ChromeVoxUserCommands.handleTabAction_ = function() {
95  cvox.ChromeVox.tts.stop();
96
97  // If we are tabbing from an invalid location, prevent the default action.
98  // We pass the isFocusable function as a predicate to specify we only want to
99  // revert to focusable nodes.
100  if (!cvox.ChromeVox.navigationManager.resolve(cvox.DomUtil.isFocusable)) {
101    cvox.ChromeVox.navigationManager.setFocus();
102    return false;
103  }
104
105  // If the user is already focused on a link or control,
106  // nothing more needs to be done.
107  var isLinkControl = cvox.ChromeVoxUserCommands.isFocusedOnLinkControl_();
108  if (isLinkControl) {
109    return true;
110  }
111
112  // Try to find something reasonable to focus on.
113  // Use selection if it exists because it means that the user has probably
114  // clicked with their mouse and we should respect their position.
115  // If there is no selection, then use the last known position based on
116  // NavigationManager's currentNode.
117  var anchorNode = null;
118  var focusNode = null;
119  var sel = window.getSelection();
120  if (!cvox.ChromeVoxUserCommands.wasMouseClicked) {
121    sel = null;
122  } else {
123    cvox.ChromeVoxUserCommands.wasMouseClicked = false;
124  }
125  if (sel == null || sel.anchorNode == null || sel.focusNode == null) {
126    anchorNode = cvox.ChromeVox.navigationManager.getCurrentNode();
127    focusNode = cvox.ChromeVox.navigationManager.getCurrentNode();
128  } else {
129    anchorNode = sel.anchorNode;
130    focusNode = sel.focusNode;
131  }
132
133  // See if we can set focus to either anchorNode or focusNode.
134  // If not, try the parents. Otherwise give up and create a dummy span.
135  if (anchorNode == null || focusNode == null) {
136    return true;
137  }
138  if (cvox.DomUtil.isFocusable(anchorNode)) {
139    anchorNode.focus();
140    return true;
141  }
142  if (cvox.DomUtil.isFocusable(focusNode)) {
143    focusNode.focus();
144    return true;
145  }
146  if (cvox.DomUtil.isFocusable(anchorNode.parentNode)) {
147    anchorNode.parentNode.focus();
148    return true;
149  }
150  if (cvox.DomUtil.isFocusable(focusNode.parentNode)) {
151    focusNode.parentNode.focus();
152    return true;
153  }
154
155  // Insert and focus a dummy span immediately before the current position
156  // so that the default tab action will start off as close to the user's
157  // current position as possible.
158  var bestGuess = anchorNode;
159  var dummySpan = cvox.ChromeVoxUserCommands.createTabDummySpan_();
160  bestGuess.parentNode.insertBefore(dummySpan, bestGuess);
161  dummySpan.focus();
162  return true;
163};
164
165
166/**
167 * @return {boolean} True if we are focused on a link or any other control.
168 * @private
169 */
170cvox.ChromeVoxUserCommands.isFocusedOnLinkControl_ = function() {
171  var tagName = 'A';
172  if ((document.activeElement.tagName == tagName) ||
173      cvox.DomUtil.isControl(document.activeElement)) {
174    return true;
175  }
176  return false;
177};
178
179
180/**
181 * If a lingering tab dummy span exists, remove it.
182 */
183cvox.ChromeVoxUserCommands.removeTabDummySpan = function() {
184  // Break the following line to get around a Chromium js linter warning.
185  // TODO(plundblad): Find a better solution.
186  var previousDummySpan = document.
187      getElementById('ChromeVoxTabDummySpan');
188  if (previousDummySpan && document.activeElement != previousDummySpan) {
189    previousDummySpan.parentNode.removeChild(previousDummySpan);
190  }
191};
192
193
194/**
195 * Create a new tab dummy span.
196 * @return {Element} The dummy span element to be inserted.
197 * @private
198 */
199cvox.ChromeVoxUserCommands.createTabDummySpan_ = function() {
200  var span = document.createElement('span');
201  span.id = 'ChromeVoxTabDummySpan';
202  span.tabIndex = -1;
203  return span;
204};
205
206
207/**
208 * @param {string} cmd The programmatic command name.
209 * @return {function(Object=): boolean} The callable command taking an optional
210 * args dictionary.
211 * @private
212 */
213cvox.ChromeVoxUserCommands.createCommand_ = function(cmd) {
214  return goog.bind(function(opt_kwargs) {
215    var cmdStruct = cvox.ChromeVoxUserCommands.lookupCommand_(cmd, opt_kwargs);
216    return cvox.ChromeVoxUserCommands.dispatchCommand_(cmdStruct);
217  }, cvox.ChromeVoxUserCommands);
218};
219
220
221/**
222 * @param {Object} cmdStruct The command to do.
223 * @return {boolean} False to prevent the default action. True otherwise.
224 * @private
225 */
226cvox.ChromeVoxUserCommands.dispatchCommand_ = function(cmdStruct) {
227  if (cvox.Widget.isActive()) {
228    return true;
229  }
230  if (!cvox.PlatformUtil.matchesPlatform(cmdStruct.platformFilter) ||
231      (cmdStruct.skipInput && cvox.FocusUtil.isFocusInTextInputField())) {
232    return true;
233  }
234  // Handle dispatching public command events
235  if (cvox.ChromeVoxUserCommands.enableCommandDispatchingToPage &&
236      (cvox.UserEventDetail.JUMP_COMMANDS.indexOf(cmdStruct.command) != -1)) {
237    var detail = new cvox.UserEventDetail({command: cmdStruct.command});
238    var evt = detail.createEventObject();
239    var currentNode = cvox.ChromeVox.navigationManager.getCurrentNode();
240    if (!currentNode) {
241      currentNode = document.body;
242    }
243    currentNode.dispatchEvent(evt);
244    return false;
245  }
246  // Not a public command; act on this command directly.
247  return cvox.ChromeVoxUserCommands.doCommand_(cmdStruct);
248};
249
250
251/**
252 * @param {Object} cmdStruct The command to do.
253 * @return {boolean} False to prevent the default action. True otherwise.
254 * @private
255 */
256cvox.ChromeVoxUserCommands.doCommand_ = function(cmdStruct) {
257  if (cvox.Widget.isActive()) {
258    return true;
259  }
260
261  if (!cvox.PlatformUtil.matchesPlatform(cmdStruct.platformFilter) ||
262      (cmdStruct.skipInput && cvox.FocusUtil.isFocusInTextInputField())) {
263    return true;
264  }
265
266  if (cmdStruct.disallowOOBE && document.URL.match(/^chrome:\/\/oobe/i)) {
267    return true;
268  }
269
270  var cmd = cmdStruct.command;
271
272  if (!cmdStruct.allowEvents) {
273    cvox.ChromeVoxEventSuspender.enterSuspendEvents();
274  }
275
276  if (cmdStruct.disallowContinuation) {
277    cvox.ChromeVox.navigationManager.stopReading(true);
278  }
279
280  if (cmdStruct.forward) {
281    cvox.ChromeVox.navigationManager.setReversed(false);
282  } else if (cmdStruct.backward) {
283    cvox.ChromeVox.navigationManager.setReversed(true);
284  }
285
286  if (cmdStruct.findNext) {
287    cmd = 'find';
288    cmdStruct.announce = true;
289  }
290
291  var errorMsg = '';
292  var prefixMsg = '';
293  var ret = false;
294  switch (cmd) {
295    case 'handleTab':
296    case 'handleTabPrev':
297      ret = cvox.ChromeVoxUserCommands.handleTabAction_();
298      break;
299    case 'forward':
300    case 'backward':
301      ret = !cvox.ChromeVox.navigationManager.navigate();
302      break;
303    case 'right':
304    case 'left':
305      cvox.ChromeVox.navigationManager.subnavigate();
306      break;
307    case 'find':
308      if (!cmdStruct.findNext) {
309        throw 'Invalid find command.';
310      }
311      var NodeInfoStruct =
312          cvox.CommandStore.NODE_INFO_MAP[cmdStruct.findNext];
313      var predicateName = NodeInfoStruct.predicate;
314      var predicate = cvox.DomPredicates[predicateName];
315      var error = '';
316      var wrap = '';
317      if (cmdStruct.forward) {
318        wrap = cvox.ChromeVox.msgs.getMsg('wrapped_to_top');
319        error = cvox.ChromeVox.msgs.getMsg(NodeInfoStruct.forwardError);
320      } else if (cmdStruct.backward) {
321        wrap = cvox.ChromeVox.msgs.getMsg('wrapped_to_bottom');
322        error = cvox.ChromeVox.msgs.getMsg(NodeInfoStruct.backwardError);
323      }
324      var found = null;
325      var status = cmdStruct.status || cvox.UserEventDetail.Status.PENDING;
326      var resultNode = cmdStruct.resultNode || null;
327      switch (status) {
328        case cvox.UserEventDetail.Status.SUCCESS:
329          if (resultNode) {
330            cvox.ChromeVox.navigationManager.updateSelToArbitraryNode(
331                resultNode, true);
332          }
333          break;
334        case cvox.UserEventDetail.Status.FAILURE:
335          prefixMsg = error;
336          break;
337        default:
338          found = cvox.ChromeVox.navigationManager.findNext(
339              predicate, predicateName);
340          if (!found) {
341            cvox.ChromeVox.navigationManager.saveSel();
342            prefixMsg = wrap;
343            cvox.ChromeVox.navigationManager.syncToBeginning();
344            cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.WRAP);
345            found = cvox.ChromeVox.navigationManager.findNext(
346                predicate, predicateName, true);
347            if (!found) {
348              prefixMsg = error;
349              cvox.ChromeVox.navigationManager.restoreSel();
350            }
351          }
352          break;
353      }
354      // NavigationManager performs announcement inside of frames when finding.
355      if (found && found.start.node.tagName == 'IFRAME') {
356        cmdStruct.announce = false;
357      }
358      break;
359    // TODO(stoarca): Bad naming. Should be less instead of previous.
360    case 'previousGranularity':
361      cvox.ChromeVox.navigationManager.makeLessGranular(true);
362      prefixMsg = cvox.ChromeVox.navigationManager.getGranularityMsg();
363      break;
364    case 'nextGranularity':
365      cvox.ChromeVox.navigationManager.makeMoreGranular(true);
366      prefixMsg = cvox.ChromeVox.navigationManager.getGranularityMsg();
367      break;
368
369    case 'previousCharacter':
370      cvox.ChromeVox.navigationManager.navigate(false,
371          cvox.NavigationShifter.GRANULARITIES.CHARACTER);
372      break;
373    case 'nextCharacter':
374      cvox.ChromeVox.navigationManager.navigate(false,
375          cvox.NavigationShifter.GRANULARITIES.CHARACTER);
376      break;
377
378    case 'previousWord':
379      cvox.ChromeVox.navigationManager.navigate(false,
380          cvox.NavigationShifter.GRANULARITIES.WORD);
381      break;
382    case 'nextWord':
383      cvox.ChromeVox.navigationManager.navigate(false,
384          cvox.NavigationShifter.GRANULARITIES.WORD);
385      break;
386
387    case 'previousSentence':
388      cvox.ChromeVox.navigationManager.navigate(false,
389          cvox.NavigationShifter.GRANULARITIES.SENTENCE);
390      break;
391    case 'nextSentence':
392      cvox.ChromeVox.navigationManager.navigate(false,
393          cvox.NavigationShifter.GRANULARITIES.SENTENCE);
394      break;
395
396    case 'previousLine':
397      cvox.ChromeVox.navigationManager.navigate(false,
398          cvox.NavigationShifter.GRANULARITIES.LINE);
399      break;
400    case 'nextLine':
401      cvox.ChromeVox.navigationManager.navigate(false,
402          cvox.NavigationShifter.GRANULARITIES.LINE);
403      break;
404
405    case 'previousObject':
406      cvox.ChromeVox.navigationManager.navigate(false,
407          cvox.NavigationShifter.GRANULARITIES.OBJECT);
408      break;
409    case 'nextObject':
410      cvox.ChromeVox.navigationManager.navigate(false,
411          cvox.NavigationShifter.GRANULARITIES.OBJECT);
412      break;
413
414    case 'previousGroup':
415      cvox.ChromeVox.navigationManager.navigate(false,
416          cvox.NavigationShifter.GRANULARITIES.GROUP);
417      break;
418    case 'nextGroup':
419      cvox.ChromeVox.navigationManager.navigate(false,
420          cvox.NavigationShifter.GRANULARITIES.GROUP);
421      break;
422
423    case 'previousRow':
424    case 'previousCol':
425      // Fold these commands to their "next" equivalents since we already set
426      // isReversed above.
427      cmd = cmd == 'previousRow' ? 'nextRow' : 'nextCol';
428    case 'nextRow':
429    case 'nextCol':
430      cvox.ChromeVox.navigationManager.performAction('enterShifterSilently');
431      cvox.ChromeVox.navigationManager.performAction(cmd);
432      break;
433
434    case 'moveToStartOfLine':
435    case 'moveToEndOfLine':
436      cvox.ChromeVox.navigationManager.setGranularity(
437          cvox.NavigationShifter.GRANULARITIES.LINE);
438      cvox.ChromeVox.navigationManager.sync();
439      cvox.ChromeVox.navigationManager.collapseSelection();
440      break;
441
442    case 'readFromHere':
443      cvox.ChromeVox.navigationManager.setGranularity(
444          cvox.NavigationShifter.GRANULARITIES.OBJECT, true, true);
445      cvox.ChromeVox.navigationManager.startReading(
446          cvox.AbstractTts.QUEUE_MODE_FLUSH);
447      break;
448    case 'cycleTypingEcho':
449      cvox.ChromeVox.host.sendToBackgroundPage({
450        'target': 'Prefs',
451        'action': 'setPref',
452        'pref': 'typingEcho',
453        'value': cvox.TypingEcho.cycle(cvox.ChromeVox.typingEcho),
454        'announce': true
455      });
456      break;
457    case 'jumpToTop':
458    case cvox.BrailleKeyCommand.TOP:
459      cvox.ChromeVox.navigationManager.syncToBeginning();
460      break;
461    case 'jumpToBottom':
462    case cvox.BrailleKeyCommand.BOTTOM:
463      cvox.ChromeVox.navigationManager.syncToBeginning();
464      break;
465    case 'stopSpeech':
466      cvox.ChromeVox.navigationManager.stopReading(true);
467      break;
468    case 'toggleKeyboardHelp':
469      cvox.KeyboardHelpWidget.getInstance().toggle();
470      break;
471    case 'help':
472      cvox.ChromeVox.tts.stop();
473      cvox.ChromeVox.host.sendToBackgroundPage({
474        'target': 'HelpDocs',
475        'action': 'open'
476      });
477      break;
478    case 'contextMenu':
479      // Move this logic to a central dispatching class if it grows any bigger.
480      var node = cvox.ChromeVox.navigationManager.getCurrentNode();
481      if (node.tagName == 'SELECT' && !node.multiple) {
482        new cvox.SelectWidget(node).show();
483      } else {
484        var contextMenuWidget = new cvox.ContextMenuWidget();
485        contextMenuWidget.toggle();
486      }
487      break;
488    case 'showBookmarkManager':
489      // TODO(stoarca): Should this have tts.stop()??
490      cvox.ChromeVox.host.sendToBackgroundPage({
491        'target': 'BookmarkManager',
492        'action': 'open'
493      });
494      break;
495    case 'showOptionsPage':
496      cvox.ChromeVox.tts.stop();
497      cvox.ChromeVox.host.sendToBackgroundPage({
498        'target': 'Options',
499        'action': 'open'
500      });
501      break;
502    case 'showKbExplorerPage':
503      cvox.ChromeVox.tts.stop();
504      cvox.ChromeVox.host.sendToBackgroundPage({
505        'target': 'KbExplorer',
506        'action': 'open'
507      });
508      break;
509    case 'readLinkURL':
510      var activeElement = document.activeElement;
511      var currentSelectionAnchor = window.getSelection().anchorNode;
512
513      var url = '';
514      if (activeElement.tagName == 'A') {
515        url = cvox.DomUtil.getLinkURL(activeElement);
516      } else if (currentSelectionAnchor) {
517        url = cvox.DomUtil.getLinkURL(currentSelectionAnchor.parentNode);
518      }
519
520      if (url != '') {
521        cvox.ChromeVox.tts.speak(url);
522      } else {
523        cvox.ChromeVox.tts.speak(cvox.ChromeVox.msgs.getMsg('no_url_found'));
524      }
525      break;
526    case 'readCurrentTitle':
527      cvox.ChromeVox.tts.speak(document.title);
528      break;
529    case 'readCurrentURL':
530      cvox.ChromeVox.tts.speak(document.URL);
531      break;
532    case 'performDefaultAction':
533      if (cvox.DomPredicates.linkPredicate([document.activeElement])) {
534        if (cvox.DomUtil.isInternalLink(document.activeElement)) {
535          // First, sync our selection to the destination of the internal link.
536          cvox.DomUtil.syncInternalLink(document.activeElement);
537          // Now, sync our selection based on the current granularity.
538          cvox.ChromeVox.navigationManager.sync();
539          // Announce this new selection.
540          cmdStruct.announce = true;
541        }
542      }
543      break;
544    case 'forceClickOnCurrentItem':
545      prefixMsg = cvox.ChromeVox.msgs.getMsg('element_clicked');
546      var targetNode = cvox.ChromeVox.navigationManager.getCurrentNode();
547      cvox.DomUtil.clickElem(targetNode, false, false);
548      break;
549    case 'forceDoubleClickOnCurrentItem':
550      prefixMsg = cvox.ChromeVox.msgs.getMsg('element_double_clicked');
551      var targetNode = cvox.ChromeVox.navigationManager.getCurrentNode();
552      cvox.DomUtil.clickElem(targetNode, false, false, true);
553      break;
554    case 'toggleChromeVox':
555      cvox.ChromeVox.host.sendToBackgroundPage({
556        'target': 'Prefs',
557        'action': 'setPref',
558        'pref': 'active',
559        'value': !cvox.ChromeVox.isActive
560      });
561      break;
562    case 'fullyDescribe':
563      var descs = cvox.ChromeVox.navigationManager.getFullDescription();
564      cvox.ChromeVox.navigationManager.speakDescriptionArray(
565          descs,
566          cvox.AbstractTts.QUEUE_MODE_FLUSH,
567          null);
568      break;
569    case 'speakTimeAndDate':
570      var dateTime = new Date();
571      cvox.ChromeVox.tts.speak(
572          dateTime.toLocaleTimeString() + ', ' + dateTime.toLocaleDateString());
573      break;
574    case 'toggleSelection':
575      var selState = cvox.ChromeVox.navigationManager.togglePageSel();
576      prefixMsg = cvox.ChromeVox.msgs.getMsg(
577          selState ? 'begin_selection' : 'end_selection');
578    break;
579    case 'startHistoryRecording':
580      cvox.History.getInstance().startRecording();
581      break;
582    case 'stopHistoryRecording':
583      cvox.History.getInstance().stopRecording();
584      break;
585    case 'enableConsoleTts':
586      cvox.ConsoleTts.getInstance().setEnabled(true);
587      break;
588    case 'toggleBrailleCaptions':
589      cvox.ChromeVox.host.sendToBackgroundPage({
590        'target': 'Prefs',
591        'action': 'setPref',
592        'pref': 'brailleCaptions',
593        'value': !cvox.BrailleOverlayWidget.getInstance().isActive()
594      });
595      break;
596
597    // Table actions.
598    case 'goToFirstCell':
599    case 'goToLastCell':
600    case 'goToRowFirstCell':
601    case 'goToRowLastCell':
602    case 'goToColFirstCell':
603    case 'goToColLastCell':
604    case 'announceHeaders':
605    case 'speakTableLocation':
606    case 'exitShifterContent':
607      if (!cvox.DomPredicates.tablePredicate(cvox.DomUtil.getAncestors(
608              cvox.ChromeVox.navigationManager.getCurrentNode())) ||
609          !cvox.ChromeVox.navigationManager.performAction(cmd)) {
610        errorMsg = 'not_inside_table';
611      }
612      break;
613
614    // Generic actions.
615    case 'enterShifter':
616    case 'exitShifter':
617      cvox.ChromeVox.navigationManager.performAction(cmd);
618      break;
619    // TODO(stoarca): Code repetition.
620    case 'decreaseTtsRate':
621      // TODO(stoarca): This function name is way too long.
622      cvox.ChromeVox.tts.increaseOrDecreaseProperty(
623          cvox.AbstractTts.RATE, false);
624      break;
625    case 'increaseTtsRate':
626      cvox.ChromeVox.tts.increaseOrDecreaseProperty(
627          cvox.AbstractTts.RATE, true);
628      break;
629    case 'decreaseTtsPitch':
630      cvox.ChromeVox.tts.increaseOrDecreaseProperty(
631          cvox.AbstractTts.PITCH, false);
632      break;
633    case 'increaseTtsPitch':
634      cvox.ChromeVox.tts.increaseOrDecreaseProperty(
635          cvox.AbstractTts.PITCH, true);
636      break;
637    case 'decreaseTtsVolume':
638      cvox.ChromeVox.tts.increaseOrDecreaseProperty(
639          cvox.AbstractTts.VOLUME, false);
640      break;
641    case 'increaseTtsVolume':
642      cvox.ChromeVox.tts.increaseOrDecreaseProperty(
643          cvox.AbstractTts.VOLUME, true);
644      break;
645      case 'cyclePunctuationEcho':
646        cvox.ChromeVox.host.sendToBackgroundPage({
647            'target': 'TTS',
648            'action': 'cyclePunctuationEcho'
649          });
650        break;
651
652    case 'toggleStickyMode':
653      cvox.ChromeVox.host.sendToBackgroundPage({
654        'target': 'Prefs',
655        'action': 'setPref',
656        'pref': 'sticky',
657        'value': !cvox.ChromeVox.isStickyPrefOn,
658        'announce': true
659      });
660      break;
661    case 'toggleKeyPrefix':
662      cvox.ChromeVox.keyPrefixOn = !cvox.ChromeVox.keyPrefixOn;
663      break;
664    case 'passThroughMode':
665      cvox.ChromeVox.passThroughMode = true;
666      cvox.ChromeVox.tts.speak(
667          cvox.ChromeVox.msgs.getMsg('pass_through_key'));
668      break;
669    case 'toggleSearchWidget':
670      cvox.SearchWidget.getInstance().toggle();
671      break;
672
673    case 'toggleEarcons':
674      prefixMsg = cvox.ChromeVox.earcons.toggle() ?
675          cvox.ChromeVox.msgs.getMsg('earcons_on') :
676              cvox.ChromeVox.msgs.getMsg('earcons_off');
677      break;
678
679    case 'showHeadingsList':
680    case 'showLinksList':
681    case 'showFormsList':
682    case 'showTablesList':
683    case 'showLandmarksList':
684      if (!cmdStruct.nodeList) {
685        break;
686      }
687      var nodeListStruct =
688          cvox.CommandStore.NODE_INFO_MAP[cmdStruct.nodeList];
689
690      cvox.NodeSearchWidget.create(nodeListStruct.typeMsg,
691                  cvox.DomPredicates[nodeListStruct.predicate]).show();
692      break;
693
694    case 'openLongDesc':
695      var currentNode = cvox.ChromeVox.navigationManager.getCurrentNode();
696      if (cvox.DomUtil.hasLongDesc(currentNode)) {
697        cvox.ChromeVox.host.sendToBackgroundPage({
698          'target': 'OpenTab',
699          'url': currentNode.longDesc // Use .longDesc instead of getAttribute
700                                      // since we want Chrome to convert the
701                                      // longDesc to an absolute URL.
702        });
703      } else {
704        cvox.ChromeVox.tts.speak(
705          cvox.ChromeVox.msgs.getMsg('no_long_desc'),
706          cvox.AbstractTts.QUEUE_MODE_FLUSH,
707          cvox.AbstractTts.PERSONALITY_ANNOTATION);
708      }
709      break;
710
711    case 'pauseAllMedia':
712      var videos = document.getElementsByTagName('VIDEO');
713      for (var i = 0, mediaElem; mediaElem = videos[i]; i++) {
714        mediaElem.pause();
715      }
716      var audios = document.getElementsByTagName('AUDIO');
717      for (var i = 0, mediaElem; mediaElem = audios[i]; i++) {
718        mediaElem.pause();
719      }
720      break;
721
722    // Math specific commands.
723    case 'toggleSemantics':
724      if (cvox.TraverseMath.toggleSemantic()) {
725        cvox.ChromeVox.tts.speak(cvox.ChromeVox.msgs.getMsg('semantics_on'));
726      } else {
727        cvox.ChromeVox.tts.speak(cvox.ChromeVox.msgs.getMsg('semantics_off'));
728      }
729      break;
730
731    // Braille specific commands.
732    case cvox.BrailleKeyCommand.ROUTING:
733      var braille = cmdStruct.content;
734      if (braille) {
735        cvox.BrailleUtil.click(braille, cmdStruct.event.displayPosition);
736      }
737      break;
738    case cvox.BrailleKeyCommand.PAN_LEFT:
739    case cvox.BrailleKeyCommand.LINE_UP:
740    case cvox.BrailleKeyCommand.PAN_RIGHT:
741    case cvox.BrailleKeyCommand.LINE_DOWN:
742      // TODO(dtseng, plundblad): This needs to sync to the last pan position
743      // after line up/pan left and move the display to the far right on the
744      // line in case the synced to node is longer than one display line.
745      // Should also work with all widgets.
746      cvox.ChromeVox.navigationManager.navigate(false,
747          cvox.NavigationShifter.GRANULARITIES.LINE);
748      break;
749
750    case 'debug':
751      // TODO(stoarca): This doesn't belong here.
752      break;
753
754    case 'nop':
755      break;
756    default:
757      throw 'Command behavior not defined: ' + cmd;
758  }
759
760  if (errorMsg != '') {
761    cvox.ChromeVox.tts.speak(
762        cvox.ChromeVox.msgs.getMsg(errorMsg),
763        cvox.AbstractTts.QUEUE_MODE_FLUSH,
764        cvox.AbstractTts.PERSONALITY_ANNOTATION);
765  } else if (cvox.ChromeVox.navigationManager.isReading()) {
766    if (cmdStruct.disallowContinuation) {
767      cvox.ChromeVox.navigationManager.stopReading(true);
768    } else if (cmd != 'readFromHere') {
769      cvox.ChromeVox.navigationManager.skip();
770    }
771  } else {
772    if (cmdStruct.announce) {
773      cvox.ChromeVox.navigationManager.finishNavCommand(prefixMsg);
774    }
775  }
776  if (!cmdStruct.allowEvents) {
777    cvox.ChromeVoxEventSuspender.exitSuspendEvents();
778  }
779  return !!cmdStruct.doDefault || ret;
780};
781
782
783/**
784 * Default handler for public user commands that are dispatched to the web app
785 * first so that the web developer can handle these commands instead of
786 * ChromeVox if they decide they can do a better job than the default algorithm.
787 *
788 * @param {Object} cvoxUserEvent The cvoxUserEvent to handle.
789 */
790cvox.ChromeVoxUserCommands.handleChromeVoxUserEvent = function(cvoxUserEvent) {
791  var detail = new cvox.UserEventDetail(cvoxUserEvent.detail);
792  if (detail.command) {
793    cvox.ChromeVoxUserCommands.doCommand_(
794        cvox.ChromeVoxUserCommands.lookupCommand_(detail.command, detail));
795  }
796};
797
798
799/**
800 * Returns an object containing information about the given command.
801 * @param {string} cmd The name of the command.
802 * @param {Object=} opt_kwargs Optional key values to add to the command
803 * structure.
804 * @return {Object} A key value mapping.
805 * @private
806 */
807cvox.ChromeVoxUserCommands.lookupCommand_ = function(cmd, opt_kwargs) {
808  var cmdStruct = cvox.CommandStore.CMD_WHITELIST[cmd];
809  if (!cmdStruct) {
810    throw 'Invalid command: ' + cmd;
811  }
812  cmdStruct = goog.object.clone(cmdStruct);
813  cmdStruct.command = cmd;
814  if (opt_kwargs) {
815    goog.object.extend(cmdStruct, opt_kwargs);
816  }
817  return cmdStruct;
818};
819
820
821cvox.ChromeVoxUserCommands.init_();
822