blob: b3bad09cd62e3697e4bac108812a89d173fd6820 [file] [log] [blame]
// Copyright 2017 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.
import {Commands} from './commands.js';
import {Navigator} from './navigator.js';
import {KeyboardRootNode} from './nodes/keyboard_node.js';
import {PreferenceManager} from './preference_manager.js';
import {SAConstants} from './switch_access_constants.js';
const AutomationNode = chrome.automation.AutomationNode;
/**
* The top-level class for the Switch Access accessibility feature. Handles
* initialization and small matters that don't fit anywhere else in the
* codebase.
*/
export class SwitchAccess {
static initialize() {
SwitchAccess.instance = new SwitchAccess();
chrome.automation.getDesktop((desktop) => {
chrome.automation.getFocus(focus => {
// Focus is available. Finish init without waiting for further events.
// Disallow web view nodes, which indicate a root web area is still
// loading and pending focus.
if (focus && focus.role !== chrome.automation.RoleType.WEB_VIEW) {
SwitchAccess.finishInit_(desktop);
return;
}
// Wait for the focus to be sent. If |focus| was undefined, this is
// guaranteed. Otherwise, also set a timed callback to ensure we do
// eventually init.
let callbackId = 0;
const listener = maybeEvent => {
if (maybeEvent &&
maybeEvent.target.role === chrome.automation.RoleType.WEB_VIEW) {
return;
}
desktop.removeEventListener(
chrome.automation.EventType.FOCUS, listener, false);
window.clearTimeout(callbackId);
SwitchAccess.finishInit_(desktop);
};
desktop.addEventListener(
chrome.automation.EventType.FOCUS, listener, false);
callbackId = window.setTimeout(listener, 5000);
});
});
}
/** @private */
constructor() {
/**
* Feature flag controlling improvement of text input capabilities.
* @private {boolean}
*/
this.enableImprovedTextInput_ = false;
/** @private {boolean} */
this.enableMultistepAutomationFeatures_ = false;
chrome.commandLinePrivate.hasSwitch(
'enable-experimental-accessibility-switch-access-text', (result) => {
this.enableImprovedTextInput_ = result;
});
chrome.commandLinePrivate.hasSwitch(
'enable-experimental-accessibility-switch-access-multistep-automation',
(enabled) => {
this.enableMultistepAutomationFeatures_ = enabled;
});
/* @private {!SAConstants.Mode} */
this.mode_ = SAConstants.Mode.ITEM_SCAN;
}
/**
* Returns whether or not the feature flag
* for improved text input is enabled.
* @return {boolean}
*/
improvedTextInputEnabled() {
return this.enableImprovedTextInput_;
}
/** @return {boolean} */
multistepAutomationFeaturesEnabled() {
return this.enableMultistepAutomationFeatures_;
}
/** @return {!SAConstants.Mode} */
get mode() {
return this.mode_;
}
/** @param {!SAConstants.Mode} newMode */
set mode(newMode) {
this.mode_ = newMode;
}
/**
* Helper function to robustly find a node fitting a given FindParams, even if
* that node has not yet been created.
* Used to find the menu and back button.
* @param {!chrome.automation.FindParams} findParams
* @param {!function(!AutomationNode): void} foundCallback
*/
static findNodeMatching(findParams, foundCallback) {
const desktop = Navigator.byItem.desktopNode;
// First, check if the node is currently in the tree.
let node = desktop.find(findParams);
if (node) {
foundCallback(node);
return;
}
// If it's not currently in the tree, listen for changes to the desktop
// tree.
const eventHandler = new EventHandler(
desktop, chrome.automation.EventType.CHILDREN_CHANGED,
null /** callback */);
const onEvent = (event) => {
if (event.target.matches(findParams)) {
// If the event target is the node we're looking for, we've found it.
eventHandler.stop();
foundCallback(event.target);
} else if (event.target.children.length > 0) {
// Otherwise, see if one of its children is the node we're looking for.
node = event.target.find(findParams);
if (node) {
eventHandler.stop();
foundCallback(node);
}
}
};
eventHandler.setCallback(onEvent);
eventHandler.start();
}
/**
* Creates and records the specified error.
* @param {SAConstants.ErrorType} errorType
* @param {string} errorString
* @param {boolean} shouldRecover
* @return {!Error}
*/
static error(errorType, errorString, shouldRecover = false) {
if (shouldRecover) {
setTimeout(Navigator.byItem.moveToValidNode.bind(Navigator.byItem), 0);
}
const errorTypeCountForUMA = Object.keys(SAConstants.ErrorType).length;
chrome.metricsPrivate.recordEnumerationValue(
'Accessibility.CrosSwitchAccess.Error',
/** @type {number} */ (errorType), errorTypeCountForUMA);
return new Error(errorString);
}
/**
* @param {!chrome.automation.AutomationNode} desktop
* @private
*/
static finishInit_(desktop) {
// Navigator must be initialized first.
Navigator.initializeSingletonInstance(desktop);
Commands.initialize();
KeyboardRootNode.startWatchingVisibility();
PreferenceManager.initialize();
}
}