blob: f1d7e4393e61d29423c31c8b4ac2bad1a3405f23 [file] [log] [blame]
// 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('Color');
goog.require('CustomAutomationEvent');
goog.require('LogStore');
goog.require('Output');
goog.require('PhoneticData');
goog.require('TreeDumper');
goog.require('cvox.ChromeVoxBackground');
goog.require('cvox.ChromeVoxKbHandler');
goog.require('cvox.ChromeVoxPrefs');
goog.require('cvox.CommandStore');
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;
var StateType = chrome.automation.StateType;
/** @private {boolean} */
CommandHandler.incognito_ = !!chrome.runtime.getManifest()['incognito'];
/**
* Handles ChromeVox Next commands.
* @param {string} command
* @return {boolean} True if the command should propagate.
*/
CommandHandler.onCommand = function(command) {
// Check for a command disallowed in OOBE/login.
if (CommandHandler.incognito_ && cvox.CommandStore.CMD_WHITELIST[command] &&
cvox.CommandStore.CMD_WHITELIST[command].disallowOOBE)
return true;
// 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.
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',
type: 'panel'
};
chrome.windows.create(explorerPage);
break;
case 'showLogPage':
var logPage = {url: 'cvox2/background/log.html'};
chrome.tabs.create(logPage);
break;
case 'enableLogging':
var prefs = new cvox.ChromeVoxPrefs();
for (var type in cvox.ChromeVoxPrefs.loggingPrefs) {
prefs.setLoggingPrefs(cvox.ChromeVoxPrefs.loggingPrefs[type], true);
}
break;
case 'disableLogging':
var prefs = new cvox.ChromeVoxPrefs();
for (var type in cvox.ChromeVoxPrefs.loggingPrefs) {
prefs.setLoggingPrefs(cvox.ChromeVoxPrefs.loggingPrefs[type], false);
}
break;
case 'dumpTree':
chrome.automation.getDesktop(function(root) {
LogStore.getInstance().writeTreeLog(new TreeDumper(root));
});
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['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 'toggleBrailleTable':
var brailleTableType = localStorage['brailleTableType'];
var output = '';
if (brailleTableType == 'brailleTable6') {
brailleTableType = 'brailleTable8';
// This label reads "switch to 8 dot braille".
output = '@OPTIONS_BRAILLE_TABLE_TYPE_6';
} else {
brailleTableType = 'brailleTable6';
// This label reads "switch to 6 dot braille".
output = '@OPTIONS_BRAILLE_TABLE_TYPE_8';
}
localStorage['brailleTable'] = localStorage[brailleTableType];
localStorage['brailleTableType'] = brailleTableType;
cvox.BrailleBackground.getInstance().getTranslatorManager().refresh(
localStorage[brailleTableType]);
new Output().format(output).go();
return false;
case 'help':
(new PanelCommand(PanelCommandType.TUTORIAL)).send();
return false;
case 'showNextUpdatePage':
(new PanelCommand(PanelCommandType.UPDATE_NOTES)).send();
localStorage['notifications_update_notification_shown'] = true;
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;
case 'enableChromeVoxArcSupportForCurrentApp':
chrome.accessibilityPrivate.setNativeChromeVoxArcSupportForCurrentApp(
true);
break;
case 'disableChromeVoxArcSupportForCurrentApp':
chrome.accessibilityPrivate.setNativeChromeVoxArcSupportForCurrentApp(
false);
break;
case 'showTtsSettings':
chrome.accessibilityPrivate.openSettingsSubpage(
'manageAccessibility/tts');
break;
default:
break;
}
// Require a current range.
if (!ChromeVoxState.instance.currentRange_)
return true;
var current = ChromeVoxState.instance.currentRange;
// If true, will check if the predicate matches the current node.
var matchCurrent = false;
// Allow edit commands first.
if (!CommandHandler.onEditCommand_(command))
return false;
var dir = Dir.FORWARD;
var pred = null;
var predErrorMsg = undefined;
var rootPred = AutomationPredicate.rootOrEditableRoot;
var shouldWrap = true;
var speechProps = {};
var skipSync = false;
var didNavigate = false;
var tryScrolling = true;
var skipSettingSelection = false;
switch (command) {
case 'nextCharacter':
didNavigate = true;
speechProps['phoneticCharacters'] = true;
current = current.move(cursors.Unit.CHARACTER, Dir.FORWARD);
break;
case 'previousCharacter':
dir = Dir.BACKWARD;
didNavigate = true;
speechProps['phoneticCharacters'] = true;
current = current.move(cursors.Unit.CHARACTER, dir);
break;
case 'nextWord':
didNavigate = true;
current = current.move(cursors.Unit.WORD, Dir.FORWARD);
break;
case 'previousWord':
dir = Dir.BACKWARD;
didNavigate = true;
current = current.move(cursors.Unit.WORD, dir);
break;
case 'forward':
case 'nextLine':
didNavigate = true;
current = current.move(cursors.Unit.LINE, Dir.FORWARD);
break;
case 'backward':
case 'previousLine':
dir = Dir.BACKWARD;
didNavigate = true;
current = current.move(cursors.Unit.LINE, dir);
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':
pred = AutomationPredicate.checkBox;
predErrorMsg = 'no_next_checkbox';
break;
case 'previousCheckbox':
dir = Dir.BACKWARD;
pred = AutomationPredicate.checkBox;
predErrorMsg = 'no_previous_checkbox';
break;
case 'nextComboBox':
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':
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':
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';
skipSettingSelection = true;
break;
case 'nextGraphic':
pred = AutomationPredicate.image;
predErrorMsg = 'no_next_graphic';
skipSettingSelection = true;
break;
case 'nextHeading':
pred = AutomationPredicate.heading;
predErrorMsg = 'no_next_heading';
break;
case 'nextHeading1':
pred = AutomationPredicate.makeHeadingPredicate(1);
predErrorMsg = 'no_next_heading_1';
break;
case 'nextHeading2':
pred = AutomationPredicate.makeHeadingPredicate(2);
predErrorMsg = 'no_next_heading_2';
break;
case 'nextHeading3':
pred = AutomationPredicate.makeHeadingPredicate(3);
predErrorMsg = 'no_next_heading_3';
break;
case 'nextHeading4':
pred = AutomationPredicate.makeHeadingPredicate(4);
predErrorMsg = 'no_next_heading_4';
break;
case 'nextHeading5':
pred = AutomationPredicate.makeHeadingPredicate(5);
predErrorMsg = 'no_next_heading_5';
break;
case 'nextHeading6':
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':
pred = AutomationPredicate.link;
predErrorMsg = 'no_next_link';
break;
case 'previousLink':
dir = Dir.BACKWARD;
pred = AutomationPredicate.link;
predErrorMsg = 'no_previous_link';
break;
case 'nextTable':
pred = AutomationPredicate.table;
predErrorMsg = 'no_next_table';
break;
case 'previousTable':
dir = Dir.BACKWARD;
pred = AutomationPredicate.table;
predErrorMsg = 'no_previous_table';
break;
case 'nextVisitedLink':
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':
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':
dir = Dir.BACKWARD;
didNavigate = true;
current = current.move(cursors.Unit.NODE, dir);
break;
case 'previousGroup':
skipSync = true;
dir = Dir.BACKWARD;
pred = AutomationPredicate.group;
break;
case 'nextGroup':
skipSync = true;
pred = AutomationPredicate.group;
break;
case 'previousSimilarItem':
dir = Dir.BACKWARD;
// Falls through.
case 'nextSimilarItem':
skipSync = true;
var node = current.start.node;
var originalNode = node;
// Scan upwards until we get a role we don't want to ignore.
while (node && AutomationPredicate.ignoreDuringJump(node))
node = node.parent;
var useNode = node || originalNode;
pred = AutomationPredicate.roles([node.role]);
break;
case 'jumpToTop':
var node = AutomationUtil.findNodePost(
current.start.node.root, Dir.FORWARD, AutomationPredicate.object);
if (node)
current = cursors.Range.fromNode(node);
tryScrolling = false;
break;
case 'jumpToBottom':
var node = AutomationUtil.findLastNode(
current.start.node.root, AutomationPredicate.object);
if (node)
current = cursors.Range.fromNode(node);
tryScrolling = false;
break;
case 'forceClickOnCurrentItem':
if (ChromeVoxState.instance.currentRange) {
var actionNode = ChromeVoxState.instance.currentRange.start.node;
// Scan for a clickable, which overrides the |actionNode|.
var clickable = actionNode;
while (clickable && !clickable.clickable)
clickable = clickable.parent;
if (clickable) {
clickable.doDefault();
return false;
}
if (EventSourceState.get() == EventSourceType.TOUCH_GESTURE &&
AutomationPredicate.editText(actionNode)) {
// Dispatch a click to ensure the VK gets shown.
var location = actionNode.location;
var event = {
type: chrome.accessibilityPrivate.SyntheticMouseEventType.PRESS,
x: location.left + Math.round(location.width / 2),
y: location.top + Math.round(location.height / 2)
};
chrome.accessibilityPrivate.sendSyntheticMouseEvent(event);
event.type =
chrome.accessibilityPrivate.SyntheticMouseEventType.RELEASE;
chrome.accessibilityPrivate.sendSyntheticMouseEvent(event);
return false;
}
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 'jumpToDetails':
var node = current.start.node;
while (node && !node.details)
node = node.parent;
if (node)
current = cursors.Range.fromNode(node.details);
break;
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 (AutomationPredicate.root(maybeDoc)) {
ChromeVoxState.isReadingContinuously = false;
return;
}
ChromeVoxState.instance.setCurrentRange(newRange);
newRange.select();
new Output()
.withoutHints()
.withRichSpeechAndBraille(
ChromeVoxState.instance.currentRange, prevRange,
Output.EventType.NAVIGATE)
.onSpeechEnd(continueReading)
.go();
}.bind(this);
var startNode = ChromeVoxState.instance.currentRange.start.node;
var collapsedRange = cursors.Range.fromNode(startNode);
new Output()
.withoutHints()
.withRichSpeechAndBraille(
collapsedRange, collapsedRange, 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 'showPanelMenuMostRecent':
(new PanelCommand(PanelCommandType.OPEN_MENUS_MOST_RECENT)).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)
return false;
if (target.root && target.root.role == RoleType.DESKTOP) {
// Search for the first container with a name.
while (target && (!target.name || !AutomationPredicate.root(target)))
target = target.parent;
} else {
// Search for a window with a title.
while (target && (!target.name || target.role != RoleType.WINDOW))
target = target.parent;
}
if (!target)
output.format('@no_title');
else
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 'toggleSelection':
if (!ChromeVoxState.instance.pageSel_) {
ChromeVoxState.instance.pageSel_ = ChromeVoxState.instance.currentRange;
DesktopAutomationHandler.instance.ignoreDocumentSelectionFromAction(
true);
} else {
var root = ChromeVoxState.instance.currentRange.start.node.root;
if (root && root.selectionStartObject && root.selectionEndObject) {
var sel = new cursors.Range(
new cursors.Cursor(
root.selectionStartObject, root.selectionStartOffset),
new cursors.Cursor(
root.selectionEndObject, root.selectionEndOffset));
var o = new Output()
.format('@end_selection')
.withSpeechAndBraille(sel, sel, Output.EventType.NAVIGATE)
.go();
DesktopAutomationHandler.instance.ignoreDocumentSelectionFromAction(
false);
}
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;
shouldWrap = false;
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;
shouldWrap = false;
break;
case 'nextRow':
var tableOpts = {row: true, dir: dir};
pred = AutomationPredicate.makeTableCellPredicate(
current.start.node, tableOpts);
predErrorMsg = 'no_cell_below';
rootPred = AutomationPredicate.table;
shouldWrap = false;
break;
case 'nextCol':
var tableOpts = {col: true, dir: dir};
pred = AutomationPredicate.makeTableCellPredicate(
current.start.node, tableOpts);
predErrorMsg = 'no_cell_right';
rootPred = AutomationPredicate.row;
shouldWrap = false;
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':
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;
shouldWrap = false;
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);
// Try to start on the last cell of the table and allow
// matching that node.
var startNode = node.lastChild;
while (startNode.lastChild &&
!AutomationPredicate.cellLike(startNode.role))
startNode = startNode.lastChild;
current = cursors.Range.fromNode(startNode);
matchCurrent = true;
// Should not be outputted.
predErrorMsg = 'no_cell_below';
rootPred = AutomationPredicate.table;
shouldWrap = false;
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;
case 'scrollBackward':
var node = current.start.node;
while (node &&
!node.standardActions.includes(
chrome.automation.ActionType.SCROLL_BACKWARD))
node = node.parent;
if (node)
node.scrollBackward();
break;
case 'scrollForward':
var node = current.start.node;
while (node &&
!node.standardActions.includes(
chrome.automation.ActionType.SCROLL_FORWARD))
node = node.parent;
if (node)
node.scrollForward();
break;
// These commands are only available when invoked from touch.
case 'nextAtGranularity':
case 'previousAtGranularity':
var backwards = command == 'previousAtGranularity';
switch (GestureCommandHandler.granularity) {
case GestureGranularity.CHARACTER:
command = backwards ? 'previousCharacter' : 'nextCharacter';
break;
case GestureGranularity.WORD:
command = backwards ? 'previousWord' : 'nextWord';
break;
case GestureGranularity.LINE:
command = backwards ? 'previousLine' : 'nextLine';
break;
}
CommandHandler.onCommand(command);
return false;
case 'nextGranularity':
case 'previousGranularity':
var backwards = command == 'previousGranularity';
var gran = GestureCommandHandler.granularity;
var next = backwards ?
(--gran >= 0 ? gran : GestureGranularity.COUNT - 1) :
++gran % GestureGranularity.COUNT;
GestureCommandHandler.granularity =
/** @type {GestureGranularity} */ (next);
var announce = '';
switch (GestureCommandHandler.granularity) {
case GestureGranularity.CHARACTER:
announce = Msgs.getMsg('character_granularity');
break;
case GestureGranularity.WORD:
announce = Msgs.getMsg('word_granularity');
break;
case GestureGranularity.LINE:
announce = Msgs.getMsg('line_granularity');
break;
}
cvox.ChromeVox.tts.speak(announce, cvox.QueueMode.FLUSH);
return false;
case 'announceBatteryDescription':
chrome.accessibilityPrivate.getBatteryDescription(function(
batteryDescription) {
new Output()
.withString(batteryDescription)
.withQueueMode(cvox.QueueMode.FLUSH)
.go();
});
break;
case 'announceRichTextDescription':
var node = ChromeVoxState.instance.currentRange.start.node;
var optSubs = [];
node.fontSize ? optSubs.push('font size: ' + node.fontSize) :
optSubs.push('');
node.color ? optSubs.push(Color.getColorDescription(node.color)) :
optSubs.push('');
node.bold ? optSubs.push(Msgs.getMsg('bold')) : optSubs.push('');
node.italic ? optSubs.push(Msgs.getMsg('italic')) : optSubs.push('');
node.underline ? optSubs.push(Msgs.getMsg('underline')) :
optSubs.push('');
node.lineThrough ? optSubs.push(Msgs.getMsg('linethrough')) :
optSubs.push('');
node.fontFamily ? optSubs.push('font family: ' + node.fontFamily) :
optSubs.push('');
var richTextDescription = Msgs.getMsg('rich_text_attributes', optSubs);
new Output()
.withString(richTextDescription)
.withQueueMode(cvox.QueueMode.CATEGORY_FLUSH)
.go();
return false;
case 'readPhoneticPronunciation':
// Get node info.
var node = ChromeVoxState.instance.currentRange.start.node;
var index = ChromeVoxState.instance.currentRange.start.index;
var text = node.name;
// If there is no text to speak, inform the user and return early.
if (!text) {
new Output()
.withString(Msgs.getMsg('empty_name'))
.withQueueMode(cvox.QueueMode.CATEGORY_FLUSH)
.go();
return false;
}
// Get word start and end indices.
var wordStarts, wordEnds;
if (node.role == RoleType.INLINE_TEXT_BOX) {
wordStarts = node.wordStarts;
wordEnds = node.wordEnds;
} else {
wordStarts = node.nonInlineTextWordStarts;
wordEnds = node.nonInlineTextWordEnds;
}
// Find the word we want to speak phonetically. If index === -1, then the
// index represents an entire node. If that is the case, we want to find
// the first word in the node's name. We do this by setting index to 0.
if (index === -1)
index = 0;
var word = '';
for (var z = 0; z < wordStarts.length; ++z) {
if (wordStarts[z] <= index && wordEnds[z] >= index) {
word = text.substring(wordStarts[z], wordEnds[z]);
break;
}
}
// Get unicode-aware array of characters.
var characterArray = [...word];
// We currently only load phonetic data for the browser UI language.
var language = chrome.i18n.getUILanguage();
for (var i = 0; i < characterArray.length; ++i) {
var character = characterArray[i];
var phoneticText =
PhoneticData.getPhoneticDisambiguation(language, character);
// Speak the character followed by its phonetic disambiguation, if it
// was found.
new Output()
.withString(character)
.withQueueMode(
i === 0 ? cvox.QueueMode.CATEGORY_FLUSH : cvox.QueueMode.QUEUE)
.go();
if (phoneticText) {
new Output()
.withString(phoneticText)
.withQueueMode(cvox.QueueMode.QUEUE)
.go();
}
}
return false;
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 = null;
if (matchCurrent && pred(bound))
node = bound;
if (!node) {
node = AutomationUtil.findNextNode(
bound, dir, pred, {skipInitialAncestry: true, root: rootPred});
}
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);
if (!shouldWrap) {
if (predErrorMsg) {
new Output()
.withString(Msgs.getMsg(predErrorMsg))
.withQueueMode(cvox.QueueMode.FLUSH)
.go();
}
return false;
}
var root = bound;
while (root && !AutomationPredicate.rootOrEditableRoot(root))
root = root.parent;
if (!root)
root = 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, root: rootPred});
if (node && !skipSync) {
node = AutomationUtil.findNodePre(
node, Dir.FORWARD, AutomationPredicate.object) ||
node;
}
if (node) {
current = cursors.Range.fromNode(node);
} else if (predErrorMsg) {
new Output()
.withString(Msgs.getMsg(predErrorMsg))
.withQueueMode(cvox.QueueMode.FLUSH)
.go();
return false;
}
}
}
}
if (tryScrolling && current && current.start && current.start.node &&
ChromeVoxState.instance.currentRange.start.node) {
var exited = AutomationUtil.getUniqueAncestors(
current.start.node, ChromeVoxState.instance.currentRange.start.node);
var scrollable = null;
for (var i = 0; i < exited.length; i++) {
if (AutomationPredicate.autoScrollable(exited[i])) {
scrollable = exited[i];
break;
}
}
// TODO(dtseng): handle more precise positioning after scroll e.g. list with
// 10 items shoing 1-7, scroll forward, should position at item 8.
if (scrollable) {
var callback = function(result) {
if (result) {
var innerCallback = function(currentNode, evt) {
scrollable.removeEventListener(
EventType.SCROLL_POSITION_CHANGED, innerCallback);
if (pred || (currentNode && currentNode.root)) {
// Jump or if there is a valid current range, then move from it
// since we have refreshed node data.
CommandHandler.onCommand(command);
return;
}
// Otherwise, sync to the directed deepest child.
var sync = scrollable;
if (dir == Dir.FORWARD) {
while (sync.firstChild)
sync = sync.firstChild;
} else {
while (sync.lastChild)
sync = sync.lastChild;
}
ChromeVoxState.instance.navigateToRange(
cursors.Range.fromNode(sync), false, speechProps);
}.bind(this, current.start.node);
scrollable.addEventListener(
EventType.SCROLL_POSITION_CHANGED, innerCallback, true);
} else {
ChromeVoxState.instance.navigateToRange(current, false, speechProps);
}
};
if (dir == Dir.FORWARD)
scrollable.scrollForward(callback);
else
scrollable.scrollBackward(callback);
return false;
}
}
if (current) {
ChromeVoxState.instance.navigateToRange(
current, undefined, speechProps, skipSettingSelection);
}
return false;
};
/**
* 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);
};
/**
* 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 {!cursors.Range} 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, CommandHandler.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);
}
};
/**
* Provides a partial mapping from ChromeVox key combinations to
* Search-as-a-function key as seen in Chrome OS documentation.
* @param {string} command
* @return {boolean} True if the command should propagate.
* @private
*/
CommandHandler.onEditCommand_ = function(command) {
var current = ChromeVoxState.instance.currentRange;
if (cvox.ChromeVox.isStickyModeOn() || !current || !current.start ||
!current.start.node || !current.start.node.state[StateType.EDITABLE])
return true;
var textEditHandler = DesktopAutomationHandler.instance.textEditHandler;
if (!textEditHandler)
return true;
// Skip customized keys for read only text fields.
if (textEditHandler.node.restriction ==
chrome.automation.Restriction.READ_ONLY)
return true;
// Skips customized keys if they get suppressed in speech.
if (AutomationPredicate.shouldOnlyOutputSelectionChangeInBraille(
textEditHandler.node))
return true;
var isMultiline = AutomationPredicate.multiline(current.start.node);
switch (command) {
case 'previousCharacter':
BackgroundKeyboardHandler.sendKeyPress(36, {shift: true});
break;
case 'nextCharacter':
BackgroundKeyboardHandler.sendKeyPress(35, {shift: true});
break;
case 'previousWord':
BackgroundKeyboardHandler.sendKeyPress(36, {shift: true, ctrl: true});
break;
case 'nextWord':
BackgroundKeyboardHandler.sendKeyPress(35, {shift: true, ctrl: true});
break;
case 'previousObject':
if (!isMultiline || textEditHandler.isSelectionOnFirstLine())
return true;
BackgroundKeyboardHandler.sendKeyPress(36);
break;
case 'nextObject':
if (!isMultiline)
return true;
if (textEditHandler.isSelectionOnLastLine()) {
textEditHandler.moveToAfterEditText();
return false;
}
BackgroundKeyboardHandler.sendKeyPress(35);
break;
case 'previousLine':
if (!isMultiline || textEditHandler.isSelectionOnFirstLine())
return true;
BackgroundKeyboardHandler.sendKeyPress(33);
break;
case 'nextLine':
if (!isMultiline)
return true;
if (textEditHandler.isSelectionOnLastLine()) {
textEditHandler.moveToAfterEditText();
return false;
}
BackgroundKeyboardHandler.sendKeyPress(34);
break;
case 'jumpToTop':
BackgroundKeyboardHandler.sendKeyPress(36, {ctrl: true});
break;
case 'jumpToBottom':
BackgroundKeyboardHandler.sendKeyPress(35, {ctrl: true});
break;
default:
return true;
}
return false;
};
/**
* Performs global initialization.
*/
CommandHandler.init = function() {
cvox.ChromeVoxKbHandler.commandHandler = CommandHandler.onCommand;
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);
});
}
});
};
}); // goog.scope