blob: c4c2073f1617de4aea2c8211d1b12f811e529540 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {ArrayUtil} from '../common/array_util.js';
import {EventHandler} from '../common/event_handler.js';
import {ActionManager} from './action_manager.js';
import {Navigator} from './navigator.js';
import {SwitchAccess} from './switch_access.js';
const AutomationNode = chrome.automation.AutomationNode;
const EventType = chrome.automation.EventType;
const MenuAction = chrome.accessibilityPrivate.SwitchAccessMenuAction;
const ScreenRect = chrome.accessibilityPrivate.ScreenRect;
const SwitchAccessBubble = chrome.accessibilityPrivate.SwitchAccessBubble;
/**
* Class to handle interactions with the Switch Access action menu, including
* opening and closing the menu and setting its location / the actions to be
* displayed.
*/
export class MenuManager {
/** @private */
constructor() {
/** @private {?Array<!MenuAction>} */
this.displayedActions_ = null;
/** @private {ScreenRect|undefined} */
this.displayedLocation_;
/** @private {boolean} */
this.isMenuOpen_ = false;
/** @private {AutomationNode} */
this.menuAutomationNode_;
/** @private {!EventHandler} */
this.clickHandler_ = new EventHandler(
[], EventType.CLICKED, event => this.onButtonClicked_(event));
}
static create() {
if (MenuManager.instance) {
throw new Error('Cannot instantiate more than one MenuManager');
}
MenuManager.instance = new MenuManager();
return MenuManager.instance;
}
// ================= Static Methods ==================
/** @return {boolean} */
static isMenuOpen() {
return Boolean(MenuManager.instance) && MenuManager.instance.isMenuOpen_;
}
/** @return {AutomationNode} */
static get menuAutomationNode() {
if (MenuManager.instance) {
return MenuManager.instance.menuAutomationNode_;
}
return null;
}
// ================ Instance Methods =================
/**
* If multiple actions are available for the currently highlighted node,
* opens the menu. Otherwise performs the node's default action.
* @param {!Array<!MenuAction>} actions
* @param {ScreenRect|undefined} location
*/
open(actions, location) {
if (!this.isMenuOpen_) {
if (!location) {
return;
}
this.displayedLocation_ = location;
}
if (ArrayUtil.contentsAreEqual(actions, this.displayedActions_)) {
return;
}
this.displayMenuWithActions_(actions);
}
/** Exits the menu. */
close() {
this.isMenuOpen_ = false;
this.displayedActions_ = null;
// To match the accessibilityPrivate function signature, displayedLocation_
// has to be undefined rather than null.
this.displayedLocation_ = undefined;
Navigator.byItem.exitIfInGroup(this.menuAutomationNode_);
this.menuAutomationNode_ = null;
chrome.accessibilityPrivate.updateSwitchAccessBubble(
SwitchAccessBubble.MENU, false /* show */);
}
// ================= Private Methods ==================
/**
* @param {string=} actionString
* @return {?MenuAction}
* @private
*/
asAction_(actionString) {
if (Object.values(MenuAction).includes(actionString)) {
return /** @type {!MenuAction} */ (actionString);
}
return null;
}
/**
* Opens or reloads the menu for the current action node with the specified
* actions.
* @param {!Array<!MenuAction>} actions
* @private
*/
displayMenuWithActions_(actions) {
chrome.accessibilityPrivate.updateSwitchAccessBubble(
SwitchAccessBubble.MENU, true /* show */, this.displayedLocation_,
actions);
this.isMenuOpen_ = true;
this.findAndJumpToMenu_();
this.displayedActions_ = actions;
}
/**
* Searches the automation tree to find the node for the Switch Access menu.
* If we've already found a node, and it's still valid, then jump to that
* node.
* @private
*/
findAndJumpToMenu_() {
if (this.hasMenuNode_() && this.menuAutomationNode_) {
this.jumpToMenu_(this.menuAutomationNode_);
return;
}
SwitchAccess.findNodeMatching(
{
role: chrome.automation.RoleType.MENU,
attributes: {className: 'SwitchAccessMenuView'},
},
node => this.jumpToMenu_(node));
}
/** @private */
hasMenuNode_() {
return this.menuAutomationNode_ && this.menuAutomationNode_.role &&
!this.menuAutomationNode_.state[chrome.automation.StateType.OFFSCREEN];
}
/**
* Saves the automation node representing the menu, adds all listeners, and
* jumps to the node.
* @param {!AutomationNode} node
* @private
*/
jumpToMenu_(node) {
if (!this.isMenuOpen_) {
return;
}
// If the menu hasn't fully loaded, wait for that before jumping.
if (node.children.length < 1 ||
node.firstChild.state[chrome.automation.StateType.OFFSCREEN]) {
new EventHandler(
node, [EventType.CHILDREN_CHANGED, EventType.LOCATION_CHANGED],
() => this.jumpToMenu_(node), {listenOnce: true})
.start();
return;
}
this.menuAutomationNode_ = node;
this.clickHandler_.setNodes(this.menuAutomationNode_);
this.clickHandler_.start();
Navigator.byItem.jumpTo(this.menuAutomationNode_);
}
/**
* Listener for when buttons are clicked. Identifies the action to perform
* and forwards the request to the action manager.
* @param {!chrome.automation.AutomationEvent} event
* @private
*/
onButtonClicked_(event) {
const selectedAction = this.asAction_(event.target.value);
if (!this.isMenuOpen_ || !selectedAction) {
return;
}
ActionManager.performAction(selectedAction);
}
}
/** @private */
MenuManager.instance;