| // Copyright 2018 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 {ActionManager} from './action_manager.js'; |
| import {Navigator} from './navigator.js'; |
| import {SwitchAccess} from './switch_access.js'; |
| import {SwitchAccessMenuAction} from './switch_access_constants.js'; |
| |
| const AutomationNode = chrome.automation.AutomationNode; |
| |
| /** |
| * 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<!SwitchAccessMenuAction>} */ |
| this.displayedActions_ = null; |
| |
| /** @private {chrome.accessibilityPrivate.ScreenRect} */ |
| this.displayedLocation_; |
| |
| /** @private {boolean} */ |
| this.isMenuOpen_ = false; |
| |
| /** @private {AutomationNode} */ |
| this.menuAutomationNode_; |
| |
| /** @private {!EventHandler} */ |
| this.clickHandler_ = new EventHandler( |
| [], chrome.automation.EventType.CLICKED, |
| this.onButtonClicked_.bind(this)); |
| } |
| |
| static get instance() { |
| if (!MenuManager.instance_) { |
| MenuManager.instance_ = new MenuManager(); |
| } |
| return MenuManager.instance_; |
| } |
| |
| // ================= Static Methods ================== |
| |
| /** |
| * If multiple actions are available for the currently highlighted node, |
| * opens the menu. Otherwise performs the node's default action. |
| * @param {!Array<!SwitchAccessMenuAction>} actions |
| * @param {chrome.accessibilityPrivate.ScreenRect|undefined} location |
| */ |
| static open(actions, location) { |
| if (!MenuManager.instance.isMenuOpen_) { |
| if (!location) { |
| return; |
| } |
| MenuManager.instance.displayedLocation_ = location; |
| } |
| |
| if (ArrayUtil.contentsAreEqual( |
| actions, MenuManager.instance.displayedActions_)) { |
| return; |
| } |
| MenuManager.instance.displayMenuWithActions_(actions); |
| } |
| |
| /** Exits the menu. */ |
| static close() { |
| MenuManager.instance.isMenuOpen_ = false; |
| MenuManager.instance.actionNode_ = null; |
| MenuManager.instance.displayedActions_ = null; |
| MenuManager.instance.displayedLocation_ = null; |
| Navigator.instance.exitIfInGroup(MenuManager.instance.menuAutomationNode_); |
| MenuManager.instance.menuAutomationNode_ = null; |
| |
| chrome.accessibilityPrivate.updateSwitchAccessBubble( |
| chrome.accessibilityPrivate.SwitchAccessBubble.MENU, false /* show */); |
| } |
| |
| /** @return {boolean} */ |
| static isMenuOpen() { |
| return MenuManager.instance.isMenuOpen_; |
| } |
| |
| /** @return {!AutomationNode} */ |
| static get menuAutomationNode() { |
| return MenuManager.instance.menuAutomationNode_; |
| } |
| |
| // ================= Private Methods ================== |
| |
| /** |
| * @param {string=} actionString |
| * @return {?SwitchAccessMenuAction} |
| * @private |
| */ |
| asAction_(actionString) { |
| if (Object.values(SwitchAccessMenuAction).includes(actionString)) { |
| return /** @type {!SwitchAccessMenuAction} */ (actionString); |
| } |
| return null; |
| } |
| |
| /** |
| * Opens or reloads the menu for the current action node with the specified |
| * actions. |
| * @param {!Array<SwitchAccessMenuAction>} actions |
| * @private |
| */ |
| displayMenuWithActions_(actions) { |
| chrome.accessibilityPrivate.updateSwitchAccessBubble( |
| chrome.accessibilityPrivate.SwitchAccessBubble.MENU, true /* show */, |
| this.displayedLocation_, actions); |
| |
| this.isMenuOpen_ = true; |
| this.findAndJumpToMenuAutomationNode_(); |
| 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 |
| */ |
| findAndJumpToMenuAutomationNode_() { |
| if (this.hasValidMenuAutomationNode_() && this.menuAutomationNode_) { |
| this.jumpToMenuAutomationNode_(this.menuAutomationNode_); |
| return; |
| } |
| SwitchAccess.findNodeMatching( |
| { |
| role: chrome.automation.RoleType.MENU, |
| attributes: {className: 'SwitchAccessMenuView'} |
| }, |
| this.jumpToMenuAutomationNode_.bind(this)); |
| } |
| |
| /** @private */ |
| hasValidMenuAutomationNode_() { |
| 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 |
| */ |
| jumpToMenuAutomationNode_(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, |
| [ |
| chrome.automation.EventType.CHILDREN_CHANGED, |
| chrome.automation.EventType.LOCATION_CHANGED |
| ], |
| this.jumpToMenuAutomationNode_.bind(this, node), {listenOnce: true}) |
| .start(); |
| return; |
| } |
| |
| this.menuAutomationNode_ = node; |
| this.clickHandler_.setNodes(this.menuAutomationNode_); |
| this.clickHandler_.start(); |
| Navigator.instance.jumpToSwitchAccessMenu(); |
| } |
| |
| /** |
| * 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); |
| } |
| } |