| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview ChromeVox commands. |
| */ |
| |
| goog.provide('CommandHandler'); |
| |
| goog.require('ChromeVoxState'); |
| goog.require('CustomAutomationEvent'); |
| goog.require('Output'); |
| goog.require('cvox.ChromeVoxBackground'); |
| |
| goog.scope(function() { |
| var AutomationEvent = chrome.automation.AutomationEvent; |
| var AutomationNode = chrome.automation.AutomationNode; |
| var Dir = constants.Dir; |
| var EventType = chrome.automation.EventType; |
| var RoleType = chrome.automation.RoleType; |
| |
| /** |
| * Handles ChromeVox Next commands. |
| * @param {string} command |
| * @return {boolean} True if the command should propagate. |
| */ |
| CommandHandler.onCommand = function(command) { |
| // Check for loss of focus which results in us invalidating our current |
| // range. Note this call is synchronis. |
| chrome.automation.getFocus(function(focusedNode) { |
| var cur = ChromeVoxState.instance.currentRange; |
| if (cur && !cur.isValid()) { |
| ChromeVoxState.instance.setCurrentRange( |
| cursors.Range.fromNode(focusedNode)); |
| } |
| |
| if (!focusedNode) |
| ChromeVoxState.instance.setCurrentRange(null); |
| }); |
| |
| // These commands don't require a current range and work in all modes. |
| switch (command) { |
| case 'speakTimeAndDate': |
| chrome.automation.getDesktop(function(d) { |
| // First, try speaking the on-screen time. |
| var allTime = d.findAll({role: RoleType.TIME}); |
| allTime.filter(function(t) { return t.root.role == RoleType.DESKTOP; }); |
| |
| var timeString = ''; |
| allTime.forEach(function(t) { |
| if (t.name) timeString = t.name; |
| }); |
| if (timeString) { |
| cvox.ChromeVox.tts.speak(timeString, cvox.QueueMode.FLUSH); |
| } else { |
| // Fallback to the old way of speaking time. |
| var output = new Output(); |
| var dateTime = new Date(); |
| output |
| .withString( |
| dateTime.toLocaleTimeString() + ', ' + |
| dateTime.toLocaleDateString()) |
| .go(); |
| } |
| }); |
| return false; |
| case 'showOptionsPage': |
| chrome.runtime.openOptionsPage(); |
| break; |
| case 'toggleChromeVox': |
| if (cvox.ChromeVox.isChromeOS) |
| return false; |
| |
| cvox.ChromeVox.isActive = !cvox.ChromeVox.isActive; |
| if (!cvox.ChromeVox.isActive) { |
| var msg = Msgs.getMsg('chromevox_inactive'); |
| cvox.ChromeVox.tts.speak(msg, cvox.QueueMode.FLUSH); |
| return false; |
| } |
| break; |
| case 'toggleStickyMode': |
| cvox.ChromeVoxBackground.setPref( |
| 'sticky', !cvox.ChromeVox.isStickyPrefOn, true); |
| |
| if (cvox.ChromeVox.isStickyPrefOn) |
| chrome.accessibilityPrivate.setKeyboardListener(true, true); |
| else |
| chrome.accessibilityPrivate.setKeyboardListener(true, false); |
| return false; |
| case 'passThroughMode': |
| cvox.ChromeVox.passThroughMode = true; |
| cvox.ChromeVox.tts.speak( |
| Msgs.getMsg('pass_through_key'), cvox.QueueMode.QUEUE); |
| return true; |
| case 'showKbExplorerPage': |
| var explorerPage = {url: 'chromevox/background/kbexplorer.html'}; |
| chrome.tabs.create(explorerPage); |
| break; |
| case 'decreaseTtsRate': |
| CommandHandler.increaseOrDecreaseSpeechProperty_( |
| cvox.AbstractTts.RATE, false); |
| return false; |
| case 'increaseTtsRate': |
| CommandHandler.increaseOrDecreaseSpeechProperty_( |
| cvox.AbstractTts.RATE, true); |
| return false; |
| case 'decreaseTtsPitch': |
| CommandHandler.increaseOrDecreaseSpeechProperty_( |
| cvox.AbstractTts.PITCH, false); |
| return false; |
| case 'increaseTtsPitch': |
| CommandHandler.increaseOrDecreaseSpeechProperty_( |
| cvox.AbstractTts.PITCH, true); |
| return false; |
| case 'decreaseTtsVolume': |
| CommandHandler.increaseOrDecreaseSpeechProperty_( |
| cvox.AbstractTts.VOLUME, false); |
| return false; |
| case 'increaseTtsVolume': |
| CommandHandler.increaseOrDecreaseSpeechProperty_( |
| cvox.AbstractTts.VOLUME, true); |
| return false; |
| case 'stopSpeech': |
| cvox.ChromeVox.tts.stop(); |
| ChromeVoxState.isReadingContinuously = false; |
| return false; |
| case 'toggleEarcons': |
| cvox.AbstractEarcons.enabled = !cvox.AbstractEarcons.enabled; |
| var announce = cvox.AbstractEarcons.enabled ? Msgs.getMsg('earcons_on') : |
| Msgs.getMsg('earcons_off'); |
| cvox.ChromeVox.tts.speak( |
| announce, cvox.QueueMode.FLUSH, |
| cvox.AbstractTts.PERSONALITY_ANNOTATION); |
| return false; |
| case 'cycleTypingEcho': |
| cvox.ChromeVox.typingEcho = |
| cvox.TypingEcho.cycle(cvox.ChromeVox.typingEcho); |
| var announce = ''; |
| switch (cvox.ChromeVox.typingEcho) { |
| case cvox.TypingEcho.CHARACTER: |
| announce = Msgs.getMsg('character_echo'); |
| break; |
| case cvox.TypingEcho.WORD: |
| announce = Msgs.getMsg('word_echo'); |
| break; |
| case cvox.TypingEcho.CHARACTER_AND_WORD: |
| announce = Msgs.getMsg('character_and_word_echo'); |
| break; |
| case cvox.TypingEcho.NONE: |
| announce = Msgs.getMsg('none_echo'); |
| break; |
| } |
| cvox.ChromeVox.tts.speak( |
| announce, cvox.QueueMode.FLUSH, |
| cvox.AbstractTts.PERSONALITY_ANNOTATION); |
| return false; |
| case 'cyclePunctuationEcho': |
| cvox.ChromeVox.tts.speak( |
| Msgs.getMsg(ChromeVoxState.backgroundTts.cyclePunctuationEcho()), |
| cvox.QueueMode.FLUSH); |
| return false; |
| case 'reportIssue': |
| var url = 'https://code.google.com/p/chromium/issues/entry?' + |
| 'labels=Type-Bug,Pri-2,cvox2,OS-Chrome&' + |
| 'components=UI>accessibility&' + |
| 'description='; |
| |
| var description = {}; |
| description['Mode'] = ChromeVoxState.instance.mode; |
| description['Version'] = chrome.app.getDetails().version; |
| description['Reproduction Steps'] = '%0a1.%0a2.%0a3.'; |
| for (var key in description) |
| url += key + ':%20' + description[key] + '%0a'; |
| chrome.tabs.create({url: url}); |
| return false; |
| case 'toggleBrailleCaptions': |
| cvox.BrailleCaptionsBackground.setActive( |
| !cvox.BrailleCaptionsBackground.isEnabled()); |
| return false; |
| case 'toggleChromeVoxVersion': |
| if (!ChromeVoxState.instance.toggleNext()) |
| return false; |
| if (ChromeVoxState.instance.currentRange) { |
| ChromeVoxState.instance.navigateToRange( |
| ChromeVoxState.instance.currentRange); |
| } |
| break; |
| case 'help': |
| (new PanelCommand(PanelCommandType.TUTORIAL)).send(); |
| return false; |
| case 'showNextUpdatePage': |
| (new PanelCommand(PanelCommandType.UPDATE_NOTES)).send(); |
| return false; |
| case 'darkenScreen': |
| chrome.accessibilityPrivate.darkenScreen(true); |
| new Output().format('@darken_screen').go(); |
| return false; |
| case 'undarkenScreen': |
| chrome.accessibilityPrivate.darkenScreen(false); |
| new Output().format('@undarken_screen').go(); |
| return false; |
| case 'toggleSpeechOnOrOff': |
| var state = cvox.ChromeVox.tts.toggleSpeechOnOrOff(); |
| new Output().format(state ? '@speech_on' : '@speech_off').go(); |
| return false; |
| default: |
| break; |
| } |
| |
| // Require a current range. |
| if (!ChromeVoxState.instance.currentRange_) |
| return true; |
| |
| // Next/classic compat commands hereafter. |
| if (ChromeVoxState.instance.mode == ChromeVoxMode.CLASSIC) |
| return true; |
| |
| var current = ChromeVoxState.instance.currentRange_; |
| var dir = Dir.FORWARD; |
| var pred = null; |
| var predErrorMsg = undefined; |
| var rootPred = AutomationPredicate.root; |
| var speechProps = {}; |
| var skipSync = false; |
| var didNavigate = false; |
| switch (command) { |
| case 'nextCharacter': |
| didNavigate = true; |
| speechProps['phoneticCharacters'] = true; |
| current = current.move(cursors.Unit.CHARACTER, Dir.FORWARD); |
| break; |
| case 'previousCharacter': |
| didNavigate = true; |
| speechProps['phoneticCharacters'] = true; |
| current = current.move(cursors.Unit.CHARACTER, Dir.BACKWARD); |
| break; |
| case 'nextWord': |
| didNavigate = true; |
| current = current.move(cursors.Unit.WORD, Dir.FORWARD); |
| break; |
| case 'previousWord': |
| didNavigate = true; |
| current = current.move(cursors.Unit.WORD, Dir.BACKWARD); |
| break; |
| case 'forward': |
| case 'nextLine': |
| didNavigate = true; |
| current = current.move(cursors.Unit.LINE, Dir.FORWARD); |
| break; |
| case 'backward': |
| case 'previousLine': |
| didNavigate = true; |
| current = current.move(cursors.Unit.LINE, Dir.BACKWARD); |
| break; |
| case 'nextButton': |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.button; |
| predErrorMsg = 'no_next_button'; |
| break; |
| case 'previousButton': |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.button; |
| predErrorMsg = 'no_previous_button'; |
| break; |
| case 'nextCheckbox': |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.checkBox; |
| predErrorMsg = 'no_next_checkbox'; |
| break; |
| case 'previousCheckbox': |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.checkBox; |
| predErrorMsg = 'no_previous_checkbox'; |
| break; |
| case 'nextComboBox': |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.comboBox; |
| predErrorMsg = 'no_next_combo_box'; |
| break; |
| case 'previousComboBox': |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.comboBox; |
| predErrorMsg = 'no_previous_combo_box'; |
| break; |
| case 'nextEditText': |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.editText; |
| predErrorMsg = 'no_next_edit_text'; |
| break; |
| case 'previousEditText': |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.editText; |
| predErrorMsg = 'no_previous_edit_text'; |
| break; |
| case 'nextFormField': |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.formField; |
| predErrorMsg = 'no_next_form_field'; |
| break; |
| case 'previousFormField': |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.formField; |
| predErrorMsg = 'no_previous_form_field'; |
| break; |
| case 'previousGraphic': |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.image; |
| predErrorMsg = 'no_previous_graphic'; |
| break; |
| case 'nextGraphic': |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.image; |
| predErrorMsg = 'no_next_graphic'; |
| break; |
| case 'nextHeading': |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.heading; |
| predErrorMsg = 'no_next_heading'; |
| break; |
| case 'nextHeading1': |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.makeHeadingPredicate(1); |
| predErrorMsg = 'no_next_heading_1'; |
| break; |
| case 'nextHeading2': |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.makeHeadingPredicate(2); |
| predErrorMsg = 'no_next_heading_2'; |
| break; |
| case 'nextHeading3': |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.makeHeadingPredicate(3); |
| predErrorMsg = 'no_next_heading_3'; |
| break; |
| case 'nextHeading4': |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.makeHeadingPredicate(4); |
| predErrorMsg = 'no_next_heading_4'; |
| break; |
| case 'nextHeading5': |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.makeHeadingPredicate(5); |
| predErrorMsg = 'no_next_heading_5'; |
| break; |
| case 'nextHeading6': |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.makeHeadingPredicate(6); |
| predErrorMsg = 'no_next_heading_6'; |
| break; |
| case 'previousHeading': |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.heading; |
| predErrorMsg = 'no_previous_heading'; |
| break; |
| case 'previousHeading1': |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.makeHeadingPredicate(1); |
| predErrorMsg = 'no_previous_heading_1'; |
| break; |
| case 'previousHeading2': |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.makeHeadingPredicate(2); |
| predErrorMsg = 'no_previous_heading_2'; |
| break; |
| case 'previousHeading3': |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.makeHeadingPredicate(3); |
| predErrorMsg = 'no_previous_heading_3'; |
| break; |
| case 'previousHeading4': |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.makeHeadingPredicate(4); |
| predErrorMsg = 'no_previous_heading_4'; |
| break; |
| case 'previousHeading5': |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.makeHeadingPredicate(5); |
| predErrorMsg = 'no_previous_heading_5'; |
| break; |
| case 'previousHeading6': |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.makeHeadingPredicate(6); |
| predErrorMsg = 'no_previous_heading_6'; |
| break; |
| case 'nextLink': |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.link; |
| predErrorMsg = 'no_next_link'; |
| break; |
| case 'previousLink': |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.link; |
| predErrorMsg = 'no_previous_link'; |
| break; |
| case 'nextTable': |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.table; |
| predErrorMsg = 'no_next_table'; |
| break; |
| case 'previousTable': |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.table; |
| predErrorMsg = 'no_previous_table'; |
| break; |
| case 'nextVisitedLink': |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.visitedLink; |
| predErrorMsg = 'no_next_visited_link'; |
| break; |
| case 'previousVisitedLink': |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.visitedLink; |
| predErrorMsg = 'no_previous_visited_link'; |
| break; |
| case 'nextLandmark': |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.landmark; |
| predErrorMsg = 'no_next_landmark'; |
| break; |
| case 'previousLandmark': |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.landmark; |
| predErrorMsg = 'no_previous_landmark'; |
| break; |
| case 'right': |
| case 'nextObject': |
| didNavigate = true; |
| current = current.move(cursors.Unit.NODE, Dir.FORWARD); |
| break; |
| case 'left': |
| case 'previousObject': |
| didNavigate = true; |
| current = current.move(cursors.Unit.NODE, Dir.BACKWARD); |
| break; |
| case 'previousGroup': |
| skipSync = true; |
| dir = Dir.BACKWARD; |
| pred = AutomationPredicate.group; |
| break; |
| case 'nextGroup': |
| skipSync = true; |
| dir = Dir.FORWARD; |
| pred = AutomationPredicate.group; |
| break; |
| case 'jumpToTop': |
| var node = AutomationUtil.findNodePost( |
| current.start.node.root, Dir.FORWARD, AutomationPredicate.leaf); |
| if (node) |
| current = cursors.Range.fromNode(node); |
| break; |
| case 'jumpToBottom': |
| var node = AutomationUtil.findNodePost( |
| current.start.node.root, Dir.BACKWARD, AutomationPredicate.leaf); |
| if (node) |
| current = cursors.Range.fromNode(node); |
| break; |
| case 'forceClickOnCurrentItem': |
| if (ChromeVoxState.instance.currentRange) { |
| var actionNode = ChromeVoxState.instance.currentRange.start.node; |
| while (actionNode.role == RoleType.INLINE_TEXT_BOX || |
| actionNode.role == RoleType.STATIC_TEXT) |
| actionNode = actionNode.parent; |
| if (actionNode.inPageLinkTarget) { |
| ChromeVoxState.instance.navigateToRange( |
| cursors.Range.fromNode(actionNode.inPageLinkTarget)); |
| } else { |
| actionNode.doDefault(); |
| } |
| } |
| // Skip all other processing; if focus changes, we should get an event |
| // for that. |
| return false; |
| case 'readFromHere': |
| ChromeVoxState.isReadingContinuously = true; |
| var continueReading = function() { |
| if (!ChromeVoxState.isReadingContinuously || |
| !ChromeVoxState.instance.currentRange_) |
| return; |
| |
| var prevRange = ChromeVoxState.instance.currentRange_; |
| var newRange = |
| ChromeVoxState.instance.currentRange_.move( |
| cursors.Unit.NODE, Dir.FORWARD); |
| |
| // Stop if we've wrapped back to the document. |
| var maybeDoc = newRange.start.node; |
| if (maybeDoc.role == RoleType.ROOT_WEB_AREA && |
| maybeDoc.parent.root.role == RoleType.DESKTOP) { |
| ChromeVoxState.isReadingContinuously = false; |
| return; |
| } |
| |
| ChromeVoxState.instance.setCurrentRange(newRange); |
| |
| new Output() |
| .withRichSpeechAndBraille(ChromeVoxState.instance.currentRange_, |
| prevRange, |
| Output.EventType.NAVIGATE) |
| .onSpeechEnd(continueReading) |
| .go(); |
| }.bind(this); |
| |
| new Output() |
| .withRichSpeechAndBraille(ChromeVoxState.instance.currentRange_, |
| null, |
| Output.EventType.NAVIGATE) |
| .onSpeechEnd(continueReading) |
| .go(); |
| |
| return false; |
| case 'contextMenu': |
| if (ChromeVoxState.instance.currentRange_) { |
| var actionNode = ChromeVoxState.instance.currentRange_.start.node; |
| if (actionNode.role == RoleType.INLINE_TEXT_BOX) |
| actionNode = actionNode.parent; |
| actionNode.showContextMenu(); |
| return false; |
| } |
| break; |
| case 'toggleKeyboardHelp': |
| (new PanelCommand(PanelCommandType.OPEN_MENUS)).send(); |
| return false; |
| case 'showHeadingsList': |
| (new PanelCommand(PanelCommandType.OPEN_MENUS, 'role_heading')).send(); |
| return false; |
| case 'showFormsList': |
| (new PanelCommand(PanelCommandType.OPEN_MENUS, 'role_form')).send(); |
| return false; |
| case 'showLandmarksList': |
| (new PanelCommand(PanelCommandType.OPEN_MENUS, 'role_landmark')).send(); |
| return false; |
| case 'showLinksList': |
| (new PanelCommand(PanelCommandType.OPEN_MENUS, 'role_link')).send(); |
| return false; |
| case 'showTablesList': |
| (new PanelCommand(PanelCommandType.OPEN_MENUS, 'table_strategy')).send(); |
| return false; |
| case 'toggleSearchWidget': |
| (new PanelCommand(PanelCommandType.SEARCH)).send(); |
| return false; |
| case 'readCurrentTitle': |
| var target = ChromeVoxState.instance.currentRange_.start.node; |
| var output = new Output(); |
| |
| if (target.root.role == RoleType.ROOT_WEB_AREA) { |
| // Web. |
| target = target.root; |
| output.withString(target.name || target.docUrl); |
| } else { |
| // Views. |
| while (target.role != RoleType.WINDOW) target = target.parent; |
| if (target) |
| output.withString(target.name || ''); |
| } |
| output.go(); |
| return false; |
| case 'readCurrentURL': |
| var output = new Output(); |
| var target = ChromeVoxState.instance.currentRange_.start.node.root; |
| output.withString(target.docUrl || '').go(); |
| return false; |
| case 'copy': |
| window.setTimeout(function() { |
| var textarea = document.createElement('textarea'); |
| document.body.appendChild(textarea); |
| textarea.focus(); |
| document.execCommand('paste'); |
| var clipboardContent = textarea.value; |
| textarea.remove(); |
| cvox.ChromeVox.tts.speak( |
| Msgs.getMsg('copy', [clipboardContent]), cvox.QueueMode.FLUSH); |
| ChromeVoxState.instance.pageSel_ = null; |
| }, 20); |
| return true; |
| case 'toggleSelection': |
| if (!ChromeVoxState.instance.pageSel_) { |
| ChromeVoxState.instance.pageSel_ = ChromeVoxState.instance.currentRange; |
| } else { |
| var root = ChromeVoxState.instance.currentRange_.start.node.root; |
| if (root && root.anchorObject && root.focusObject) { |
| var sel = new cursors.Range( |
| new cursors.Cursor(root.anchorObject, root.anchorOffset), |
| new cursors.Cursor(root.focusObject, root.focusOffset)); |
| var o = new Output() |
| .format('@end_selection') |
| .withSpeechAndBraille(sel, sel, Output.EventType.NAVIGATE) |
| .go(); |
| } |
| ChromeVoxState.instance.pageSel_ = null; |
| return false; |
| } |
| break; |
| case 'fullyDescribe': |
| var o = new Output(); |
| o.withContextFirst() |
| .withRichSpeechAndBraille(current, null, Output.EventType.NAVIGATE) |
| .go(); |
| return false; |
| case 'viewGraphicAsBraille': |
| CommandHandler.viewGraphicAsBraille_(current); |
| return false; |
| // Table commands. |
| case 'previousRow': |
| dir = Dir.BACKWARD; |
| var tableOpts = {row: true, dir: dir}; |
| pred = AutomationPredicate.makeTableCellPredicate( |
| current.start.node, tableOpts); |
| predErrorMsg = 'no_cell_above'; |
| rootPred = AutomationPredicate.table; |
| break; |
| case 'previousCol': |
| dir = Dir.BACKWARD; |
| var tableOpts = {col: true, dir: dir}; |
| pred = AutomationPredicate.makeTableCellPredicate( |
| current.start.node, tableOpts); |
| predErrorMsg = 'no_cell_left'; |
| rootPred = AutomationPredicate.row; |
| break; |
| case 'nextRow': |
| dir = Dir.FORWARD; |
| var tableOpts = {row: true, dir: dir}; |
| pred = AutomationPredicate.makeTableCellPredicate( |
| current.start.node, tableOpts); |
| predErrorMsg = 'no_cell_below'; |
| rootPred = AutomationPredicate.table; |
| break; |
| case 'nextCol': |
| dir = Dir.FORWARD; |
| var tableOpts = {col: true, dir: dir}; |
| pred = AutomationPredicate.makeTableCellPredicate( |
| current.start.node, tableOpts); |
| predErrorMsg = 'no_cell_right'; |
| rootPred = AutomationPredicate.row; |
| break; |
| case 'goToRowFirstCell': |
| case 'goToRowLastCell': |
| var node = current.start.node; |
| while (node && node.role != RoleType.ROW) |
| node = node.parent; |
| if (!node) |
| break; |
| var end = AutomationUtil.findNodePost(node, |
| command == 'goToRowLastCell' ? Dir.BACKWARD : Dir.FORWARD, |
| AutomationPredicate.leaf); |
| if (end) |
| current = cursors.Range.fromNode(end); |
| break; |
| case 'goToColFirstCell': |
| dir = Dir.FORWARD; |
| var node = current.start.node; |
| while (node && node.role != RoleType.TABLE) |
| node = node.parent; |
| if (!node || !node.firstChild) |
| return false; |
| var tableOpts = {col: true, dir: dir, end: true}; |
| pred = AutomationPredicate.makeTableCellPredicate( |
| current.start.node, tableOpts); |
| current = cursors.Range.fromNode(node.firstChild); |
| // Should not be outputted. |
| predErrorMsg = 'no_cell_above'; |
| rootPred = AutomationPredicate.table; |
| break; |
| case 'goToColLastCell': |
| dir = Dir.BACKWARD; |
| var node = current.start.node; |
| while (node && node.role != RoleType.TABLE) |
| node = node.parent; |
| if (!node || !node.lastChild) |
| return false; |
| var tableOpts = {col: true, dir: dir, end: true}; |
| pred = AutomationPredicate.makeTableCellPredicate( |
| current.start.node, tableOpts); |
| current = cursors.Range.fromNode(node.lastChild); |
| // Should not be outputted. |
| predErrorMsg = 'no_cell_below'; |
| rootPred = AutomationPredicate.table; |
| break; |
| case 'goToFirstCell': |
| case 'goToLastCell': |
| node = current.start.node; |
| while (node && node.role != RoleType.TABLE) |
| node = node.parent; |
| if (!node) |
| break; |
| var end = AutomationUtil.findNodePost(node, |
| command == 'goToLastCell' ? Dir.BACKWARD : Dir.FORWARD, |
| AutomationPredicate.leaf); |
| if (end) |
| current = cursors.Range.fromNode(end); |
| break; |
| default: |
| return true; |
| } |
| |
| if (didNavigate) |
| chrome.metricsPrivate.recordUserAction('Accessibility.ChromeVox.Navigate'); |
| |
| if (pred) { |
| chrome.metricsPrivate.recordUserAction('Accessibility.ChromeVox.Jump'); |
| |
| var bound = current.getBound(dir).node; |
| if (bound) { |
| var node = AutomationUtil.findNextNode( |
| bound, dir, pred, {skipInitialAncestry: true}); |
| |
| if (node && !skipSync) { |
| node = AutomationUtil.findNodePre( |
| node, Dir.FORWARD, AutomationPredicate.object) || |
| node; |
| } |
| |
| if (node) { |
| current = cursors.Range.fromNode(node); |
| } else { |
| cvox.ChromeVox.earcons.playEarcon(cvox.Earcon.WRAP); |
| var root = AutomationUtil.getTopLevelRoot(bound) || bound.root; |
| if (dir == Dir.FORWARD) { |
| bound = root; |
| } else { |
| bound = AutomationUtil.findNodePost( |
| root, dir, AutomationPredicate.leaf) || bound; |
| } |
| node = AutomationUtil.findNextNode( |
| bound, dir, pred, {skipInitialAncestry: true}); |
| |
| if (node && !skipSync) { |
| node = AutomationUtil.findNodePre( |
| node, Dir.FORWARD, AutomationPredicate.object) || node; |
| } |
| |
| if (node) { |
| current = cursors.Range.fromNode(node); |
| } else if (predErrorMsg) { |
| cvox.ChromeVox.tts.speak( |
| Msgs.getMsg(predErrorMsg), cvox.QueueMode.FLUSH); |
| return false; |
| } |
| } |
| } |
| } |
| |
| if (current) |
| ChromeVoxState.instance.navigateToRange(current, undefined, speechProps); |
| |
| return false; |
| }; |
| |
| /** |
| * React to mode changes. |
| * @param {ChromeVoxMode} newMode |
| * @param {ChromeVoxMode?} oldMode |
| */ |
| CommandHandler.onModeChanged = function(newMode, oldMode) { |
| // Previously uninitialized. |
| if (!oldMode) |
| cvox.ChromeVoxKbHandler.commandHandler = CommandHandler.onCommand; |
| |
| var hasListener = |
| chrome.commands.onCommand.hasListener(CommandHandler.onCommand); |
| if (newMode == ChromeVoxMode.CLASSIC && hasListener) |
| chrome.commands.onCommand.removeListener(CommandHandler.onCommand); |
| else if (newMode == ChromeVoxMode.CLASSIC && !hasListener) |
| chrome.commands.onCommand.addListener(CommandHandler.onCommand); |
| }; |
| |
| /** |
| * Increase or decrease a speech property and make an announcement. |
| * @param {string} propertyName The name of the property to change. |
| * @param {boolean} increase If true, increases the property value by one |
| * step size, otherwise decreases. |
| * @private |
| */ |
| CommandHandler.increaseOrDecreaseSpeechProperty_ = |
| function(propertyName, increase) { |
| cvox.ChromeVox.tts.increaseOrDecreaseProperty(propertyName, increase); |
| var announcement; |
| var valueAsPercent = Math.round( |
| cvox.ChromeVox.tts.propertyToPercentage(propertyName) * 100); |
| switch (propertyName) { |
| case cvox.AbstractTts.RATE: |
| announcement = Msgs.getMsg('announce_rate', [valueAsPercent]); |
| break; |
| case cvox.AbstractTts.PITCH: |
| announcement = Msgs.getMsg('announce_pitch', [valueAsPercent]); |
| break; |
| case cvox.AbstractTts.VOLUME: |
| announcement = Msgs.getMsg('announce_volume', [valueAsPercent]); |
| break; |
| } |
| if (announcement) { |
| cvox.ChromeVox.tts.speak( |
| announcement, cvox.QueueMode.FLUSH, |
| cvox.AbstractTts.PERSONALITY_ANNOTATION); |
| } |
| }; |
| |
| /** |
| * To support viewGraphicAsBraille_(), the current image node. |
| * @type {AutomationNode?}; |
| */ |
| CommandHandler.imageNode_; |
| |
| /** |
| * Called when an image frame is received on a node. |
| * @param {!(AutomationEvent|CustomAutomationEvent)} event The event. |
| * @private |
| */ |
| CommandHandler.onImageFrameUpdated_ = function(event) { |
| var target = event.target; |
| if (target != CommandHandler.imageNode_) |
| return; |
| |
| if (!AutomationUtil.isDescendantOf( |
| ChromeVoxState.instance.currentRange.start.node, |
| CommandHandler.imageNode_)) { |
| CommandHandler.imageNode_.removeEventListener( |
| EventType.IMAGE_FRAME_UPDATED, |
| CommandHandler.onImageFrameUpdated_, false); |
| CommandHandler.imageNode_ = null; |
| return; |
| } |
| |
| if (target.imageDataUrl) { |
| cvox.ChromeVox.braille.writeRawImage(target.imageDataUrl); |
| cvox.ChromeVox.braille.freeze(); |
| } |
| }; |
| |
| /** |
| * Handle the command to view the first graphic within the current range |
| * as braille. |
| * @param {!AutomationNode} current The current range. |
| * @private |
| */ |
| CommandHandler.viewGraphicAsBraille_ = function(current) { |
| if (CommandHandler.imageNode_) { |
| CommandHandler.imageNode_.removeEventListener( |
| EventType.IMAGE_FRAME_UPDATED, |
| CommandHandler.onImageFrameUpdated_, false); |
| CommandHandler.imageNode_ = null; |
| } |
| |
| // Find the first node within the current range that supports image data. |
| var imageNode = AutomationUtil.findNodePost( |
| current.start.node, Dir.FORWARD, |
| AutomationPredicate.supportsImageData); |
| if (!imageNode) |
| return; |
| |
| imageNode.addEventListener(EventType.IMAGE_FRAME_UPDATED, |
| this.onImageFrameUpdated_, false); |
| CommandHandler.imageNode_ = imageNode; |
| if (imageNode.imageDataUrl) { |
| var event = new CustomAutomationEvent( |
| EventType.IMAGE_FRAME_UPDATED, imageNode, 'page'); |
| CommandHandler.onImageFrameUpdated_(event); |
| } else { |
| imageNode.getImageData(0, 0); |
| } |
| }; |
| |
| /** |
| * Performs global initialization. |
| * @private |
| */ |
| CommandHandler.init_ = function() { |
| var firstRunId = 'jdgcneonijmofocbhmijhacgchbihela'; |
| chrome.runtime.onMessageExternal.addListener( |
| function(request, sender, sendResponse) { |
| if (sender.id != firstRunId) |
| return; |
| |
| if (request.openTutorial) { |
| var launchTutorial = function(desktop, evt) { |
| desktop.removeEventListener( |
| chrome.automation.EventType.FOCUS, launchTutorial, true); |
| CommandHandler.onCommand('help'); |
| }; |
| |
| // Since we get this command early on ChromeVox launch, the first run |
| // UI is not yet shown. Monitor for when first run gets focused, and |
| // show our tutorial. |
| chrome.automation.getDesktop(function(desktop) { |
| launchTutorial = launchTutorial.bind(this, desktop); |
| desktop.addEventListener( |
| chrome.automation.EventType.FOCUS, launchTutorial, true); |
| }); |
| } |
| }); |
| }; |
| |
| CommandHandler.init_(); |
| |
| }); // goog.scope |