blob: a45b77cbc1f4853e30e2b284310dc5850b1f0d1e [file] [log] [blame]
// Copyright 2022 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 Handles logic for the ChromeVox panel that requires state from
* the background context.
*/
import {constants} from '../../../common/constants.js';
import {CursorRange} from '../../../common/cursors/range.js';
import {Earcon} from '../../common/abstract_earcons.js';
import {BridgeConstants} from '../../common/bridge_constants.js';
import {BridgeHelper} from '../../common/bridge_helper.js';
import {PanelBridge} from '../../common/panel_bridge.js';
import {ALL_PANEL_MENU_NODE_DATA} from '../../common/panel_menu_data.js';
import {QueueMode} from '../../common/tts_interface.js';
import {ChromeVox} from '../chromevox.js';
import {ChromeVoxState, ChromeVoxStateObserver} from '../chromevox_state.js';
import {Output} from '../output/output.js';
import {OutputEventType} from '../output/output_types.js';
import {ISearch} from './i_search.js';
import {ISearchHandler} from './i_search_handler.js';
import {PanelNodeMenuBackground} from './panel_node_menu_background.js';
import {PanelTabMenuBackground} from './panel_tab_menu_background.js';
const AutomationNode = chrome.automation.AutomationNode;
const Constants = BridgeConstants.PanelBackground;
/** @implements {ISearchHandler} */
export class PanelBackground {
/** @private */
constructor() {
/** @private {ISearch} */
this.iSearch_;
/** @private {AutomationNode} */
this.savedNode_;
}
static init() {
if (PanelBackground.instance) {
throw 'Trying to create two copies of singleton PanelBackground';
}
PanelBackground.instance = new PanelBackground();
PanelBackground.stateObserver_ = new PanelStateObserver();
ChromeVoxState.addObserver(PanelBackground.stateObserver_);
BridgeHelper.registerHandler(
Constants.TARGET, Constants.Action.CLEAR_SAVED_NODE,
() => PanelBackground.instance.clearSavedNode_());
BridgeHelper.registerHandler(
Constants.TARGET, Constants.Action.CREATE_ALL_NODE_MENU_BACKGROUNDS,
opt_activateMenuTitle =>
PanelBackground.instance.createAllNodeMenuBackgrounds_(
opt_activateMenuTitle));
BridgeHelper.registerHandler(
Constants.TARGET, Constants.Action.CREATE_NEW_I_SEARCH,
() => PanelBackground.instance.createNewISearch_());
BridgeHelper.registerHandler(
Constants.TARGET, Constants.Action.DESTROY_I_SEARCH,
() => PanelBackground.instance.destroyISearch_());
BridgeHelper.registerHandler(
Constants.TARGET, Constants.Action.FOCUS_TAB,
(windowId, tabId) => PanelTabMenuBackground.focusTab(windowId, tabId));
BridgeHelper.registerHandler(
Constants.TARGET, Constants.Action.GET_ACTIONS_FOR_CURRENT_NODE,
() => PanelBackground.instance.getActionsForCurrentNode_());
BridgeHelper.registerHandler(
Constants.TARGET, Constants.Action.GET_TAB_MENU_DATA,
() => PanelTabMenuBackground.getTabMenuData());
BridgeHelper.registerHandler(
Constants.TARGET, Constants.Action.INCREMENTAL_SEARCH,
(searchStr, dir, opt_nextObject) =>
PanelBackground.instance.incrementalSearch_(
searchStr, dir, opt_nextObject));
BridgeHelper.registerHandler(
Constants.TARGET, Constants.Action.NODE_MENU_CALLBACK,
callbackNodeIndex =>
PanelNodeMenuBackground.focusNodeCallback(callbackNodeIndex));
BridgeHelper.registerHandler(
Constants.TARGET,
Constants.Action.PERFORM_CUSTOM_ACTION_ON_CURRENT_NODE,
actionId => PanelBackground.instance.performCustomActionOnCurrentNode_(
actionId));
BridgeHelper.registerHandler(
Constants.TARGET,
Constants.Action.PERFORM_STANDARD_ACTION_ON_CURRENT_NODE,
action => PanelBackground.instance.performStandardActionOnCurrentNode_(
action));
BridgeHelper.registerHandler(
Constants.TARGET, Constants.Action.SAVE_CURRENT_NODE,
() => PanelBackground.instance.saveCurrentNode_());
BridgeHelper.registerHandler(
Constants.TARGET, Constants.Action.SET_RANGE_TO_I_SEARCH_NODE,
() => PanelBackground.instance.setRangeToISearchNode_());
BridgeHelper.registerHandler(
Constants.TARGET, Constants.Action.WAIT_FOR_PANEL_COLLAPSE,
() => PanelBackground.instance.waitForPanelCollapse_());
}
/** @private */
clearSavedNode_() {
this.savedNode_ = null;
}
/**
* @param {string=} opt_activateMenuTitleId Optional string specifying the
* activated menu.
* @private
*/
createAllNodeMenuBackgrounds_(opt_activateMenuTitleId) {
if (!this.savedNode_) {
return;
}
for (const data of ALL_PANEL_MENU_NODE_DATA) {
const isActivatedMenu = opt_activateMenuTitleId === data.titleId;
const menuBackground =
new PanelNodeMenuBackground(data, this.savedNode_, isActivatedMenu);
menuBackground.populate();
}
}
/**
* Creates a new ISearch object, ready to search starting from the current
* ChromeVox focus.
* @private
*/
createNewISearch_() {
if (this.iSearch_) {
this.iSearch_.clear();
}
// TODO(accessibility): not sure if this actually works anymore since all
// the refactoring.
if (!ChromeVoxState.instance.currentRange ||
!ChromeVoxState.instance.currentRange.start) {
return;
}
this.iSearch_ = new ISearch(ChromeVoxState.instance.currentRange.start);
this.iSearch_.handler = this;
}
/**
* Destroy the ISearch object so it can be garbage collected.
* @private
*/
destroyISearch_() {
this.iSearch_.handler = null;
this.iSearch_ = null;
}
/**
* @return {{
* standardActions: !Array<!chrome.automation.ActionType>,
* customActions: !Array<!chrome.automation.CustomAction>
* }}
* @private
*/
getActionsForCurrentNode_() {
const result = {
standardActions: [],
customActions: [],
};
if (!this.savedNode_) {
return result;
}
if (this.savedNode_.standardActions) {
result.standardActions = this.savedNode_.standardActions;
}
if (this.savedNode_.customActions) {
result.customActions = this.savedNode_.customActions;
}
return result;
}
/**
* @param {string} searchStr
* @param {constants.Dir} dir
* @param {boolean=} opt_nextObject
* @private
*/
incrementalSearch_(searchStr, dir, opt_nextObject) {
if (!this.iSearch_) {
console.error(
'Trying to incrementally search when no ISearch has been created');
return;
}
this.iSearch_.search(searchStr, dir, opt_nextObject);
}
/**
* @param {number} actionId
* @private
*/
performCustomActionOnCurrentNode_(actionId) {
if (this.savedNode_) {
this.savedNode_.performCustomAction(actionId);
}
}
/**
* @param {!chrome.automation.ActionType} action
* @private
*/
performStandardActionOnCurrentNode_(action) {
if (this.savedNode_) {
this.savedNode_.performStandardAction(action);
}
}
/**
* Sets the current ChromeVox focus to the current ISearch node.
* @private
*/
setRangeToISearchNode_() {
if (!this.iSearch_) {
console.error(
'Setting range to ISearch node when no ISearch in progress');
return;
}
const node = this.iSearch_.cursor.node;
if (!node) {
return;
}
ChromeVoxState.instance.navigateToRange(CursorRange.fromNode(node));
}
/** @override */
onSearchReachedBoundary(boundaryNode) {
this.iSearchOutput_(boundaryNode);
ChromeVox.earcons.playEarcon(Earcon.WRAP);
}
/** @override */
onSearchResultChanged(node, start, end) {
this.iSearchOutput_(node, start, end);
}
/**
* @param {!AutomationNode} node
* @param {number=} opt_start
* @param {number=} opt_end
* @private
*/
iSearchOutput_(node, opt_start, opt_end) {
Output.forceModeForNextSpeechUtterance(QueueMode.FLUSH);
const o = new Output();
if (opt_start && opt_end) {
o.withString([
node.name.substr(0, opt_start),
node.name.substr(opt_start, opt_end - opt_start),
node.name.substr(opt_end),
].join(', '));
o.format('$role', node);
} else {
o.withRichSpeechAndBraille(
CursorRange.fromNode(node), null, OutputEventType.NAVIGATE);
}
o.go();
ChromeVoxState.instance.setCurrentRange(CursorRange.fromNode(node));
}
/** @private */
saveCurrentNode_() {
if (ChromeVoxState.instance.currentRange) {
this.savedNode_ = ChromeVoxState.instance.currentRange.start.node;
}
}
/**
* Listens for focus events, and returns once the target is not the panel.
* @private
*/
async waitForPanelCollapse_() {
return new Promise(async resolve => {
const desktop =
await new Promise((resolve) => chrome.automation.getDesktop(resolve));
// Watch for a focus event outside the panel.
const onFocus = event => {
if (event.target.docUrl &&
event.target.docUrl.includes('chromevox/panel')) {
return;
}
desktop.removeEventListener(
chrome.automation.EventType.FOCUS, onFocus, true);
// Clears focus on the page by focusing the root explicitly. This makes
// sure we don't get future focus events as a result of giving this
// entire page focus (which would interfere with our desired range).
if (event.target.root) {
event.target.root.focus();
}
resolve();
};
desktop.addEventListener(
chrome.automation.EventType.FOCUS, onFocus, true);
});
}
}
/** @type {PanelBackground} */
PanelBackground.instance;
/** @private {PanelStateObserver} */
PanelBackground.stateObserver_;
/** @implements {ChromeVoxStateObserver} */
class PanelStateObserver {
/** @override */
onCurrentRangeChanged(range, opt_fromEditing) {
PanelBridge.onCurrentRangeChanged();
}
}