| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview Class to manage the ChromeVox menus. |
| */ |
| import {StringUtil} from '/common/string_util.js'; |
| import {TestImportManager} from '/common/testing/test_import_manager.js'; |
| |
| import {BackgroundBridge} from '../common/background_bridge.js'; |
| import {BrailleCommandData} from '../common/braille/braille_command_data.js'; |
| import {Command, CommandCategory} from '../common/command.js'; |
| import {CommandStore} from '../common/command_store.js'; |
| import {EventSourceType} from '../common/event_source_type.js'; |
| import {GestureCommandData} from '../common/gesture_command_data.js'; |
| import {KeyMap} from '../common/key_map.js'; |
| import {KeyBinding} from '../common/key_sequence.js'; |
| import {KeyUtil} from '../common/key_util.js'; |
| import {Msgs} from '../common/msgs.js'; |
| import {ALL_PANEL_MENU_NODE_DATA, PanelNodeMenuData, PanelNodeMenuId, PanelNodeMenuItemData} from '../common/panel_menu_data.js'; |
| |
| import {PanelInterface} from './panel_interface.js'; |
| import {PanelMenu, PanelNodeMenu, PanelSearchMenu} from './panel_menu.js'; |
| import {PanelMode} from './panel_mode.js'; |
| |
| const $ = (id: string): HTMLElement | null => document.getElementById(id); |
| |
| interface TouchMenuData { |
| titleText: string; |
| gestureText: string; |
| command: Command; |
| } |
| |
| export class MenuManager { |
| private activeMenu_: PanelMenu | null = null; |
| private lastMenu_ = ''; |
| private menus_: PanelMenu[] = []; |
| private nodeMenuDictionary_: |
| Partial<Record<PanelNodeMenuId, PanelNodeMenu>> = {}; |
| private searchMenu_: PanelSearchMenu | null = null; |
| |
| static disableMissingMsgsErrorsForTesting = false; |
| |
| /** |
| * Activate a menu, which implies hiding the previous active menu. |
| * @param menu The new menu to activate. |
| * @param activateFirstItem Whether or not we should activate the |
| * menu's first item. |
| */ |
| activateMenu(menu: PanelMenu | null, activateFirstItem: boolean): void { |
| if (menu === this.activeMenu_) { |
| return; |
| } |
| |
| if (this.activeMenu_) { |
| this.activeMenu_.deactivate(); |
| this.activeMenu_ = null; |
| } |
| |
| this.activeMenu_ = menu; |
| // TODO(b/314203187): Not null asserted, check that this is correct. |
| PanelInterface.instance!.setPendingCallback(null); |
| |
| if (this.activeMenu_) { |
| this.activeMenu_.activate(activateFirstItem); |
| } |
| } |
| |
| async addActionsMenuItems( |
| actionsMenu: PanelMenu, |
| bindingMap: Map<Command, KeyBinding>): Promise<void> { |
| const actions = |
| await BackgroundBridge.PanelBackground.getActionsForCurrentNode(); |
| for (const standardAction of actions.standardActions) { |
| const actionMsg = ACTION_TO_MSG_ID[standardAction]; |
| if (!actionMsg) { |
| continue; |
| } |
| const commandName = CommandStore.commandForMessage(actionMsg); |
| let shortcutName = ''; |
| if (commandName) { |
| const commandBinding = bindingMap.get(commandName); |
| shortcutName = commandBinding ? commandBinding.keySeq as string : ''; |
| } |
| const actionDesc = Msgs.getMsg(actionMsg); |
| actionsMenu.addMenuItem( |
| actionDesc, shortcutName, '' /* menuItemBraille */, '' /* gesture */, |
| () => BackgroundBridge.PanelBackground |
| .performStandardActionOnCurrentNode(standardAction)); |
| } |
| |
| for (const customAction of actions.customActions) { |
| actionsMenu.addMenuItem( |
| customAction.description, '' /* menuItemShortcut */, |
| '' /* menuItemBraille */, '' /* gesture */, |
| () => |
| BackgroundBridge.PanelBackground.performCustomActionOnCurrentNode( |
| customAction.id)); |
| } |
| } |
| |
| /** |
| * Create a new menu with the given name and add it to the menu bar. |
| * @param menuMsg The msg id of the new menu to add. |
| * @return The menu just created. |
| */ |
| addMenu(menuMsg: string): PanelMenu { |
| const menu = new PanelMenu(menuMsg); |
| $('menu-bar')!.appendChild(menu.menuBarItemElement); |
| menu.menuBarItemElement.addEventListener( |
| 'mouseover', |
| () => this.activateMenu(menu, true /* activateFirstItem */), false); |
| menu.menuBarItemElement.addEventListener( |
| 'mouseup', event => this.onMouseUpOnMenuTitle(menu, event), false); |
| $('menus_background')!.appendChild(menu.menuContainerElement); |
| this.menus_.push(menu); |
| return menu; |
| } |
| |
| addMenuItemFromKeyBinding( |
| binding: KeyBinding, menu: PanelMenu | null, |
| isTouchScreen: boolean): void { |
| if (!binding.title || !menu) { |
| return; |
| } |
| |
| let keyText; |
| let brailleText; |
| let gestureText; |
| if (isTouchScreen) { |
| const gestureData = Object.values(GestureCommandData.GESTURE_COMMAND_MAP); |
| const data = gestureData.find(data => data.command === binding.command); |
| if (data) { |
| gestureText = Msgs.getMsg(data.msgId); |
| } |
| } else { |
| keyText = binding.keySeq; |
| brailleText = BrailleCommandData.getDotShortcut(binding.command, true); |
| } |
| |
| menu.addMenuItem( |
| binding.title, keyText, brailleText, gestureText, |
| () => BackgroundBridge.CommandHandler.onCommand(binding.command), |
| binding.command); |
| } |
| |
| /** |
| * Create a new node menu with the given name and add it to the menu bar. |
| * @param menuData The title/predicate for the new menu. |
| */ |
| addNodeMenu(menuData: PanelNodeMenuData): void { |
| const menu = new PanelNodeMenu(menuData.titleId); |
| $('menu-bar')!.appendChild(menu.menuBarItemElement); |
| menu.menuBarItemElement.addEventListener( |
| 'mouseover', |
| () => this.activateMenu(menu, true /* activateFirstItem */)); |
| menu.menuBarItemElement.addEventListener( |
| 'mouseup', event => this.onMouseUpOnMenuTitle(menu, event)); |
| $('menus_background')!.appendChild(menu.menuContainerElement); |
| this.menus_.push(menu); |
| this.nodeMenuDictionary_[menuData.menuId] = menu; |
| } |
| |
| addNodeMenuItem(itemData: PanelNodeMenuItemData): void { |
| this.nodeMenuDictionary_[itemData.menuId]?.addItemFromData(itemData); |
| } |
| |
| /** |
| * Create a new search menu with the given name and add it to the menu bar. |
| * @param menuMsg The msg id of the new menu to add. |
| * @return The menu just created. |
| */ |
| addSearchMenu(menuMsg: string): PanelMenu { |
| this.searchMenu_ = new PanelSearchMenu(menuMsg); |
| // Add event listeners to search bar. |
| this.searchMenu_.searchBar.addEventListener( |
| 'input', |
| (event: Event) => this.onSearchBarQuery(event as InputEvent), false); |
| this.searchMenu_.searchBar.addEventListener('mouseup', event => { |
| // Clicking in the panel causes us to either activate an item or close the |
| // menus altogether. Prevent that from happening if we click the search |
| // bar. |
| event.preventDefault(); |
| event.stopPropagation(); |
| }, false); |
| |
| $('menu-bar')!.appendChild(this.searchMenu_.menuBarItemElement); |
| this.searchMenu_.menuBarItemElement.addEventListener( |
| 'mouseover', |
| () => |
| this.activateMenu(this.searchMenu_, false /* activateFirstItem */), |
| false); |
| this.searchMenu_.menuBarItemElement.addEventListener( |
| 'mouseup', event => this.onMouseUpOnMenuTitle(this.searchMenu_!, event), |
| false); |
| $('menus_background')!.appendChild(this.searchMenu_.menuContainerElement); |
| this.menus_.push(this.searchMenu_); |
| return this.searchMenu_; |
| } |
| |
| addTouchGestureMenuItems(touchMenu: PanelMenu): void { |
| const touchGestureItems: TouchMenuData[] = []; |
| for (const data of Object.values(GestureCommandData.GESTURE_COMMAND_MAP)) { |
| const command = data.command; |
| if (!command) { |
| continue; |
| } |
| |
| const gestureText = Msgs.getMsg(data.msgId); |
| const msgForCmd = data.commandDescriptionMsgId || |
| CommandStore.messageForCommand(command); |
| let titleText; |
| if (msgForCmd) { |
| titleText = Msgs.getMsg(msgForCmd); |
| } else { |
| console.error('No localization for: ' + command + ' (gesture)'); |
| titleText = ''; |
| } |
| touchGestureItems.push({titleText, gestureText, command}); |
| } |
| |
| touchGestureItems.sort( |
| (item1, item2) => item1.titleText.localeCompare(item2.titleText)); |
| |
| for (const item of touchGestureItems) { |
| touchMenu.addMenuItem( |
| item.titleText, '', '', item.gestureText, |
| () => BackgroundBridge.CommandHandler.onCommand(item.command), |
| item.command); |
| } |
| } |
| |
| /** |
| * Advance the index of the current active menu by |delta|. |
| * @param delta The number to add to the active menu index. |
| */ |
| advanceActiveMenuBy(delta: number): void { |
| let activeIndex = this.menus_.findIndex(menu => menu === this.activeMenu_); |
| |
| if (activeIndex >= 0) { |
| activeIndex += delta; |
| activeIndex = (activeIndex + this.menus_.length) % this.menus_.length; |
| } else { |
| if (delta >= 0) { |
| activeIndex = 0; |
| } else { |
| activeIndex = this.menus_.length - 1; |
| } |
| } |
| |
| activeIndex = this.findEnabledMenuIndex(activeIndex, delta > 0 ? 1 : -1); |
| if (activeIndex === -1) { |
| return; |
| } |
| |
| this.activateMenu(this.menus_[activeIndex], true /* activateFirstItem */); |
| } |
| |
| /** |
| * Advance the index of the current active menu item by |delta|. |
| * @param delta The number to add to the active menu item index. |
| */ |
| advanceItemBy(delta: number): void { |
| if (this.activeMenu_) { |
| this.activeMenu_.advanceItemBy(delta); |
| } |
| } |
| |
| /** |
| * Clear any previous menus. The menus are all regenerated each time the |
| * menus are opened. |
| */ |
| clearMenus(): void { |
| while (this.menus_.length) { |
| const menu = this.menus_.pop(); |
| $('menu-bar')!.removeChild(menu!.menuBarItemElement); |
| $('menus_background')!.removeChild(menu!.menuContainerElement); |
| |
| if (this.activeMenu_) { |
| this.lastMenu_ = this.activeMenu_.menuMsg; |
| } |
| this.activeMenu_ = null; |
| } |
| } |
| |
| /** Disables menu items that are prohibited without a signed-in user. */ |
| denySignedOut(): void { |
| for (const menu of this.menus_) { |
| for (const item of menu.items) { |
| // TODO(b/314203187): Not null asserted, check that this is correct. |
| if (CommandStore.denySignedOut(item.element!.id as Command)) { |
| item.disable(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Starting at |startIndex|, looks for an enabled menu. |
| * @return The index of the enabled menu. -1 if not found. |
| */ |
| findEnabledMenuIndex(startIndex: number, delta: number): number { |
| const endIndex = (delta > 0) ? this.menus_.length : -1; |
| while (startIndex !== endIndex) { |
| if (this.menus_[startIndex].enabled) { |
| return startIndex; |
| } |
| startIndex += delta; |
| } |
| return -1; |
| } |
| |
| /** |
| * Get the callback for whatever item is currently selected. |
| * @return The callback for the current item. |
| * |
| * TODO(b/267329383): Specify this as Promise<void> once PanelMenu |
| * is converted to typescript. |
| */ |
| getCallbackForCurrentItem(): (() => Promise<any>) | null{ |
| if (this.activeMenu_) { |
| return this.activeMenu_.getCallbackForCurrentItem(); |
| } |
| return null; |
| } |
| |
| getSelectedMenu(menuTitle?: string): PanelMenu { |
| const specifiedMenu = |
| this.menus_.find(menu => menu.menuMsg === menuTitle); |
| return specifiedMenu || this.searchMenu_ || this.menus_[0]; |
| } |
| |
| async getSortedKeyBindings(): Promise<KeyBinding[]> { |
| // TODO(accessibility): Commands should be based off of CommandStore and |
| // not the keymap. There are commands that don't have a key binding (e.g. |
| // commands for touch). |
| const keymap = KeyMap.get(); |
| |
| // A shallow copy of the bindings is returned, so re-ordering the elements |
| // does not change the original. |
| const sortedBindings = keymap.bindings(); |
| for (const binding of sortedBindings) { |
| const command = binding.command; |
| const keySeq = binding.sequence; |
| binding.keySeq = await KeyUtil.keySequenceToString(keySeq, true); |
| const titleMsgId = CommandStore.messageForCommand(command); |
| if (!titleMsgId) { |
| // Title messages are intentionally missing for some keyboard shortcuts. |
| if (!(command in COMMANDS_WITH_NO_MSG_ID) && |
| !MenuManager.disableMissingMsgsErrorsForTesting) { |
| console.error('No localization for: ' + command); |
| } |
| binding.title = ''; |
| continue; |
| } |
| const title = Msgs.getMsg(titleMsgId); |
| binding.title = StringUtil.toTitleCase(title); |
| } |
| sortedBindings.sort( |
| (binding1, binding2) => |
| binding1.title!.localeCompare(String(binding2.title))); |
| return sortedBindings; |
| } |
| |
| makeBindingMap(sortedBindings: KeyBinding[]): Map<Command, KeyBinding> { |
| const bindingMap = new Map(); |
| for (const binding of sortedBindings) { |
| bindingMap.set(binding.command, binding); |
| } |
| return bindingMap; |
| } |
| |
| makeCategoryMapping( |
| actionsMenu: PanelMenu, chromevoxMenu: PanelMenu, jumpMenu: PanelMenu, |
| speechMenu: PanelMenu): Record<CommandCategory, PanelMenu|null> { |
| return { |
| [CommandCategory.ACTIONS]: actionsMenu, |
| [CommandCategory.BRAILLE]: null, |
| [CommandCategory.CONTROLLING_SPEECH]: speechMenu, |
| [CommandCategory.DEVELOPER]: null, |
| [CommandCategory.HELP_COMMANDS]: chromevoxMenu, |
| [CommandCategory.INFORMATION]: speechMenu, |
| [CommandCategory.JUMP_COMMANDS]: jumpMenu, |
| [CommandCategory.MODIFIER_KEYS]: chromevoxMenu, |
| [CommandCategory.NAVIGATION]: jumpMenu, |
| [CommandCategory.NO_CATEGORY]: null, |
| [CommandCategory.OVERVIEW]: jumpMenu, |
| [CommandCategory.TABLES]: jumpMenu, |
| }; |
| } |
| |
| /** @return True if the event was handled. */ |
| onKeyDown(event: KeyboardEvent): boolean { |
| if (!this.activeMenu) { |
| return false; |
| } |
| |
| if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { |
| return false; |
| } |
| |
| // We need special logic for navigating the search bar. |
| // If left/right arrow are pressed, we should adjust the search bar's |
| // cursor. We only want to advance the active menu if we are at the |
| // beginning/end of the search bar's contents. |
| if (this.searchMenu_ && event.target === this.searchMenu_.searchBar) { |
| const input = event.target as HTMLInputElement; |
| switch (event.key) { |
| case 'ArrowLeft': |
| case 'ArrowRight': |
| if (input.value) { |
| // TODO(b/314203187): Not null asserted, check that this is correct. |
| const cursorIndex = |
| input.selectionStart! + (event.key === 'ArrowRight' ? 1 : -1); |
| const queryLength = input.value.length; |
| if (cursorIndex >= 0 && cursorIndex <= queryLength) { |
| return false; |
| } |
| } |
| break; |
| case ' ': |
| return false; |
| } |
| } |
| |
| switch (event.key) { |
| case 'ArrowLeft': |
| this.advanceActiveMenuBy(-1); |
| break; |
| case 'ArrowRight': |
| this.advanceActiveMenuBy(1); |
| break; |
| case 'ArrowUp': |
| this.advanceItemBy(-1); |
| break; |
| case 'ArrowDown': |
| this.advanceItemBy(1); |
| break; |
| case 'Escape': |
| // TODO(b/314203187): Not null asserted, check that this is correct. |
| PanelInterface.instance!.closeMenusAndRestoreFocus(); |
| break; |
| case 'PageUp': |
| this.advanceItemBy(10); |
| break; |
| case 'PageDown': |
| this.advanceItemBy(-10); |
| break; |
| case 'Home': |
| this.scrollToTop(); |
| break; |
| case 'End': |
| this.scrollToBottom(); |
| break; |
| case 'Enter': |
| case ' ': |
| if (!this.getCallbackForCurrentItem()) { |
| // If there's no callback for the current menu item, then we shouldn't |
| // perform any special logic. Return false here and let the key event |
| // propagate so that it can potentially be handled elsewhere. |
| return false; |
| } |
| |
| // TODO(b/314203187): Not null asserted, check that this is correct. |
| PanelInterface.instance!.setPendingCallback( |
| this.getCallbackForCurrentItem()); |
| PanelInterface.instance!.closeMenusAndRestoreFocus(); |
| break; |
| default: |
| // Don't mark this event as handled. |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Called when the user releases the mouse button. If it's anywhere other |
| * than on the menus button, close the menus and return focus to the page, |
| * and if the mouse was released over a menu item, execute that item's |
| * callback. |
| */ |
| onMouseUp(event: MouseEvent): void { |
| if (!this.activeMenu_) { |
| return; |
| } |
| |
| let target: HTMLElement|null = event.target as HTMLElement; |
| while (target && !target.classList.contains('menu-item')) { |
| // Allow the user to click and release on the menu button and leave |
| // the menu button. |
| if (target.id === 'menus_button') { |
| return; |
| } |
| |
| target = target.parentElement; |
| } |
| |
| // TODO(b/314203187): Not null asserted, check that this is correct. |
| if (target && this.activeMenu_) { |
| PanelInterface.instance!.setPendingCallback( |
| this.activeMenu_.getCallbackForElement(target)); |
| } |
| PanelInterface.instance!.closeMenusAndRestoreFocus(); |
| } |
| |
| /** |
| * Activate a menu whose title has been clicked. Stop event propagation at |
| * this point so we don't close the ChromeVox menus and restore focus. |
| * @param menu The menu we would like to activate. |
| * @param mouseUpEvent The mouseup event. |
| */ |
| onMouseUpOnMenuTitle(menu: PanelMenu, mouseUpEvent: MouseEvent): void { |
| this.activateMenu(menu, true /* activateFirstItem */); |
| mouseUpEvent.preventDefault(); |
| mouseUpEvent.stopPropagation(); |
| } |
| |
| /** |
| * Open / show the ChromeVox Menus. |
| * @param {Event=} event An optional event that triggered this. |
| * @param {string=} activateMenuTitle?: string Title msg id of menu to open. |
| */ |
| async onOpenMenus(event?: Event, activateMenuTitle?: string): Promise<void> { |
| // If the menu was already open, close it now and exit early. |
| // TODO(b/314203187): Not null asserted, check that this is correct. |
| if (PanelInterface.instance!.mode !== PanelMode.COLLAPSED) { |
| PanelInterface.instance!.setMode(PanelMode.COLLAPSED); |
| return; |
| } |
| |
| // Eat the event so that a mousedown isn't turned into a drag, allowing |
| // users to click-drag-release to select a menu item. |
| if (event) { |
| event.stopPropagation(); |
| event.preventDefault(); |
| } |
| |
| await BackgroundBridge.PanelBackground.saveCurrentNode(); |
| // TODO(b/314203187): Not null asserted, check that this is correct. |
| PanelInterface.instance!.setMode(PanelMode.FULLSCREEN_MENUS); |
| |
| // The panel does not get focus immediately when we request to be full |
| // screen (handled in ChromeVoxPanel natively on hash changed). Wait, if |
| // needed, for focus to begin initialization. |
| if (!document.hasFocus()) { |
| await waitForWindowFocus(); |
| } |
| |
| const eventSource = await BackgroundBridge.EventSource.get(); |
| const touchScreen = (eventSource === EventSourceType.TOUCH_GESTURE); |
| |
| // Build the top-level menus. |
| this.addSearchMenu('panel_search_menu'); |
| const jumpMenu = this.addMenu('panel_menu_jump'); |
| const speechMenu = this.addMenu('panel_menu_speech'); |
| const touchMenu = |
| touchScreen ? this.addMenu('panel_menu_touchgestures') : null; |
| const chromevoxMenu = this.addMenu('panel_menu_chromevox'); |
| const actionsMenu = this.addMenu('panel_menu_actions'); |
| |
| // Create a mapping between categories from CommandStore, and our |
| // top-level menus. Some categories aren't mapped to any menu. |
| const categoryToMenu = this.makeCategoryMapping( |
| actionsMenu, chromevoxMenu, jumpMenu, speechMenu); |
| |
| // Make a copy of the key bindings, get the localized title of each |
| // command, and then sort them. |
| const sortedBindings = await this.getSortedKeyBindings(); |
| |
| // Insert items from the bindings into the menus. |
| const bindingMap = this.makeBindingMap(sortedBindings); |
| for (const binding of bindingMap.values()) { |
| const category = CommandStore.categoryForCommand(binding.command); |
| const menu = category ? categoryToMenu[category] : null; |
| this.addMenuItemFromKeyBinding(binding, menu, touchScreen); |
| } |
| |
| // Add Touch Gestures menu items. |
| if (touchMenu) { |
| this.addTouchGestureMenuItems(touchMenu); |
| } |
| |
| // TODO(b/314203187): Not null asserted, check that this is correct. |
| if (PanelInterface.instance!.sessionState !== 'IN_SESSION') { |
| this.denySignedOut(); |
| } |
| |
| // Add a menu item that disables / closes ChromeVox. |
| // TODO(b/314203187): Not null asserted, check that this is correct. |
| chromevoxMenu.addMenuItem( |
| Msgs.getMsg('disable_chromevox'), 'Ctrl+Alt+Z', '', '', |
| async () => PanelInterface.instance!.onClose()); |
| |
| for (const menuData of ALL_PANEL_MENU_NODE_DATA) { |
| this.addNodeMenu(menuData); |
| } |
| await BackgroundBridge.PanelBackground.createAllNodeMenuBackgrounds( |
| activateMenuTitle); |
| |
| await this.addActionsMenuItems(actionsMenu, bindingMap); |
| |
| // Activate either the specified menu or the search menu. |
| const selectedMenu = this.getSelectedMenu(activateMenuTitle); |
| |
| const activateFirstItem = (selectedMenu !== this.searchMenu); |
| this.activateMenu(selectedMenu, activateFirstItem); |
| } |
| |
| /** |
| * Listens to changes in the menu search bar. Populates the search menu |
| * with items that match the search bar's contents. |
| * Note: we ignore PanelNodeMenu items and items without shortcuts. |
| * @param event The input event. |
| */ |
| onSearchBarQuery(event: InputEvent): void { |
| if (!this.searchMenu_) { |
| throw Error('MenuManager.searchMenu_ must be defined'); |
| } |
| const query = (event.target as HTMLInputElement).value.toLowerCase(); |
| this.searchMenu_.clear(); |
| // Show the search results menu. |
| this.activateMenu(this.searchMenu_, false /* activateFirstItem */); |
| // Populate. |
| if (query) { |
| for (const menu of this.menus_) { |
| if (menu === this.searchMenu_ || menu instanceof PanelNodeMenu) { |
| continue; |
| } |
| for (const item of menu.items) { |
| if (!item.menuItemShortcut) { |
| // Only add menu items that have shortcuts. |
| continue; |
| } |
| const itemText = item.text.toLowerCase(); |
| const match = itemText.includes(query) && |
| (itemText !== |
| Msgs.getMsg('panel_menu_item_none').toLowerCase()) && |
| item.enabled; |
| if (match) { |
| this.searchMenu_.copyAndAddMenuItem(item); |
| } |
| } |
| } |
| } |
| |
| if (this.searchMenu_.items.length === 0) { |
| this.searchMenu_.addMenuItem( |
| Msgs.getMsg( |
| 'panel_menu_item_none'), '', '', '', () => Promise.resolve()); |
| } |
| this.searchMenu_.activateItem(0); |
| } |
| |
| /** Sets the index of the current active menu to be the last index. */ |
| scrollToBottom(): void { |
| this.activeMenu_!.scrollToBottom(); |
| } |
| |
| /** Sets the index of the current active menu to be 0. */ |
| scrollToTop(): void { |
| this.activeMenu_!.scrollToTop(); |
| } |
| |
| // The following getters and setters are temporary during the migration from |
| // panel.js. |
| |
| get activeMenu(): PanelMenu | null { |
| return this.activeMenu_; |
| } |
| set activeMenu(menu: PanelMenu | null) { |
| this.activeMenu_ = menu; |
| } |
| |
| get lastMenu(): string { |
| return this.lastMenu_; |
| } |
| set lastMenu(menuMsg: string) { |
| this.lastMenu_ = menuMsg; |
| } |
| |
| get menus(): PanelMenu[] { |
| return this.menus_; |
| } |
| |
| get nodeMenuDictionary(): Partial<Record<PanelNodeMenuId, PanelNodeMenu>> { |
| return this.nodeMenuDictionary_; |
| } |
| |
| get searchMenu(): PanelSearchMenu | null { |
| return this.searchMenu_; |
| } |
| set searchMenu(menu: PanelSearchMenu | null) { |
| this.searchMenu_ = menu; |
| } |
| } |
| |
| // Local to module. |
| |
| const COMMANDS_WITH_NO_MSG_ID = [ |
| 'nativeNextCharacter', |
| 'nativePreviousCharacter', |
| 'nativeNextWord', |
| 'nativePreviousWord', |
| 'enableLogging', |
| 'disableLogging', |
| 'dumpTree', |
| 'showActionsMenu', |
| 'enableChromeVoxArcSupportForCurrentApp', |
| 'disableChromeVoxArcSupportForCurrentApp', |
| 'showTalkBackKeyboardShortcuts', |
| 'copy', |
| ]; |
| |
| const ACTION_TO_MSG_ID: Record<string, string> = { |
| decrement: 'action_decrement_description', |
| doDefault: 'perform_default_action', |
| increment: 'action_increment_description', |
| scrollBackward: 'action_scroll_backward_description', |
| scrollForward: 'action_scroll_forward_description', |
| showContextMenu: 'show_context_menu', |
| longClick: 'force_long_click_on_current_item', |
| }; |
| |
| async function waitForWindowFocus(): Promise<any> { |
| return new Promise( |
| resolve => window.addEventListener('focus', resolve, {once: true})); |
| } |
| |
| TestImportManager.exportForTesting(MenuManager); |