Commit 3af9a813 authored by Anastasia Helfinstein's avatar Anastasia Helfinstein Committed by Commit Bot

[Switch Access] Split MenuManager and ActionManager into separate classes

AX-Relnotes: n/a.
Bug: None
Change-Id: I2422e7911c4ac0633a5c7296286c0cbc384a51d4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2436628Reviewed-by: default avatarAkihiro Ota <akihiroota@chromium.org>
Commit-Queue: Anastasia Helfinstein <anastasi@google.com>
Cr-Commit-Position: refs/heads/master@{#813329}
parent 6b9eac03
......@@ -25,6 +25,7 @@ run_jsbundler("switch_access_copied_files") {
mode = "copy"
dest_dir = switch_access_dir
sources = [
"action_manager.js",
"auto_scan_manager.js",
"background.js",
"cache.js",
......@@ -130,6 +131,7 @@ js2gtest("switch_access_extjs_tests") {
js_type_check("closure_compile") {
deps = [
":action_manager",
":auto_scan_manager",
":back_button_node",
":background",
......@@ -168,6 +170,15 @@ js_type_check("closure_compile") {
]
}
js_library("action_manager") {
deps = [
":switch_access_constants",
":switch_access_node",
"../common:array_util",
]
externs_list = [ "$externs_path/automation.js" ]
}
js_library("auto_scan_manager") {
deps = [ ":switch_access_constants" ]
}
......@@ -210,8 +221,8 @@ js_library("combo_box_node") {
js_library("commands") {
deps = [
":action_manager",
":auto_scan_manager",
":menu_manager",
":navigation_manager",
]
externs_list = [ "$externs_path/accessibility_private.js" ]
......@@ -321,6 +332,7 @@ js_library("modal_dialog_node") {
js_library("navigation_manager") {
deps = [
":action_manager",
":basic_node",
":desktop_node",
":focus_ring_manager",
......
// Copyright 2020 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.
/**
* Class to handle performing actions with Switch Access, including determining
* which actions are available in the given context.
*/
class ActionManager {
/** @private */
constructor() {
/**
* The node on which actions are currently being performed.
* Null if the menu is closed.
* @private {SAChildNode}
*/
this.actionNode_;
/** @private {!Array<!SAConstants.MenuType>} */
this.menuStack_ = [];
}
static get instance() {
if (!ActionManager.instance_) {
ActionManager.instance_ = new ActionManager();
}
return ActionManager.instance_;
}
// ================= Static Methods ==================
/**
* Exits all of the open menus and unconditionally closes the menu window.
*/
static exitAllMenus() {
this.menuStack_ = [];
this.actionNode_ = null;
MenuManager.close();
}
/**
* Exits the current menu. If there are no menus on the stack, closes the
* menu.
*/
static exitCurrentMenu() {
ActionManager.instance.menuStack_.pop();
if (ActionManager.instance.menuStack_.length > 0) {
ActionManager.instance.openCurrentMenu_();
} else {
ActionManager.exitAllMenus();
}
}
/**
* Handles what to do when the user presses 'select'.
* If multiple actions are available for the currently highlighted node,
* opens the action menu. Otherwise performs the node's default action.
*/
static onSelect() {
const node = NavigationManager.currentNode;
if (node.actions.length <= 1 || !node.location) {
node.doDefaultAction();
return;
}
ActionManager.instance.menuStack_ = [];
ActionManager.instance.menuStack_.push(SAConstants.MenuType.MAIN_MENU);
ActionManager.instance.actionNode_ = node;
ActionManager.instance.openCurrentMenu_();
}
/**
* Given the action to be performed, appropriately handles performing it.
* @param {!SwitchAccessMenuAction} action
*/
static performAction(action) {
const manager = ActionManager.instance;
manager.handleGlobalActions_(action) ||
manager.performActionOnCurrentNode_(action);
}
/** Refreshes the current menu, if needed. */
static refreshMenu() {
if (!MenuManager.isMenuOpen()) {
return;
}
ActionManager.instance.openCurrentMenu_();
}
/**
* Refreshes the current menu, if the current action node matches the node
* provided.
* @param {!SAChildNode} node
*/
static refreshMenuForNode(node) {
if (node.equals(ActionManager.instance.actionNode_)) {
ActionManager.refreshMenu();
}
}
// ================= Private Methods ==================
/**
* Returns all possible actions for the provided menu type
* @param {!SAConstants.MenuType} type
* @return {!Array<!SwitchAccessMenuAction>}
* @private
*/
actionsForType_(type) {
switch (type) {
case SAConstants.MenuType.MAIN_MENU:
return [
SwitchAccessMenuAction.COPY,
SwitchAccessMenuAction.CUT,
SwitchAccessMenuAction.DECREMENT,
SwitchAccessMenuAction.DICTATION,
SwitchAccessMenuAction.INCREMENT,
SwitchAccessMenuAction.KEYBOARD,
SwitchAccessMenuAction.MOVE_CURSOR,
SwitchAccessMenuAction.PASTE,
SwitchAccessMenuAction.SCROLL_DOWN,
SwitchAccessMenuAction.SCROLL_LEFT,
SwitchAccessMenuAction.SCROLL_RIGHT,
SwitchAccessMenuAction.SCROLL_UP,
SwitchAccessMenuAction.SELECT,
SwitchAccessMenuAction.START_TEXT_SELECTION,
];
case SAConstants.MenuType.TEXT_NAVIGATION:
return [
SwitchAccessMenuAction.JUMP_TO_BEGINNING_OF_TEXT,
SwitchAccessMenuAction.JUMP_TO_END_OF_TEXT,
SwitchAccessMenuAction.MOVE_UP_ONE_LINE_OF_TEXT,
SwitchAccessMenuAction.MOVE_DOWN_ONE_LINE_OF_TEXT,
SwitchAccessMenuAction.MOVE_BACKWARD_ONE_WORD_OF_TEXT,
SwitchAccessMenuAction.MOVE_FORWARD_ONE_WORD_OF_TEXT,
SwitchAccessMenuAction.MOVE_BACKWARD_ONE_CHAR_OF_TEXT,
SwitchAccessMenuAction.MOVE_FORWARD_ONE_CHAR_OF_TEXT,
SwitchAccessMenuAction.END_TEXT_SELECTION
];
default:
return [];
}
}
/**
* @param {!Array<!SwitchAccessMenuAction>} actions
* @return {!Array<!SwitchAccessMenuAction>}
* @private
*/
addGlobalActions_(actions) {
actions.push(SwitchAccessMenuAction.SETTINGS);
return actions;
}
/**
* @return {!SAConstants.MenuType}
* @private
*/
get currentMenuType_() {
return this.menuStack_[this.menuStack_.length - 1];
}
/**
* @return {!Array<!SwitchAccessMenuAction>}
* @private
*/
getActionsForCurrentMenuAndNode_() {
if (!this.actionNode_ || !this.actionNode_.isValidAndVisible()) {
return [];
}
let actions = this.actionNode_.actions;
const possibleActions = this.actionsForType_(this.currentMenuType_);
actions.filter((a) => possibleActions.includes(a));
if (this.currentMenuType_ === SAConstants.MenuType.MAIN_MENU) {
actions = this.addGlobalActions_(actions);
}
return actions;
}
/**
* If the action is a global action, perform the action and return true.
* Otherwise return false.
* @param {!SwitchAccessMenuAction} action
* @return {boolean}
* @private
*/
handleGlobalActions_(action) {
switch (action) {
case SwitchAccessMenuAction.SETTINGS:
chrome.accessibilityPrivate.openSettingsSubpage(
'manageAccessibility/switchAccess');
return true;
default:
return false;
}
}
/** @private */
openCurrentMenu_() {
const actions = this.getActionsForCurrentMenuAndNode_();
if (actions.length < 2) {
ActionManager.exitCurrentMenu();
}
MenuManager.open(actions, this.actionNode_.location);
}
/**
* @param {!SwitchAccessMenuAction} action
* @private
*/
performActionOnCurrentNode_(action) {
if (!this.actionNode_.hasAction(action)) {
// Refresh the actions in the menu.
this.openCurrentMenu_();
return;
}
// We exit the menu before asking the node to perform the action, because
// having the menu on the group stack interferes with some actions. We do
// not close the menu bubble until we receive the ActionResponse CLOSE_MENU.
// If we receive a different response, we re-enter the menu.
NavigationManager.exitIfInGroup(MenuManager.menuAutomationNode);
const response = this.actionNode_.performAction(action);
if (response === SAConstants.ActionResponse.CLOSE_MENU) {
MenuManager.close();
} else {
NavigationManager.jumpToSwitchAccessMenu();
}
switch (response) {
case SAConstants.ActionResponse.RELOAD_MENU:
this.openCurrentMenu_();
break;
case SAConstants.ActionResponse.OPEN_TEXT_NAVIGATION_MENU:
if (SwitchAccess.instance.improvedTextInputEnabled()) {
this.menuStack_.push(SAConstants.MenuType.TEXT_NAVIGATION);
}
this.openCurrentMenu_();
}
}
}
......@@ -15,7 +15,7 @@ class Commands {
* @private {!Map<!SwitchAccessCommand, !function(): void>}
*/
this.commandMap_ = new Map([
[SwitchAccessCommand.SELECT, MenuManager.enter],
[SwitchAccessCommand.SELECT, ActionManager.onSelect],
[SwitchAccessCommand.NEXT, NavigationManager.moveForward],
[SwitchAccessCommand.PREVIOUS, NavigationManager.moveBackward]
]);
......
......@@ -3,19 +3,13 @@
// found in the LICENSE file.
/**
* Class to handle interactions with the Switch Access menu, including moving
* through and selecting actions.
* 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.
*/
class MenuManager {
/** @private */
constructor() {
/**
* The node that was focused when the menu was opened.
* Null if the menu is closed.
* @private {SAChildNode}
*/
this.actionNode_;
/** @private {?Array<!SwitchAccessMenuAction>} */
this.displayedActions_ = null;
......@@ -25,9 +19,6 @@ class MenuManager {
/** @private {boolean} */
this.isMenuOpen_ = false;
/** @private {boolean} */
this.inTextNavigation_ = false;
/** @private {AutomationNode} */
this.menuAutomationNode_;
......@@ -49,27 +40,26 @@ class MenuManager {
/**
* 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 enter() {
const node = NavigationManager.currentNode;
if (node.actions.length <= 1 || !node.location) {
node.doDefaultAction();
static open(actions, location) {
if (!MenuManager.instance.isMenuOpen_) {
if (!location) {
return;
}
MenuManager.instance.actionNode_ = node;
MenuManager.instance.openMainMenu_();
MenuManager.instance.displayedLocation_ = location;
}
/** Exits the menu. */
static exit() {
if (MenuManager.instance.inTextNavigation_) {
// If we're exiting the text navigation menu, we simply return to the
// main menu.
MenuManager.instance.openMainMenu_();
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;
......@@ -86,34 +76,13 @@ class MenuManager {
return MenuManager.instance.isMenuOpen_;
}
/** @param {!SAChildNode} node */
static reloadActionsForNode(node) {
if (!MenuManager.isMenuOpen() ||
!node.equals(MenuManager.instance.actionNode_)) {
return;
}
MenuManager.instance.refreshActions_();
}
static refreshMenu() {
if (!MenuManager.isMenuOpen()) {
return;
}
MenuManager.instance.refreshActions_();
/** @return {!AutomationNode} */
static get menuAutomationNode() {
return MenuManager.instance.menuAutomationNode_;
}
// ================= Private Methods ==================
/**
* @param {!Array<!SwitchAccessMenuAction>} actions
* @return {!Array<!SwitchAccessMenuAction>}
* @private
*/
addGlobalActions_(actions) {
actions.push(SwitchAccessMenuAction.SETTINGS);
return actions;
}
/**
* @param {string=} actionString
* @return {?SwitchAccessMenuAction}
......@@ -133,15 +102,13 @@ class MenuManager {
* @private
*/
displayMenuWithActions_(actions) {
const location = this.displayedLocation_ || this.actionNode_.location;
chrome.accessibilityPrivate.updateSwitchAccessBubble(
chrome.accessibilityPrivate.SwitchAccessBubble.MENU, true /* show */,
location, actions);
this.displayedLocation_, actions);
this.isMenuOpen_ = true;
this.findAndJumpToMenuAutomationNode_();
this.displayedActions_ = actions;
this.displayedLocation_ = location;
}
/**
......@@ -153,6 +120,7 @@ class MenuManager {
findAndJumpToMenuAutomationNode_() {
if (this.hasValidMenuAutomationNode_() && this.menuAutomationNode_) {
this.jumpToMenuAutomationNode_(this.menuAutomationNode_);
return;
}
SwitchAccess.findNodeMatching(
{
......@@ -162,24 +130,6 @@ class MenuManager {
this.jumpToMenuAutomationNode_.bind(this));
}
/**
* If the action is a global action, perform the action and return true.
* Otherwise return false.
* @param {!SwitchAccessMenuAction} action
* @return {boolean}
* @private
*/
handleGlobalActions_(action) {
switch (action) {
case SwitchAccessMenuAction.SETTINGS:
chrome.accessibilityPrivate.openSettingsSubpage(
'manageAccessibility/switchAccess');
return true;
default:
return false;
}
}
/** @private */
hasValidMenuAutomationNode_() {
return this.menuAutomationNode_ && this.menuAutomationNode_.role &&
......@@ -214,107 +164,20 @@ class MenuManager {
this.menuAutomationNode_ = node;
this.clickHandler_.setNodes(this.menuAutomationNode_);
this.clickHandler_.start();
NavigationManager.jumpToSwitchAccessMenu(this.menuAutomationNode_);
NavigationManager.jumpToSwitchAccessMenu();
}
/**
* Listener for when buttons are clicked. Identifies the action to perform
* and forwards the request to the current node.
* 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 ||
this.handleGlobalActions_(selectedAction)) {
return;
}
if (!this.actionNode_.hasAction(selectedAction)) {
this.refreshActions_();
return;
}
// We exit the menu before asking the node to perform the action, because
// having the menu on the group stack interferes with some actions. We do
// not close the menu bubble until we receive the ActionResponse CLOSE_MENU.
// If we receive a different response, we re-enter the menu.
NavigationManager.exitIfInGroup(this.menuAutomationNode_);
const response = this.actionNode_.performAction(selectedAction);
if (response === SAConstants.ActionResponse.CLOSE_MENU ||
!this.hasValidMenuAutomationNode_()) {
MenuManager.exit();
} else {
NavigationManager.jumpToSwitchAccessMenu(this.menuAutomationNode_);
}
switch (response) {
case SAConstants.ActionResponse.RELOAD_MAIN_MENU:
this.refreshActions_();
break;
case SAConstants.ActionResponse.OPEN_TEXT_NAVIGATION_MENU:
this.openTextNavigation_();
}
}
/** @private */
openMainMenu_() {
this.inTextNavigation_ = false;
let actions = this.actionNode_.actions;
actions = this.addGlobalActions_(actions);
actions = actions.filter((a) => !this.textNavigationActions_.includes(a));
if (ArrayUtil.contentsAreEqual(actions, this.displayedActions_)) {
return;
}
this.displayMenuWithActions_(actions);
}
/** @private */
openTextNavigation_() {
if (!SwitchAccess.instance.improvedTextInputEnabled()) {
this.openMainMenu_();
if (!this.isMenuOpen_ || !selectedAction) {
return;
}
this.inTextNavigation_ = true;
this.displayMenuWithActions_(this.textNavigationActions_);
}
/**
* Checks if we can still show a menu for the node, and if so, changes the
* actions displayed in the menu.
* @private
*/
refreshActions_() {
if (!this.actionNode_.isValidAndVisible() ||
this.actionNode_.actions.length <= 1) {
MenuManager.exit();
return;
}
this.openMainMenu_();
}
/**
* @return {!Array<!SwitchAccessMenuAction>}
* @private
*/
get textNavigationActions_() {
const actions = [
SwitchAccessMenuAction.JUMP_TO_BEGINNING_OF_TEXT,
SwitchAccessMenuAction.JUMP_TO_END_OF_TEXT,
SwitchAccessMenuAction.MOVE_UP_ONE_LINE_OF_TEXT,
SwitchAccessMenuAction.MOVE_DOWN_ONE_LINE_OF_TEXT,
SwitchAccessMenuAction.MOVE_BACKWARD_ONE_WORD_OF_TEXT,
SwitchAccessMenuAction.MOVE_FORWARD_ONE_WORD_OF_TEXT,
SwitchAccessMenuAction.MOVE_BACKWARD_ONE_CHAR_OF_TEXT,
SwitchAccessMenuAction.MOVE_FORWARD_ONE_CHAR_OF_TEXT,
];
if (SwitchAccess.instance.improvedTextInputEnabled() &&
TextNavigationManager.currentlySelecting()) {
actions.unshift(SwitchAccessMenuAction.END_TEXT_SELECTION);
}
return actions;
ActionManager.performAction(selectedAction);
}
}
......@@ -141,8 +141,9 @@ class NavigationManager {
NavigationManager.instance = new NavigationManager(desktop);
}
/** @param {AutomationNode} menuNode */
static jumpToSwitchAccessMenu(menuNode) {
/** Jumps into the Switch Access action menu. */
static jumpToSwitchAccessMenu() {
const menuNode = MenuManager.menuAutomationNode;
if (!menuNode) {
return;
}
......@@ -249,7 +250,7 @@ class NavigationManager {
}
// Make sure the menu isn't open.
MenuManager.exit();
ActionManager.exitAllMenus();
const child = navigator.group_.firstValidChild();
if (groupIsValid && child) {
......@@ -310,7 +311,7 @@ class NavigationManager {
FocusRingManager.setFocusedNode(this.node_);
}
this.group_.refresh();
MenuManager.refreshMenu();
ActionManager.refreshMenu();
}
/**
......@@ -392,7 +393,7 @@ class NavigationManager {
*/
jumpTo_(group, shouldExitMenu = true) {
if (shouldExitMenu) {
MenuManager.exit();
ActionManager.exitAllMenus();
}
this.history_.save(new FocusData(this.group_, this.node_));
......@@ -409,7 +410,7 @@ class NavigationManager {
* @private
*/
moveTo_(automationNode) {
MenuManager.exit();
ActionManager.exitAllMenus();
if (this.history_.buildFromAutomationNode(automationNode)) {
this.restoreFromHistory_();
}
......
......@@ -151,7 +151,7 @@ class BackButtonNode extends SAChildNode {
*/
static onClick_() {
if (MenuManager.isMenuOpen()) {
MenuManager.exit();
ActionManager.exitCurrentMenu();
} else {
NavigationManager.exitGroupUnconditionally();
}
......
......@@ -153,25 +153,25 @@ class BasicNode extends SAChildNode {
if (ancestor.scrollable) {
ancestor.scrollDown();
}
return SAConstants.ActionResponse.RELOAD_MAIN_MENU;
return SAConstants.ActionResponse.RELOAD_MENU;
case SwitchAccessMenuAction.SCROLL_UP:
ancestor = this.getScrollableAncestor_();
if (ancestor.scrollable) {
ancestor.scrollUp();
}
return SAConstants.ActionResponse.RELOAD_MAIN_MENU;
return SAConstants.ActionResponse.RELOAD_MENU;
case SwitchAccessMenuAction.SCROLL_RIGHT:
ancestor = this.getScrollableAncestor_();
if (ancestor.scrollable) {
ancestor.scrollRight();
}
return SAConstants.ActionResponse.RELOAD_MAIN_MENU;
return SAConstants.ActionResponse.RELOAD_MENU;
case SwitchAccessMenuAction.SCROLL_LEFT:
ancestor = this.getScrollableAncestor_();
if (ancestor.scrollable) {
ancestor.scrollLeft();
}
return SAConstants.ActionResponse.RELOAD_MAIN_MENU;
return SAConstants.ActionResponse.RELOAD_MENU;
default:
if (Object.values(chrome.automation.ActionType).includes(action)) {
this.baseNode_.performStandardAction(
......
......@@ -100,7 +100,7 @@ class EditableTextNode extends BasicNode {
return SAConstants.ActionResponse.OPEN_TEXT_NAVIGATION_MENU;
case SwitchAccessMenuAction.END_TEXT_SELECTION:
TextNavigationManager.saveSelectEnd();
return SAConstants.ActionResponse.RELOAD_MAIN_MENU;
return SAConstants.ActionResponse.RELOAD_MENU;
case SwitchAccessMenuAction.JUMP_TO_BEGINNING_OF_TEXT:
TextNavigationManager.jumpToBeginning();
......
......@@ -45,7 +45,7 @@ const SAConstants = {
NO_ACTION_TAKEN: -1,
REMAIN_OPEN: 0,
CLOSE_MENU: 1,
RELOAD_MAIN_MENU: 2,
RELOAD_MENU: 2,
OPEN_TEXT_NAVIGATION_MENU: 3,
},
......@@ -77,13 +77,11 @@ const SAConstants = {
},
/**
* IDs of menus that can appear in the menu panel.
* This must be kept in sync with the div ID of each menu
* in menu_panel.html.
* @enum {string}
* The different types of menus and sub-menus that can be shown.
* @enum {number}
* @const
*/
MenuId: {MAIN: 'main_menu', TEXT_NAVIGATION: 'text_navigation_menu'},
MenuType: {MAIN_MENU: 0, TEXT_NAVIGATION: 1},
/**
* Preferences that are configurable in Switch Access.
......
......@@ -388,7 +388,7 @@ class TextNavigationManager {
this.clipboardHasData_ = true;
const node = NavigationManager.currentNode;
if (node.hasAction(SwitchAccessMenuAction.PASTE)) {
MenuManager.reloadActionsForNode(node);
ActionManager.refreshMenuForNode(node);
}
}
}
......
......@@ -24,6 +24,7 @@
"common/repeated_event_handler.js",
"common/repeated_tree_change_handler.js",
"common/tree_walker.js",
"switch_access/action_manager.js",
"switch_access/auto_scan_manager.js",
"switch_access/cache.js",
"switch_access/commands.js",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment