Path: blob/main/src/vs/base/browser/ui/menu/menubar.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as browser from '../../browser.js';6import * as DOM from '../../dom.js';7import { StandardKeyboardEvent } from '../../keyboardEvent.js';8import { StandardMouseEvent } from '../../mouseEvent.js';9import { EventType, Gesture, GestureEvent } from '../../touch.js';10import { cleanMnemonic, HorizontalDirection, IMenuDirection, IMenuOptions, IMenuStyles, Menu, MENU_ESCAPED_MNEMONIC_REGEX, MENU_MNEMONIC_REGEX, VerticalDirection } from './menu.js';11import { ActionRunner, IAction, IActionRunner, Separator, SubmenuAction } from '../../../common/actions.js';12import { asArray } from '../../../common/arrays.js';13import { RunOnceScheduler } from '../../../common/async.js';14import { Codicon } from '../../../common/codicons.js';15import { ThemeIcon } from '../../../common/themables.js';16import { Emitter, Event } from '../../../common/event.js';17import { KeyCode, KeyMod, ScanCode, ScanCodeUtils } from '../../../common/keyCodes.js';18import { ResolvedKeybinding } from '../../../common/keybindings.js';19import { Disposable, DisposableStore, dispose, IDisposable } from '../../../common/lifecycle.js';20import { isMacintosh } from '../../../common/platform.js';21import * as strings from '../../../common/strings.js';22import './menubar.css';23import * as nls from '../../../../nls.js';24import { mainWindow } from '../../window.js';2526const $ = DOM.$;2728export interface IMenuBarOptions {29enableMnemonics?: boolean;30disableAltFocus?: boolean;31visibility?: string;32getKeybinding?: (action: IAction) => ResolvedKeybinding | undefined;33alwaysOnMnemonics?: boolean;34compactMode?: IMenuDirection;35actionRunner?: IActionRunner;36getCompactMenuActions?: () => IAction[];37}3839export interface MenuBarMenu {40actions: IAction[];41label: string;42}4344interface MenuBarMenuWithElements extends MenuBarMenu {45titleElement?: HTMLElement;46buttonElement?: HTMLElement;47}4849enum MenubarState {50HIDDEN,51VISIBLE,52FOCUSED,53OPEN54}5556export class MenuBar extends Disposable {5758static readonly OVERFLOW_INDEX: number = -1;5960private menus: MenuBarMenuWithElements[];6162private overflowMenu!: MenuBarMenuWithElements & { titleElement: HTMLElement; buttonElement: HTMLElement };6364private focusedMenu: {65index: number;66holder?: HTMLElement;67widget?: Menu;68} | undefined;6970private focusToReturn: HTMLElement | undefined;71private menuUpdater: RunOnceScheduler;7273// Input-related74private _mnemonicsInUse: boolean = false;75private openedViaKeyboard: boolean = false;76private awaitingAltRelease: boolean = false;77private ignoreNextMouseUp: boolean = false;78private mnemonics: Map<string, number>;7980private updatePending: boolean = false;81private _focusState: MenubarState;82private actionRunner: IActionRunner;8384private readonly _onVisibilityChange: Emitter<boolean>;85private readonly _onFocusStateChange: Emitter<boolean>;8687private numMenusShown: number = 0;88private overflowLayoutScheduled: IDisposable | undefined = undefined;8990private readonly menuDisposables = this._register(new DisposableStore());9192constructor(private container: HTMLElement, private options: IMenuBarOptions, private menuStyle: IMenuStyles) {93super();9495this.container.setAttribute('role', 'menubar');96if (this.isCompact) {97this.container.classList.add('compact');98}99100this.menus = [];101this.mnemonics = new Map<string, number>();102103this._focusState = MenubarState.VISIBLE;104105this._onVisibilityChange = this._register(new Emitter<boolean>());106this._onFocusStateChange = this._register(new Emitter<boolean>());107108this.createOverflowMenu();109110this.menuUpdater = this._register(new RunOnceScheduler(() => this.update(), 200));111112this.actionRunner = this.options.actionRunner ?? this._register(new ActionRunner());113this._register(this.actionRunner.onWillRun(() => {114this.setUnfocusedState();115}));116117this._register(DOM.ModifierKeyEmitter.getInstance().event(this.onModifierKeyToggled, this));118119this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_DOWN, (e) => {120const event = new StandardKeyboardEvent(e as KeyboardEvent);121let eventHandled = true;122const key = !!e.key ? e.key.toLocaleLowerCase() : '';123124const tabNav = isMacintosh && !this.isCompact;125126if (event.equals(KeyCode.LeftArrow) || (tabNav && event.equals(KeyCode.Tab | KeyMod.Shift))) {127this.focusPrevious();128} else if (event.equals(KeyCode.RightArrow) || (tabNav && event.equals(KeyCode.Tab))) {129this.focusNext();130} else if (event.equals(KeyCode.Escape) && this.isFocused && !this.isOpen) {131this.setUnfocusedState();132} else if (!this.isOpen && !event.ctrlKey && this.options.enableMnemonics && this.mnemonicsInUse && this.mnemonics.has(key)) {133const menuIndex = this.mnemonics.get(key)!;134this.onMenuTriggered(menuIndex, false);135} else {136eventHandled = false;137}138139// Never allow default tab behavior when not compact140if (!this.isCompact && (event.equals(KeyCode.Tab | KeyMod.Shift) || event.equals(KeyCode.Tab))) {141event.preventDefault();142}143144if (eventHandled) {145event.preventDefault();146event.stopPropagation();147}148}));149150const window = DOM.getWindow(this.container);151this._register(DOM.addDisposableListener(window, DOM.EventType.MOUSE_DOWN, () => {152// This mouse event is outside the menubar so it counts as a focus out153if (this.isFocused) {154this.setUnfocusedState();155}156}));157158this._register(DOM.addDisposableListener(this.container, DOM.EventType.FOCUS_IN, (e) => {159const event = e as FocusEvent;160161if (event.relatedTarget) {162if (!this.container.contains(event.relatedTarget as HTMLElement)) {163this.focusToReturn = event.relatedTarget as HTMLElement;164}165}166}));167168this._register(DOM.addDisposableListener(this.container, DOM.EventType.FOCUS_OUT, (e) => {169const event = e as FocusEvent;170171// We are losing focus and there is no related target, e.g. webview case172if (!event.relatedTarget) {173this.setUnfocusedState();174}175// We are losing focus and there is a target, reset focusToReturn value as not to redirect176else if (event.relatedTarget && !this.container.contains(event.relatedTarget as HTMLElement)) {177this.focusToReturn = undefined;178this.setUnfocusedState();179}180}));181182this._register(DOM.addDisposableListener(window, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {183if (!this.options.enableMnemonics || !e.altKey || e.ctrlKey || e.defaultPrevented) {184return;185}186187const key = e.key.toLocaleLowerCase();188if (!this.mnemonics.has(key)) {189return;190}191192this.mnemonicsInUse = true;193this.updateMnemonicVisibility(true);194195const menuIndex = this.mnemonics.get(key)!;196this.onMenuTriggered(menuIndex, false);197}));198199this.setUnfocusedState();200}201202push(arg: MenuBarMenu | MenuBarMenu[]): void {203const menus: MenuBarMenu[] = asArray(arg);204205menus.forEach((menuBarMenu) => {206const menuIndex = this.menus.length;207const cleanMenuLabel = cleanMnemonic(menuBarMenu.label);208209const mnemonicMatches = MENU_MNEMONIC_REGEX.exec(menuBarMenu.label);210211// Register mnemonics212if (mnemonicMatches) {213const mnemonic = !!mnemonicMatches[1] ? mnemonicMatches[1] : mnemonicMatches[3];214215this.registerMnemonic(this.menus.length, mnemonic);216}217218if (this.isCompact) {219this.menus.push(menuBarMenu);220} else {221const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': -1, 'aria-label': cleanMenuLabel, 'aria-haspopup': true });222const titleElement = $('div.menubar-menu-title', { 'role': 'none', 'aria-hidden': true });223224buttonElement.appendChild(titleElement);225this.container.insertBefore(buttonElement, this.overflowMenu.buttonElement);226227this.updateLabels(titleElement, buttonElement, menuBarMenu.label);228229this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.KEY_UP, (e) => {230const event = new StandardKeyboardEvent(e as KeyboardEvent);231let eventHandled = true;232233if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter)) && !this.isOpen) {234this.focusedMenu = { index: menuIndex };235this.openedViaKeyboard = true;236this.focusState = MenubarState.OPEN;237} else {238eventHandled = false;239}240241if (eventHandled) {242event.preventDefault();243event.stopPropagation();244}245}));246247this._register(Gesture.addTarget(buttonElement));248this._register(DOM.addDisposableListener(buttonElement, EventType.Tap, (e: GestureEvent) => {249// Ignore this touch if the menu is touched250if (this.isOpen && this.focusedMenu && this.focusedMenu.holder && DOM.isAncestor(e.initialTarget as HTMLElement, this.focusedMenu.holder)) {251return;252}253254this.ignoreNextMouseUp = false;255this.onMenuTriggered(menuIndex, true);256257e.preventDefault();258e.stopPropagation();259}));260261this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {262// Ignore non-left-click263const mouseEvent = new StandardMouseEvent(DOM.getWindow(buttonElement), e);264if (!mouseEvent.leftButton) {265e.preventDefault();266return;267}268269if (!this.isOpen) {270// Open the menu with mouse down and ignore the following mouse up event271this.ignoreNextMouseUp = true;272this.onMenuTriggered(menuIndex, true);273} else {274this.ignoreNextMouseUp = false;275}276277e.preventDefault();278e.stopPropagation();279}));280281this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_UP, (e) => {282if (e.defaultPrevented) {283return;284}285286if (!this.ignoreNextMouseUp) {287if (this.isFocused) {288this.onMenuTriggered(menuIndex, true);289}290} else {291this.ignoreNextMouseUp = false;292}293}));294295this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_ENTER, () => {296if (this.isOpen && !this.isCurrentMenu(menuIndex)) {297buttonElement.focus();298this.cleanupCustomMenu();299this.showCustomMenu(menuIndex, false);300} else if (this.isFocused && !this.isOpen) {301this.focusedMenu = { index: menuIndex };302buttonElement.focus();303}304}));305306this.menus.push({307label: menuBarMenu.label,308actions: menuBarMenu.actions,309buttonElement: buttonElement,310titleElement: titleElement311});312}313});314}315316createOverflowMenu(): void {317const label = this.isCompact ? nls.localize('mAppMenu', 'Application Menu') : nls.localize('mMore', 'More');318const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': this.isCompact ? 0 : -1, 'aria-label': label, 'aria-haspopup': true });319const titleElement = $('div.menubar-menu-title.toolbar-toggle-more' + ThemeIcon.asCSSSelector(Codicon.menuBarMore), { 'role': 'none', 'aria-hidden': true });320321buttonElement.appendChild(titleElement);322this.container.appendChild(buttonElement);323buttonElement.style.visibility = 'hidden';324325this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.KEY_UP, (e) => {326const event = new StandardKeyboardEvent(e as KeyboardEvent);327let eventHandled = true;328329const triggerKeys = [KeyCode.Enter];330if (!this.isCompact) {331triggerKeys.push(KeyCode.DownArrow);332} else {333triggerKeys.push(KeyCode.Space);334335if (this.options.compactMode?.horizontal === HorizontalDirection.Right) {336triggerKeys.push(KeyCode.RightArrow);337} else if (this.options.compactMode?.horizontal === HorizontalDirection.Left) {338triggerKeys.push(KeyCode.LeftArrow);339}340}341342if ((triggerKeys.some(k => event.equals(k)) && !this.isOpen)) {343this.focusedMenu = { index: MenuBar.OVERFLOW_INDEX };344this.openedViaKeyboard = true;345this.focusState = MenubarState.OPEN;346} else {347eventHandled = false;348}349350if (eventHandled) {351event.preventDefault();352event.stopPropagation();353}354}));355356this._register(Gesture.addTarget(buttonElement));357this._register(DOM.addDisposableListener(buttonElement, EventType.Tap, (e: GestureEvent) => {358// Ignore this touch if the menu is touched359if (this.isOpen && this.focusedMenu && this.focusedMenu.holder && DOM.isAncestor(e.initialTarget as HTMLElement, this.focusedMenu.holder)) {360return;361}362363this.ignoreNextMouseUp = false;364this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true);365366e.preventDefault();367e.stopPropagation();368}));369370this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_DOWN, (e) => {371// Ignore non-left-click372const mouseEvent = new StandardMouseEvent(DOM.getWindow(buttonElement), e);373if (!mouseEvent.leftButton) {374e.preventDefault();375return;376}377378if (!this.isOpen) {379// Open the menu with mouse down and ignore the following mouse up event380this.ignoreNextMouseUp = true;381this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true);382} else {383this.ignoreNextMouseUp = false;384}385386e.preventDefault();387e.stopPropagation();388}));389390this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_UP, (e) => {391if (e.defaultPrevented) {392return;393}394395if (!this.ignoreNextMouseUp) {396if (this.isFocused) {397this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true);398}399} else {400this.ignoreNextMouseUp = false;401}402}));403404this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_ENTER, () => {405if (this.isOpen && !this.isCurrentMenu(MenuBar.OVERFLOW_INDEX)) {406this.overflowMenu.buttonElement.focus();407this.cleanupCustomMenu();408this.showCustomMenu(MenuBar.OVERFLOW_INDEX, false);409} else if (this.isFocused && !this.isOpen) {410this.focusedMenu = { index: MenuBar.OVERFLOW_INDEX };411buttonElement.focus();412}413}));414415this.overflowMenu = {416buttonElement: buttonElement,417titleElement: titleElement,418label: 'More',419actions: []420};421}422423updateMenu(menu: MenuBarMenu): void {424const menuToUpdate = this.menus.filter(menuBarMenu => menuBarMenu.label === menu.label);425if (menuToUpdate && menuToUpdate.length) {426menuToUpdate[0].actions = menu.actions;427}428}429430override dispose(): void {431super.dispose();432433this.menus.forEach(menuBarMenu => {434menuBarMenu.titleElement?.remove();435menuBarMenu.buttonElement?.remove();436});437438this.overflowMenu.titleElement.remove();439this.overflowMenu.buttonElement.remove();440441dispose(this.overflowLayoutScheduled);442this.overflowLayoutScheduled = undefined;443}444445blur(): void {446this.setUnfocusedState();447}448449getWidth(): number {450if (!this.isCompact && this.menus) {451const left = this.menus[0].buttonElement!.getBoundingClientRect().left;452const right = this.hasOverflow ? this.overflowMenu.buttonElement.getBoundingClientRect().right : this.menus[this.menus.length - 1].buttonElement!.getBoundingClientRect().right;453return right - left;454}455456return 0;457}458459getHeight(): number {460return this.container.clientHeight;461}462463toggleFocus(): void {464if (!this.isFocused && this.options.visibility !== 'hidden') {465this.mnemonicsInUse = true;466this.focusedMenu = { index: this.numMenusShown > 0 ? 0 : MenuBar.OVERFLOW_INDEX };467this.focusState = MenubarState.FOCUSED;468} else if (!this.isOpen) {469this.setUnfocusedState();470}471}472473private updateOverflowAction(): void {474if (!this.menus || !this.menus.length) {475return;476}477478const overflowMenuOnlyClass = 'overflow-menu-only';479480// Remove overflow only restriction to allow the most space481this.container.classList.toggle(overflowMenuOnlyClass, false);482483const sizeAvailable = this.container.offsetWidth;484let currentSize = 0;485let full = this.isCompact;486const prevNumMenusShown = this.numMenusShown;487this.numMenusShown = 0;488489const showableMenus = this.menus.filter(menu => menu.buttonElement !== undefined && menu.titleElement !== undefined) as (MenuBarMenuWithElements & { titleElement: HTMLElement; buttonElement: HTMLElement })[];490for (const menuBarMenu of showableMenus) {491if (!full) {492const size = menuBarMenu.buttonElement.offsetWidth;493if (currentSize + size > sizeAvailable) {494full = true;495} else {496currentSize += size;497this.numMenusShown++;498if (this.numMenusShown > prevNumMenusShown) {499menuBarMenu.buttonElement.style.visibility = 'visible';500}501}502}503504if (full) {505menuBarMenu.buttonElement.style.visibility = 'hidden';506}507}508509510// If below minimium menu threshold, show the overflow menu only as hamburger menu511if (this.numMenusShown - 1 <= showableMenus.length / 4) {512for (const menuBarMenu of showableMenus) {513menuBarMenu.buttonElement.style.visibility = 'hidden';514}515516full = true;517this.numMenusShown = 0;518currentSize = 0;519}520521// Overflow522if (this.isCompact) {523this.overflowMenu.actions = [];524for (let idx = this.numMenusShown; idx < this.menus.length; idx++) {525this.overflowMenu.actions.push(new SubmenuAction(`menubar.submenu.${this.menus[idx].label}`, this.menus[idx].label, this.menus[idx].actions || []));526}527528const compactMenuActions = this.options.getCompactMenuActions?.();529if (compactMenuActions && compactMenuActions.length) {530this.overflowMenu.actions.push(new Separator());531this.overflowMenu.actions.push(...compactMenuActions);532}533534this.overflowMenu.buttonElement.style.visibility = 'visible';535} else if (full) {536// Can't fit the more button, need to remove more menus537while (currentSize + this.overflowMenu.buttonElement.offsetWidth > sizeAvailable && this.numMenusShown > 0) {538this.numMenusShown--;539const size = showableMenus[this.numMenusShown].buttonElement.offsetWidth;540showableMenus[this.numMenusShown].buttonElement.style.visibility = 'hidden';541currentSize -= size;542}543544this.overflowMenu.actions = [];545for (let idx = this.numMenusShown; idx < showableMenus.length; idx++) {546this.overflowMenu.actions.push(new SubmenuAction(`menubar.submenu.${showableMenus[idx].label}`, showableMenus[idx].label, showableMenus[idx].actions || []));547}548549if (this.overflowMenu.buttonElement.nextElementSibling !== showableMenus[this.numMenusShown].buttonElement) {550this.overflowMenu.buttonElement.remove();551this.container.insertBefore(this.overflowMenu.buttonElement, showableMenus[this.numMenusShown].buttonElement);552}553554this.overflowMenu.buttonElement.style.visibility = 'visible';555} else {556this.overflowMenu.buttonElement.remove();557this.container.appendChild(this.overflowMenu.buttonElement);558this.overflowMenu.buttonElement.style.visibility = 'hidden';559}560561// If we are only showing the overflow, add this class to avoid taking up space562this.container.classList.toggle(overflowMenuOnlyClass, this.numMenusShown === 0);563}564565private updateLabels(titleElement: HTMLElement, buttonElement: HTMLElement, label: string): void {566const cleanMenuLabel = cleanMnemonic(label);567568// Update the button label to reflect mnemonics569570if (this.options.enableMnemonics) {571const cleanLabel = strings.escape(label);572573// This is global so reset it574MENU_ESCAPED_MNEMONIC_REGEX.lastIndex = 0;575let escMatch = MENU_ESCAPED_MNEMONIC_REGEX.exec(cleanLabel);576577// We can't use negative lookbehind so we match our negative and skip578while (escMatch && escMatch[1]) {579escMatch = MENU_ESCAPED_MNEMONIC_REGEX.exec(cleanLabel);580}581582const replaceDoubleEscapes = (str: string) => str.replace(/&&/g, '&');583584if (escMatch) {585titleElement.textContent = '';586titleElement.append(587strings.ltrim(replaceDoubleEscapes(cleanLabel.substr(0, escMatch.index)), ' '),588$('mnemonic', { 'aria-hidden': 'true' }, escMatch[3]),589strings.rtrim(replaceDoubleEscapes(cleanLabel.substr(escMatch.index + escMatch[0].length)), ' ')590);591} else {592titleElement.textContent = replaceDoubleEscapes(cleanLabel).trim();593}594} else {595titleElement.textContent = cleanMenuLabel.replace(/&&/g, '&');596}597598const mnemonicMatches = MENU_MNEMONIC_REGEX.exec(label);599600// Register mnemonics601if (mnemonicMatches) {602const mnemonic = !!mnemonicMatches[1] ? mnemonicMatches[1] : mnemonicMatches[3];603604if (this.options.enableMnemonics) {605buttonElement.setAttribute('aria-keyshortcuts', 'Alt+' + mnemonic.toLocaleLowerCase());606} else {607buttonElement.removeAttribute('aria-keyshortcuts');608}609}610}611612update(options?: IMenuBarOptions): void {613if (options) {614this.options = options;615}616617// Don't update while using the menu618if (this.isFocused) {619this.updatePending = true;620return;621}622623this.menus.forEach(menuBarMenu => {624if (!menuBarMenu.buttonElement || !menuBarMenu.titleElement) {625return;626}627628this.updateLabels(menuBarMenu.titleElement, menuBarMenu.buttonElement, menuBarMenu.label);629});630631if (!this.overflowLayoutScheduled) {632this.overflowLayoutScheduled = DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this.container), () => {633this.updateOverflowAction();634this.overflowLayoutScheduled = undefined;635});636}637638this.setUnfocusedState();639}640641private registerMnemonic(menuIndex: number, mnemonic: string): void {642this.mnemonics.set(mnemonic.toLocaleLowerCase(), menuIndex);643}644645private hideMenubar(): void {646if (this.container.style.display !== 'none') {647this.container.style.display = 'none';648this._onVisibilityChange.fire(false);649}650}651652private showMenubar(): void {653if (this.container.style.display !== 'flex') {654this.container.style.display = 'flex';655this._onVisibilityChange.fire(true);656657this.updateOverflowAction();658}659}660661private get focusState(): MenubarState {662return this._focusState;663}664665private set focusState(value: MenubarState) {666if (this._focusState >= MenubarState.FOCUSED && value < MenubarState.FOCUSED) {667// Losing focus, update the menu if needed668669if (this.updatePending) {670this.menuUpdater.schedule();671this.updatePending = false;672}673}674675if (value === this._focusState) {676return;677}678679const isVisible = this.isVisible;680const isOpen = this.isOpen;681const isFocused = this.isFocused;682683this._focusState = value;684685switch (value) {686case MenubarState.HIDDEN:687if (isVisible) {688this.hideMenubar();689}690691if (isOpen) {692this.cleanupCustomMenu();693}694695if (isFocused) {696this.focusedMenu = undefined;697698if (this.focusToReturn) {699this.focusToReturn.focus();700this.focusToReturn = undefined;701}702}703704705break;706case MenubarState.VISIBLE:707if (!isVisible) {708this.showMenubar();709}710711if (isOpen) {712this.cleanupCustomMenu();713}714715if (isFocused) {716if (this.focusedMenu) {717if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {718this.overflowMenu.buttonElement.blur();719} else {720this.menus[this.focusedMenu.index].buttonElement?.blur();721}722}723724this.focusedMenu = undefined;725726if (this.focusToReturn) {727this.focusToReturn.focus();728this.focusToReturn = undefined;729}730}731732break;733case MenubarState.FOCUSED:734if (!isVisible) {735this.showMenubar();736}737738if (isOpen) {739this.cleanupCustomMenu();740}741742if (this.focusedMenu) {743// When the menu is toggled on, it may be in compact state and trying to744// focus the first menu. In this case we should focus the overflow instead.745if (this.focusedMenu.index === 0 && this.numMenusShown === 0) {746this.focusedMenu.index = MenuBar.OVERFLOW_INDEX;747}748749if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {750this.overflowMenu.buttonElement.focus();751} else {752this.menus[this.focusedMenu.index].buttonElement?.focus();753}754}755break;756case MenubarState.OPEN:757if (!isVisible) {758this.showMenubar();759}760761if (this.focusedMenu) {762this.cleanupCustomMenu();763this.showCustomMenu(this.focusedMenu.index, this.openedViaKeyboard);764}765break;766}767768this._focusState = value;769this._onFocusStateChange.fire(this.focusState >= MenubarState.FOCUSED);770}771772get isVisible(): boolean {773return this.focusState >= MenubarState.VISIBLE;774}775776private get isFocused(): boolean {777return this.focusState >= MenubarState.FOCUSED;778}779780private get isOpen(): boolean {781return this.focusState >= MenubarState.OPEN;782}783784private get hasOverflow(): boolean {785return this.isCompact || this.numMenusShown < this.menus.length;786}787788private get isCompact(): boolean {789return this.options.compactMode !== undefined;790}791792private setUnfocusedState(): void {793if (this.options.visibility === 'toggle' || this.options.visibility === 'hidden') {794this.focusState = MenubarState.HIDDEN;795} else if (this.options.visibility === 'classic' && browser.isFullscreen(mainWindow)) {796this.focusState = MenubarState.HIDDEN;797} else {798this.focusState = MenubarState.VISIBLE;799}800801this.ignoreNextMouseUp = false;802this.mnemonicsInUse = false;803this.updateMnemonicVisibility(false);804}805806private focusPrevious(): void {807808if (!this.focusedMenu || this.numMenusShown === 0) {809return;810}811812813let newFocusedIndex = (this.focusedMenu.index - 1 + this.numMenusShown) % this.numMenusShown;814if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {815newFocusedIndex = this.numMenusShown - 1;816} else if (this.focusedMenu.index === 0 && this.hasOverflow) {817newFocusedIndex = MenuBar.OVERFLOW_INDEX;818}819820if (newFocusedIndex === this.focusedMenu.index) {821return;822}823824if (this.isOpen) {825this.cleanupCustomMenu();826this.showCustomMenu(newFocusedIndex);827} else if (this.isFocused) {828this.focusedMenu.index = newFocusedIndex;829if (newFocusedIndex === MenuBar.OVERFLOW_INDEX) {830this.overflowMenu.buttonElement.focus();831} else {832this.menus[newFocusedIndex].buttonElement?.focus();833}834}835}836837private focusNext(): void {838if (!this.focusedMenu || this.numMenusShown === 0) {839return;840}841842let newFocusedIndex = (this.focusedMenu.index + 1) % this.numMenusShown;843if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {844newFocusedIndex = 0;845} else if (this.focusedMenu.index === this.numMenusShown - 1) {846newFocusedIndex = MenuBar.OVERFLOW_INDEX;847}848849if (newFocusedIndex === this.focusedMenu.index) {850return;851}852853if (this.isOpen) {854this.cleanupCustomMenu();855this.showCustomMenu(newFocusedIndex);856} else if (this.isFocused) {857this.focusedMenu.index = newFocusedIndex;858if (newFocusedIndex === MenuBar.OVERFLOW_INDEX) {859this.overflowMenu.buttonElement.focus();860} else {861this.menus[newFocusedIndex].buttonElement?.focus();862}863}864}865866private updateMnemonicVisibility(visible: boolean): void {867if (this.menus) {868this.menus.forEach(menuBarMenu => {869if (menuBarMenu.titleElement && menuBarMenu.titleElement.children.length) {870const child = menuBarMenu.titleElement.children.item(0) as HTMLElement;871if (child) {872child.style.textDecoration = (this.options.alwaysOnMnemonics || visible) ? 'underline' : '';873}874}875});876}877}878879private get mnemonicsInUse(): boolean {880return this._mnemonicsInUse;881}882883private set mnemonicsInUse(value: boolean) {884this._mnemonicsInUse = value;885}886887private get shouldAltKeyFocus(): boolean {888if (isMacintosh) {889return false;890}891892if (!this.options.disableAltFocus) {893return true;894}895896if (this.options.visibility === 'toggle') {897return true;898}899900return false;901}902903public get onVisibilityChange(): Event<boolean> {904return this._onVisibilityChange.event;905}906907public get onFocusStateChange(): Event<boolean> {908return this._onFocusStateChange.event;909}910911private onMenuTriggered(menuIndex: number, clicked: boolean) {912if (this.isOpen) {913if (this.isCurrentMenu(menuIndex)) {914this.setUnfocusedState();915} else {916this.cleanupCustomMenu();917this.showCustomMenu(menuIndex, this.openedViaKeyboard);918}919} else {920this.focusedMenu = { index: menuIndex };921this.openedViaKeyboard = !clicked;922this.focusState = MenubarState.OPEN;923}924}925926private onModifierKeyToggled(modifierKeyStatus: DOM.IModifierKeyStatus): void {927const allModifiersReleased = !modifierKeyStatus.altKey && !modifierKeyStatus.ctrlKey && !modifierKeyStatus.shiftKey && !modifierKeyStatus.metaKey;928929if (this.options.visibility === 'hidden') {930return;931}932933// Prevent alt-key default if the menu is not hidden and we use alt to focus934if (modifierKeyStatus.event && this.shouldAltKeyFocus) {935if (ScanCodeUtils.toEnum(modifierKeyStatus.event.code) === ScanCode.AltLeft) {936modifierKeyStatus.event.preventDefault();937}938}939940// Alt key pressed while menu is focused. This should return focus away from the menubar941if (this.isFocused && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.altKey) {942this.setUnfocusedState();943this.mnemonicsInUse = false;944this.awaitingAltRelease = true;945}946947// Clean alt key press and release948if (allModifiersReleased && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.lastKeyReleased === 'alt') {949if (!this.awaitingAltRelease) {950if (!this.isFocused && this.shouldAltKeyFocus) {951this.mnemonicsInUse = true;952this.focusedMenu = { index: this.numMenusShown > 0 ? 0 : MenuBar.OVERFLOW_INDEX };953this.focusState = MenubarState.FOCUSED;954} else if (!this.isOpen) {955this.setUnfocusedState();956}957}958}959960// Alt key released961if (!modifierKeyStatus.altKey && modifierKeyStatus.lastKeyReleased === 'alt') {962this.awaitingAltRelease = false;963}964965if (this.options.enableMnemonics && this.menus && !this.isOpen) {966this.updateMnemonicVisibility((!this.awaitingAltRelease && modifierKeyStatus.altKey) || this.mnemonicsInUse);967}968}969970private isCurrentMenu(menuIndex: number): boolean {971if (!this.focusedMenu) {972return false;973}974975return this.focusedMenu.index === menuIndex;976}977978private cleanupCustomMenu(): void {979if (this.focusedMenu) {980// Remove focus from the menus first981if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {982this.overflowMenu.buttonElement.focus();983} else {984this.menus[this.focusedMenu.index].buttonElement?.focus();985}986987if (this.focusedMenu.holder) {988this.focusedMenu.holder.parentElement?.classList.remove('open');989990this.focusedMenu.holder.remove();991}992993this.focusedMenu.widget?.dispose();994995this.focusedMenu = { index: this.focusedMenu.index };996}997this.menuDisposables.clear();998}9991000private showCustomMenu(menuIndex: number, selectFirst = true): void {1001const actualMenuIndex = menuIndex >= this.numMenusShown ? MenuBar.OVERFLOW_INDEX : menuIndex;1002const customMenu = actualMenuIndex === MenuBar.OVERFLOW_INDEX ? this.overflowMenu : this.menus[actualMenuIndex];10031004if (!customMenu.actions || !customMenu.buttonElement || !customMenu.titleElement) {1005return;1006}10071008const menuHolder = $('div.menubar-menu-items-holder', { 'title': '' });10091010customMenu.buttonElement.classList.add('open');10111012const titleBoundingRect = customMenu.titleElement.getBoundingClientRect();1013const titleBoundingRectZoom = DOM.getDomNodeZoomLevel(customMenu.titleElement);10141015if (this.options.compactMode?.horizontal === HorizontalDirection.Right) {1016menuHolder.style.left = `${titleBoundingRect.left + this.container.clientWidth}px`;1017} else if (this.options.compactMode?.horizontal === HorizontalDirection.Left) {1018const windowWidth = DOM.getWindow(this.container).innerWidth;1019menuHolder.style.right = `${windowWidth - titleBoundingRect.left}px`;1020menuHolder.style.left = 'auto';1021} else {1022menuHolder.style.left = `${titleBoundingRect.left * titleBoundingRectZoom}px`;1023}10241025if (this.options.compactMode?.vertical === VerticalDirection.Above) {1026// TODO@benibenj Do not hardcode the height of the menu holder1027menuHolder.style.top = `${titleBoundingRect.top - this.menus.length * 30 + this.container.clientHeight}px`;1028} else if (this.options.compactMode?.vertical === VerticalDirection.Below) {1029menuHolder.style.top = `${titleBoundingRect.top}px`;1030} else {1031menuHolder.style.top = `${titleBoundingRect.bottom * titleBoundingRectZoom}px`;1032}10331034customMenu.buttonElement.appendChild(menuHolder);10351036const menuOptions: IMenuOptions = {1037getKeyBinding: this.options.getKeybinding,1038actionRunner: this.actionRunner,1039enableMnemonics: this.options.alwaysOnMnemonics || (this.mnemonicsInUse && this.options.enableMnemonics),1040ariaLabel: customMenu.buttonElement.getAttribute('aria-label') ?? undefined,1041expandDirection: this.isCompact ? this.options.compactMode : { horizontal: HorizontalDirection.Right, vertical: VerticalDirection.Below },1042useEventAsContext: true1043};10441045const menuWidget = this.menuDisposables.add(new Menu(menuHolder, customMenu.actions, menuOptions, this.menuStyle));1046this.menuDisposables.add(menuWidget.onDidCancel(() => {1047this.focusState = MenubarState.FOCUSED;1048}));10491050if (actualMenuIndex !== menuIndex) {1051menuWidget.trigger(menuIndex - this.numMenusShown);1052} else {1053menuWidget.focus(selectFirst);1054}10551056this.focusedMenu = {1057index: actualMenuIndex,1058holder: menuHolder,1059widget: menuWidget1060};1061}1062}106310641065