blob: a3f46aeb5544fe083ca810574d0cfd93ed9f242c [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 Calculates the menu items for the node menus in the ChromeVox
* panel.
*/
import {AutomationPredicate} from '../../../common/automation_predicate.js';
import {AutomationUtil} from '../../../common/automation_util.js';
import {CursorRange} from '../../../common/cursors/range.js';
import {AutomationTreeWalker} from '../../../common/tree_walker.js';
import {Msgs} from '../../common/msgs.js';
import {PanelBridge} from '../../common/panel_bridge.js';
import {PanelNodeMenuData, PanelNodeMenuId, PanelNodeMenuItemData} from '../../common/panel_menu_data.js';
import {ChromeVoxState} from '../chromevox_state.js';
import {Output} from '../output/output.js';
import {OutputEventType} from '../output/output_types.js';
const AutomationNode = chrome.automation.AutomationNode;
export class PanelNodeMenuBackground {
/**
* @param {!PanelNodeMenuData} menuData
* @param {AutomationNode} node ChromeVox's current position.
* @param {boolean} isActivated Whether the menu was explicitly activated.
* If false, the menu is populated asynchronously by posting a task
* after searching each chunk of nodes.
*/
constructor(menuData, node, isActivated) {
/** @private {AutomationNode} */
this.node_ = node;
/** @private {AutomationPredicate.Unary} */
this.pred_ = menuData.predicate;
/** @private {!PanelNodeMenuId} */
this.menuId_ = menuData.menuId;
/** @private {boolean} */
this.isActivated_ = isActivated;
/** @private {AutomationTreeWalker|undefined} */
this.walker_;
/** @private {number} */
this.nodeCount_ = 0;
/** @private {boolean} */
this.isEmpty_ = true;
}
/** @param {number} callbackNodeIndex */
static focusNodeCallback(callbackNodeIndex) {
if (!PanelNodeMenuBackground.callbackNodes_[callbackNodeIndex]) {
return;
}
ChromeVoxState.instance.navigateToRange(CursorRange.fromNode(
/** @type {!AutomationNode} */ (
PanelNodeMenuBackground.callbackNodes_[callbackNodeIndex])));
}
/**
* Create the AutomationTreeWalker and kick off the search to find
* nodes that match the predicate for this menu.
*/
populate() {
if (!this.node_) {
this.finish_();
return;
}
const root = AutomationUtil.getTopLevelRoot(this.node_);
if (!root) {
this.finish_();
return;
}
this.walker_ = new AutomationTreeWalker(root, constants.Dir.FORWARD, {
visit(node) {
return !AutomationPredicate.shouldIgnoreNode(node);
},
});
this.nodeCount_ = 0;
this.findMoreNodes_();
}
/**
* Iterate over nodes from the tree walker. If a node matches the
* predicate, add an item to the menu.
*
* Unless |this.isActivated_| is true, then after MAX_NODES_BEFORE_ASYNC nodes
* have been scanned, call setTimeout to defer searching. This frees up the
* main event loop to keep the panel menu responsive, otherwise it basically
* freezes up until all of the nodes have been found.
* @private
*/
findMoreNodes_() {
while (this.walker_.next().node) {
const node = this.walker_.node;
if (this.pred_(node)) {
this.isEmpty_ = false;
const output = new Output();
const range = CursorRange.fromNode(node);
output.withoutHints();
output.withSpeech(range, range, OutputEventType.NAVIGATE);
const title = output.toString();
const callbackNodeIndex = PanelNodeMenuBackground.callbackNodes_.length;
PanelNodeMenuBackground.callbackNodes_.push(node);
const isActive = node === this.node_ && this.isActivated_;
const menuId = this.menuId_;
this.addMenuItemFromData_({title, callbackNodeIndex, isActive, menuId});
}
if (!this.isActivated_) {
this.nodeCount_++;
if (this.nodeCount_ >= PanelNodeMenuBackground.MAX_NODES_BEFORE_ASYNC) {
this.nodeCount_ = 0;
setTimeout(this.findMoreNodes_.bind(this), 0);
return;
}
}
}
this.finish_();
}
/**
* Called when we've finished searching for nodes. If no matches were
* found, adds an item to the menu indicating none were found.
* @private
*/
finish_() {
if (this.isEmpty_) {
this.addMenuItemFromData_({
title: Msgs.getMsg('panel_menu_item_none'),
callbackNodeIndex: -1,
isActive: false,
menuId: this.menuId_,
});
}
}
/**
* @param {!PanelNodeMenuItemData} itemData
* @private
*/
async addMenuItemFromData_(itemData) {
await PanelBridge.addMenuItem(itemData);
}
}
/**
* The number of nodes to search before posting a task to finish
* searching.
* @const {number}
*/
PanelNodeMenuBackground.MAX_NODES_BEFORE_ASYNC = 100;
/**
* An array of nodes associated with a PanelNodeMenuItem, saved so we can
* set the current ChromeVox range when the user selects an item.
* @private {!Array<chrome.automation.AutomationNode>}
*/
PanelNodeMenuBackground.callbackNodes_ = [];