From aedd4402fb7eb0022589012f440b059c6b12f12b Mon Sep 17 00:00:00 2001 From: SteVen Batten <6561887+sbatten@users.noreply.github.com> Date: Mon, 3 Dec 2018 06:39:58 -0800 Subject: [PATCH] Make menubar its own widget and add overflow (#63954) * working state * fix keybindings and add compact submenu mode * fix blur * fix mnemonics in overflow menu * fix issues with window resizing * switch to ellipsis --- src/vs/base/browser/ui/actionbar/actionbar.ts | 18 +- src/vs/base/browser/ui/menu/ellipsis.svg | 1 + src/vs/base/browser/ui/menu/menu.css | 60 ++ src/vs/base/browser/ui/menu/menu.ts | 49 +- src/vs/base/browser/ui/menu/menubar.ts | 964 ++++++++++++++++++ .../parts/titlebar/media/titlebarpart.css | 45 - .../browser/parts/titlebar/menubarControl.ts | 828 ++------------- 7 files changed, 1170 insertions(+), 795 deletions(-) create mode 100644 src/vs/base/browser/ui/menu/ellipsis.svg create mode 100644 src/vs/base/browser/ui/menu/menubar.ts diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index 08a67171915..c93f88cc8db 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -675,12 +675,28 @@ export class ActionBar extends Disposable implements IActionRunner { return this.items.length === 0; } - focus(selectFirst?: boolean): void { + focus(index?: number): void; + focus(selectFirst?: boolean): void; + focus(arg?: any): void { + let selectFirst: boolean; + let index: number; + if (arg === undefined) { + selectFirst = true; + } else if (typeof arg === 'number') { + index = arg; + } else if (typeof arg === 'boolean') { + selectFirst = arg; + } + if (selectFirst && typeof this.focusedItem === 'undefined') { // Focus the first enabled item this.focusedItem = this.items.length - 1; this.focusNext(); } else { + if (index !== undefined) { + this.focusedItem = index; + } + this.updateFocus(); } } diff --git a/src/vs/base/browser/ui/menu/ellipsis.svg b/src/vs/base/browser/ui/menu/ellipsis.svg new file mode 100644 index 00000000000..e3f85623356 --- /dev/null +++ b/src/vs/base/browser/ui/menu/ellipsis.svg @@ -0,0 +1 @@ +Ellipsis_bold_16x \ No newline at end of file diff --git a/src/vs/base/browser/ui/menu/menu.css b/src/vs/base/browser/ui/menu/menu.css index c24040ba3a3..b896e7bed28 100644 --- a/src/vs/base/browser/ui/menu/menu.css +++ b/src/vs/base/browser/ui/menu/menu.css @@ -145,4 +145,64 @@ .hc-black .monaco-menu .monaco-action-bar.vertical .action-item.focused { background: none; +} + +/* Menubar styles */ + +.menubar { + display: flex; + flex-shrink: 1; + box-sizing: border-box; + height: 30px; + -webkit-app-region: no-drag; + overflow: hidden; + flex-wrap: wrap; +} + +.fullscreen .menubar { + margin: 0px; + padding: 0px 5px; +} + +.menubar > .menubar-menu-button { + align-items: center; + box-sizing: border-box; + padding: 0px 8px; + cursor: default; + -webkit-app-region: no-drag; + zoom: 1; + white-space: nowrap; + outline: 0; +} + +.menubar .menubar-menu-items-holder { + position: absolute; + left: 0px; + opacity: 1; + z-index: 2000; +} + +.menubar .menubar-menu-items-holder.monaco-menu-container { + font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif; + outline: 0; + border: none; +} + +.menubar .menubar-menu-items-holder.monaco-menu-container :focus { + outline: 0; +} + +.menubar .toolbar-toggle-more { + background-position: center; + background-repeat: no-repeat; + background-size: 14px; + width: 20px; + height: 100%; +} + +.menubar .toolbar-toggle-more { + display: inline-block; + padding: 0; + -webkit-mask: url('ellipsis.svg') no-repeat 50% 55%/14px 14px; + mask: url('ellipsis.svg') no-repeat 50% 55%/14px 14px; } \ No newline at end of file diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index 5abc7332d4a..99aa8227783 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -9,7 +9,7 @@ import * as strings from 'vs/base/common/strings'; import { IActionRunner, IAction, Action } from 'vs/base/common/actions'; import { ActionBar, IActionItemProvider, ActionsOrientation, Separator, ActionItem, IActionItemOptions, BaseActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { ResolvedKeybinding, KeyCode, KeyCodeUtils } from 'vs/base/common/keyCodes'; -import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, hasClass, addDisposableListener, removeClass, append, $, addClasses, getClientArea, removeClasses } from 'vs/base/browser/dom'; +import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, hasClass, addDisposableListener, removeClass, append, $, addClasses, removeClasses } from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -209,7 +209,7 @@ export class Menu extends ActionBar { return this.scrollableElement.getDomNode(); } - public get onScroll(): Event { + get onScroll(): Event { return this._onScroll.event; } @@ -217,6 +217,20 @@ export class Menu extends ActionBar { return this.menuElement.scrollTop; } + trigger(index: number): void { + if (index <= this.items.length && index >= 0) { + const item = this.items[index]; + if (item instanceof SubmenuActionItem) { + super.focus(index); + item.open(true); + } else if (item instanceof MenuActionItem) { + super.run(item._action, item._context); + } else { + return; + } + } + } + private focusItemByElement(element: HTMLElement) { const lastFocusedItem = this.focusedItem; this.setFocusedItem(element); @@ -285,10 +299,6 @@ export class Menu extends ActionBar { return menuActionItem; } } - - public focus(selectFirst = true) { - super.focus(selectFirst); - } } interface IMenuItemOptions extends IActionItemOptions { @@ -571,6 +581,11 @@ class SubmenuActionItem extends MenuActionItem { })); } + open(selectFirst?: boolean): void { + this.cleanupExistingSubmenu(false); + this.createSubmenu(selectFirst); + } + onClick(e: EventLike): void { // stop clicking from trying to run an action EventHelper.stop(e, true); @@ -595,8 +610,22 @@ class SubmenuActionItem extends MenuActionItem { if (!this.parentData.submenu) { this.submenuContainer = append(this.element, $('div.monaco-submenu')); addClasses(this.submenuContainer, 'menubar-menu-items-holder', 'context-view'); - this.submenuContainer.style.left = `${getClientArea(this.element).width}px`; - this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset}px`; + + this.parentData.submenu = new Menu(this.submenuContainer, this.submenuActions, this.submenuOptions); + if (this.menuStyle) { + this.parentData.submenu.style(this.menuStyle); + } + + const boundingRect = this.element.getBoundingClientRect(); + const childBoundingRect = this.submenuContainer.getBoundingClientRect(); + + if (window.innerWidth <= boundingRect.right + childBoundingRect.width) { + this.submenuContainer.style.left = '10px'; + this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset + boundingRect.height}px`; + } else { + this.submenuContainer.style.left = `${this.element.offsetWidth}px`; + this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset}px`; + } this.submenuDisposables.push(addDisposableListener(this.submenuContainer, EventType.KEY_UP, e => { let event = new StandardKeyboardEvent(e); @@ -619,10 +648,6 @@ class SubmenuActionItem extends MenuActionItem { } })); - this.parentData.submenu = new Menu(this.submenuContainer, this.submenuActions, this.submenuOptions); - if (this.menuStyle) { - this.parentData.submenu.style(this.menuStyle); - } this.submenuDisposables.push(this.parentData.submenu.onDidCancel(() => { this.parentData.parent.focus(); diff --git a/src/vs/base/browser/ui/menu/menubar.ts b/src/vs/base/browser/ui/menu/menubar.ts new file mode 100644 index 00000000000..5d916ccaf05 --- /dev/null +++ b/src/vs/base/browser/ui/menu/menubar.ts @@ -0,0 +1,964 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as browser from 'vs/base/browser/browser'; +import * as DOM from 'vs/base/browser/dom'; +import * as strings from 'vs/base/common/strings'; +import * as nls from 'vs/nls'; +import { domEvent } from 'vs/base/browser/event'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch'; +import { cleanMnemonic, IMenuOptions, Menu, MENU_ESCAPED_MNEMONIC_REGEX, MENU_MNEMONIC_REGEX, SubmenuAction, IMenuStyles } from 'vs/base/browser/ui/menu/menu'; +import { ActionRunner, IAction, IActionRunner } from 'vs/base/common/actions'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { Event, Emitter } from 'vs/base/common/event'; +import { KeyCode, KeyCodeUtils, ResolvedKeybinding } from 'vs/base/common/keyCodes'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; + +const $ = DOM.$; + +export interface IMenuBarOptions { + enableMnemonics?: boolean; + visibility?: string; + getKeybinding?: (action: IAction) => ResolvedKeybinding; +} + +export interface MenuBarMenu { + actions: IAction[]; + label: string; +} + +enum MenubarState { + HIDDEN, + VISIBLE, + FOCUSED, + OPEN +} + +export class MenuBar extends Disposable { + + static readonly OVERFLOW_INDEX: number = -1; + + private menuCache: { + buttonElement: HTMLElement; + titleElement: HTMLElement; + label: string; + actions?: IAction[]; + }[]; + + private overflowMenu: { + buttonElement: HTMLElement; + titleElement: HTMLElement; + label: string; + actions?: IAction[]; + }; + + private focusedMenu: { + index: number; + holder?: HTMLElement; + widget?: Menu; + }; + + private focusToReturn: HTMLElement; + private menuUpdater: RunOnceScheduler; + + // Input-related + private _mnemonicsInUse: boolean; + private openedViaKeyboard: boolean; + private awaitingAltRelease: boolean; + private ignoreNextMouseUp: boolean; + private mnemonics: Map; + + private updatePending: boolean; + private _focusState: MenubarState; + private actionRunner: IActionRunner; + + private _onVisibilityChange: Emitter; + private _onFocusStateChange: Emitter; + + private numMenusShown: number; + private menuStyle: IMenuStyles; + + constructor(private container: HTMLElement, private options: IMenuBarOptions = {}) { + super(); + + this.container.attributes['role'] = 'menubar'; + + this.menuCache = []; + this.mnemonics = new Map(); + + this._onVisibilityChange = this._register(new Emitter()); + this._onFocusStateChange = this._register(new Emitter()); + + this.createOverflowMenu(); + + this.menuUpdater = this._register(new RunOnceScheduler(() => this.update(), 200)); + + this.actionRunner = this._register(new ActionRunner()); + this._register(this.actionRunner.onDidBeforeRun(() => { + this.setUnfocusedState(); + })); + + this._register(ModifierKeyEmitter.getInstance().event(this.onModifierKeyToggled, this)); + + this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_DOWN, (e) => { + let event = new StandardKeyboardEvent(e as KeyboardEvent); + let eventHandled = true; + const key = !!e.key ? KeyCodeUtils.fromString(e.key) : KeyCode.Unknown; + + if (event.equals(KeyCode.LeftArrow)) { + this.focusPrevious(); + } else if (event.equals(KeyCode.RightArrow)) { + this.focusNext(); + } else if (event.equals(KeyCode.Escape) && this.isFocused && !this.isOpen) { + this.setUnfocusedState(); + } else if (!this.isOpen && !event.ctrlKey && this.options.enableMnemonics && this.mnemonicsInUse && this.mnemonics.has(key)) { + const menuIndex = this.mnemonics.get(key); + this.onMenuTriggered(menuIndex, false); + } else { + eventHandled = false; + } + + if (eventHandled) { + event.preventDefault(); + event.stopPropagation(); + } + })); + + this._register(DOM.addDisposableListener(window, DOM.EventType.MOUSE_DOWN, () => { + // This mouse event is outside the menubar so it counts as a focus out + if (this.isFocused) { + this.setUnfocusedState(); + } + })); + + this._register(DOM.addDisposableListener(this.container, DOM.EventType.FOCUS_IN, (e) => { + let event = e as FocusEvent; + + if (event.relatedTarget) { + if (!this.container.contains(event.relatedTarget as HTMLElement)) { + this.focusToReturn = event.relatedTarget as HTMLElement; + } + } + })); + + this._register(DOM.addDisposableListener(this.container, DOM.EventType.FOCUS_OUT, (e) => { + let event = e as FocusEvent; + + if (event.relatedTarget) { + if (!this.container.contains(event.relatedTarget as HTMLElement)) { + this.focusToReturn = null; + this.setUnfocusedState(); + } + } + })); + + this._register(DOM.addDisposableListener(window, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { + if (!this.options.enableMnemonics || !e.altKey || e.ctrlKey || e.defaultPrevented) { + return; + } + + const key = KeyCodeUtils.fromString(e.key); + if (!this.mnemonics.has(key)) { + return; + } + + this.mnemonicsInUse = true; + this.updateMnemonicVisibility(true); + + const menuIndex = this.mnemonics.get(key); + this.onMenuTriggered(menuIndex, false); + })); + + this.setUnfocusedState(); + } + + push(arg: MenuBarMenu | MenuBarMenu[]): void { + const menus: MenuBarMenu[] = !Array.isArray(arg) ? [arg] : arg; + + menus.forEach((menuBarMenu) => { + const menuIndex = this.menuCache.length; + const cleanMenuLabel = cleanMnemonic(menuBarMenu.label); + + const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': -1, 'aria-label': cleanMenuLabel, 'aria-haspopup': true }); + const titleElement = $('div.menubar-menu-title', { 'role': 'none', 'aria-hidden': true }); + + buttonElement.appendChild(titleElement); + this.container.insertBefore(buttonElement, this.overflowMenu.buttonElement); + + let mnemonicMatches = MENU_MNEMONIC_REGEX.exec(menuBarMenu.label); + + // Register mnemonics + if (mnemonicMatches) { + let mnemonic = !!mnemonicMatches[1] ? mnemonicMatches[1] : mnemonicMatches[2]; + + this.registerMnemonic(this.menuCache.length, mnemonic); + } + + this.updateLabels(titleElement, buttonElement, menuBarMenu.label); + + this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.KEY_UP, (e) => { + let event = new StandardKeyboardEvent(e as KeyboardEvent); + let eventHandled = true; + + if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter)) && !this.isOpen) { + this.focusedMenu = { index: menuIndex }; + this.openedViaKeyboard = true; + this.focusState = MenubarState.OPEN; + } else { + eventHandled = false; + } + + if (eventHandled) { + event.preventDefault(); + event.stopPropagation(); + } + })); + + Gesture.addTarget(buttonElement); + this._register(DOM.addDisposableListener(buttonElement, EventType.Tap, (e: GestureEvent) => { + // Ignore this touch if the menu is touched + if (this.isOpen && this.focusedMenu.holder && DOM.isAncestor(e.initialTarget as HTMLElement, this.focusedMenu.holder)) { + return; + } + + this.ignoreNextMouseUp = false; + this.onMenuTriggered(menuIndex, true); + + e.preventDefault(); + e.stopPropagation(); + })); + + this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_DOWN, (e) => { + if (!this.isOpen) { + // Open the menu with mouse down and ignore the following mouse up event + this.ignoreNextMouseUp = true; + this.onMenuTriggered(menuIndex, true); + } else { + this.ignoreNextMouseUp = false; + } + + e.preventDefault(); + e.stopPropagation(); + })); + + this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_UP, (e) => { + if (!this.ignoreNextMouseUp) { + if (this.isFocused) { + this.onMenuTriggered(menuIndex, true); + } + } else { + this.ignoreNextMouseUp = false; + } + })); + + this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_ENTER, () => { + if (this.isOpen && !this.isCurrentMenu(menuIndex)) { + this.menuCache[menuIndex].buttonElement.focus(); + this.cleanupCustomMenu(); + this.showCustomMenu(menuIndex, false); + } else if (this.isFocused && !this.isOpen) { + this.focusedMenu = { index: menuIndex }; + buttonElement.focus(); + } + })); + + this.menuCache.push({ + label: menuBarMenu.label, + actions: menuBarMenu.actions, + buttonElement: buttonElement, + titleElement: titleElement + }); + }); + } + + createOverflowMenu(): void { + const label = nls.localize('mMore', "..."); + const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': -1, 'aria-label': label, 'aria-haspopup': true }); + const titleElement = $('div.menubar-menu-title.toolbar-toggle-more', { 'role': 'none', 'aria-hidden': true }); + + buttonElement.appendChild(titleElement); + this.container.appendChild(buttonElement); + buttonElement.style.visibility = 'hidden'; + + this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.KEY_UP, (e) => { + let event = new StandardKeyboardEvent(e as KeyboardEvent); + let eventHandled = true; + + if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter)) && !this.isOpen) { + this.focusedMenu = { index: MenuBar.OVERFLOW_INDEX }; + this.openedViaKeyboard = true; + this.focusState = MenubarState.OPEN; + } else { + eventHandled = false; + } + + if (eventHandled) { + event.preventDefault(); + event.stopPropagation(); + } + })); + + Gesture.addTarget(buttonElement); + this._register(DOM.addDisposableListener(buttonElement, EventType.Tap, (e: GestureEvent) => { + // Ignore this touch if the menu is touched + if (this.isOpen && this.focusedMenu.holder && DOM.isAncestor(e.initialTarget as HTMLElement, this.focusedMenu.holder)) { + return; + } + + this.ignoreNextMouseUp = false; + this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true); + + e.preventDefault(); + e.stopPropagation(); + })); + + this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_DOWN, (e) => { + if (!this.isOpen) { + // Open the menu with mouse down and ignore the following mouse up event + this.ignoreNextMouseUp = true; + this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true); + } else { + this.ignoreNextMouseUp = false; + } + + e.preventDefault(); + e.stopPropagation(); + })); + + this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_UP, (e) => { + if (!this.ignoreNextMouseUp) { + if (this.isFocused) { + this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true); + } + } else { + this.ignoreNextMouseUp = false; + } + })); + + this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_ENTER, () => { + if (this.isOpen && !this.isCurrentMenu(MenuBar.OVERFLOW_INDEX)) { + this.overflowMenu.buttonElement.focus(); + this.cleanupCustomMenu(); + this.showCustomMenu(MenuBar.OVERFLOW_INDEX, false); + } else if (this.isFocused && !this.isOpen) { + this.focusedMenu = { index: MenuBar.OVERFLOW_INDEX }; + buttonElement.focus(); + } + })); + + this.overflowMenu = { + buttonElement: buttonElement, + titleElement: titleElement, + label: 'More' + }; + } + + updateMenu(menu: MenuBarMenu): void { + const menuToUpdate = this.menuCache.filter(menuBarMenu => menuBarMenu.label === menu.label); + if (menuToUpdate && menuToUpdate.length) { + menuToUpdate[0].actions = menu.actions; + } + } + + dispose(): void { + super.dispose(); + + this.menuCache.forEach(menuBarMenu => { + DOM.removeNode(menuBarMenu.titleElement); + DOM.removeNode(menuBarMenu.buttonElement); + }); + + DOM.removeNode(this.overflowMenu.titleElement); + DOM.removeNode(this.overflowMenu.buttonElement); + } + + blur(): void { + this.setUnfocusedState(); + } + + getWidth(): number { + if (this.menuCache) { + const left = this.menuCache[0].buttonElement.getBoundingClientRect().left; + const right = this.hasOverflow ? this.overflowMenu.buttonElement.getBoundingClientRect().right : this.menuCache[this.menuCache.length - 1].buttonElement.getBoundingClientRect().right; + return right - left; + } + + return 0; + } + + getHeight(): number { + return this.container.clientHeight; + } + + private updateOverflowAction(): void { + if (!this.menuCache || !this.menuCache.length) { + return; + } + + const sizeAvailable = this.container.offsetWidth; + let currentSize = 0; + let full = false; + const prevNumMenusShown = this.numMenusShown; + this.numMenusShown = 0; + for (let menuBarMenu of this.menuCache) { + if (!full) { + const size = menuBarMenu.buttonElement.offsetWidth; + if (currentSize + size > sizeAvailable) { + full = true; + } else { + currentSize += size; + this.numMenusShown++; + if (this.numMenusShown > prevNumMenusShown) { + menuBarMenu.buttonElement.style.visibility = 'visible'; + } + } + } + + if (full) { + menuBarMenu.buttonElement.style.visibility = 'hidden'; + } + } + + // Overflow + if (full) { + // Can't fit the more button, need to remove more menus + while (currentSize + this.overflowMenu.buttonElement.offsetWidth > sizeAvailable && this.numMenusShown > 0) { + this.numMenusShown--; + const size = this.menuCache[this.numMenusShown].buttonElement.offsetWidth; + this.menuCache[this.numMenusShown].buttonElement.style.visibility = 'hidden'; + currentSize -= size; + } + + this.overflowMenu.actions = []; + for (let idx = this.numMenusShown; idx < this.menuCache.length; idx++) { + this.overflowMenu.actions.push(new SubmenuAction(this.menuCache[idx].label, this.menuCache[idx].actions)); + } + + DOM.removeNode(this.overflowMenu.buttonElement); + this.container.insertBefore(this.overflowMenu.buttonElement, this.menuCache[this.numMenusShown].buttonElement); + this.overflowMenu.buttonElement.style.visibility = 'visible'; + } else { + DOM.removeNode(this.overflowMenu.buttonElement); + this.container.appendChild(this.overflowMenu.buttonElement); + this.overflowMenu.buttonElement.style.visibility = 'hidden'; + } + } + + private updateLabels(titleElement: HTMLElement, buttonElement: HTMLElement, label: string): void { + const cleanMenuLabel = cleanMnemonic(label); + + // Update the button label to reflect mnemonics + titleElement.innerHTML = this.options.enableMnemonics ? + strings.escape(label).replace(MENU_ESCAPED_MNEMONIC_REGEX, '') : + cleanMenuLabel; + + let mnemonicMatches = MENU_MNEMONIC_REGEX.exec(label); + + // Register mnemonics + if (mnemonicMatches) { + let mnemonic = !!mnemonicMatches[1] ? mnemonicMatches[1] : mnemonicMatches[2]; + + if (this.options.enableMnemonics) { + buttonElement.setAttribute('aria-keyshortcuts', 'Alt+' + mnemonic.toLocaleLowerCase()); + } else { + buttonElement.removeAttribute('aria-keyshortcuts'); + } + } + } + + style(style: IMenuStyles): void { + this.menuStyle = style; + } + + update(options?: IMenuBarOptions): void { + if (options) { + this.options = options; + } + + // Don't update while using the menu + if (this.isFocused) { + this.updatePending = true; + return; + } + + this.menuCache.forEach(menuBarMenu => { + this.updateLabels(menuBarMenu.titleElement, menuBarMenu.buttonElement, menuBarMenu.label); + }); + + this.updateOverflowAction(); + + this.setUnfocusedState(); + } + + private registerMnemonic(menuIndex: number, mnemonic: string): void { + this.mnemonics.set(KeyCodeUtils.fromString(mnemonic), menuIndex); + } + + private hideMenubar(): void { + if (this.container.style.display !== 'none') { + this.container.style.display = 'none'; + this._onVisibilityChange.fire(false); + } + } + + private showMenubar(): void { + if (this.container.style.display !== 'flex') { + this.container.style.display = 'flex'; + this._onVisibilityChange.fire(true); + } + } + + private get focusState(): MenubarState { + return this._focusState; + } + + private set focusState(value: MenubarState) { + if (this._focusState >= MenubarState.FOCUSED && value < MenubarState.FOCUSED) { + // Losing focus, update the menu if needed + + if (this.updatePending) { + this.menuUpdater.schedule(); + this.updatePending = false; + } + } + + if (value === this._focusState) { + return; + } + + const isVisible = this.isVisible; + const isOpen = this.isOpen; + const isFocused = this.isFocused; + + this._focusState = value; + + switch (value) { + case MenubarState.HIDDEN: + if (isVisible) { + this.hideMenubar(); + } + + if (isOpen) { + this.cleanupCustomMenu(); + } + + if (isFocused) { + this.focusedMenu = null; + + if (this.focusToReturn) { + this.focusToReturn.focus(); + this.focusToReturn = null; + } + } + + + break; + case MenubarState.VISIBLE: + if (!isVisible) { + this.showMenubar(); + } + + if (isOpen) { + this.cleanupCustomMenu(); + } + + if (isFocused) { + if (this.focusedMenu) { + if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) { + this.overflowMenu.buttonElement.blur(); + } else { + this.menuCache[this.focusedMenu.index].buttonElement.blur(); + } + } + + this.focusedMenu = null; + + if (this.focusToReturn) { + this.focusToReturn.focus(); + this.focusToReturn = null; + } + } + + break; + case MenubarState.FOCUSED: + if (!isVisible) { + this.showMenubar(); + } + + if (isOpen) { + this.cleanupCustomMenu(); + } + + if (this.focusedMenu) { + if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) { + this.overflowMenu.buttonElement.focus(); + } else { + this.menuCache[this.focusedMenu.index].buttonElement.focus(); + } + } + break; + case MenubarState.OPEN: + if (!isVisible) { + this.showMenubar(); + } + + if (this.focusedMenu) { + this.showCustomMenu(this.focusedMenu.index, this.openedViaKeyboard); + } + break; + } + + this._focusState = value; + this._onFocusStateChange.fire(this.focusState >= MenubarState.FOCUSED); + } + + private get isVisible(): boolean { + return this.focusState >= MenubarState.VISIBLE; + } + + private get isFocused(): boolean { + return this.focusState >= MenubarState.FOCUSED; + } + + private get isOpen(): boolean { + return this.focusState >= MenubarState.OPEN; + } + + private get hasOverflow(): boolean { + return this.numMenusShown < this.menuCache.length; + } + + private setUnfocusedState(): void { + if (this.options.visibility === 'toggle' || this.options.visibility === 'hidden') { + this.focusState = MenubarState.HIDDEN; + } else if (this.options.visibility === 'default' && browser.isFullscreen()) { + this.focusState = MenubarState.HIDDEN; + } else { + this.focusState = MenubarState.VISIBLE; + } + + this.ignoreNextMouseUp = false; + this.mnemonicsInUse = false; + this.updateMnemonicVisibility(false); + } + + private focusPrevious(): void { + + if (!this.focusedMenu) { + return; + } + + + let newFocusedIndex = (this.focusedMenu.index - 1 + this.numMenusShown) % this.numMenusShown; + if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) { + newFocusedIndex = this.numMenusShown - 1; + } else if (this.focusedMenu.index === 0 && this.hasOverflow) { + newFocusedIndex = MenuBar.OVERFLOW_INDEX; + } + + if (newFocusedIndex === this.focusedMenu.index) { + return; + } + + if (this.isOpen) { + this.cleanupCustomMenu(); + this.showCustomMenu(newFocusedIndex); + } else if (this.isFocused) { + this.focusedMenu.index = newFocusedIndex; + if (newFocusedIndex === MenuBar.OVERFLOW_INDEX) { + this.overflowMenu.buttonElement.focus(); + } else { + this.menuCache[newFocusedIndex].buttonElement.focus(); + } + } + } + + private focusNext(): void { + if (!this.focusedMenu) { + return; + } + + let newFocusedIndex = (this.focusedMenu.index + 1) % this.numMenusShown; + if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) { + newFocusedIndex = 0; + } else if (this.focusedMenu.index === this.numMenusShown - 1) { + newFocusedIndex = MenuBar.OVERFLOW_INDEX; + } + + if (newFocusedIndex === this.focusedMenu.index) { + return; + } + + if (this.isOpen) { + this.cleanupCustomMenu(); + this.showCustomMenu(newFocusedIndex); + } else if (this.isFocused) { + this.focusedMenu.index = newFocusedIndex; + if (newFocusedIndex === MenuBar.OVERFLOW_INDEX) { + this.overflowMenu.buttonElement.focus(); + } else { + this.menuCache[newFocusedIndex].buttonElement.focus(); + } + } + } + + private updateMnemonicVisibility(visible: boolean): void { + if (this.menuCache) { + this.menuCache.forEach(menuBarMenu => { + if (menuBarMenu.titleElement.children.length) { + let child = menuBarMenu.titleElement.children.item(0) as HTMLElement; + if (child) { + child.style.textDecoration = visible ? 'underline' : null; + } + } + }); + } + } + + private get mnemonicsInUse(): boolean { + return this._mnemonicsInUse; + } + + private set mnemonicsInUse(value: boolean) { + this._mnemonicsInUse = value; + } + + public get onVisibilityChange(): Event { + return this._onVisibilityChange.event; + } + + public get onFocusStateChange(): Event { + return this._onFocusStateChange.event; + } + + private onMenuTriggered(menuIndex: number, clicked: boolean) { + if (this.isOpen) { + if (this.isCurrentMenu(menuIndex)) { + this.setUnfocusedState(); + } else { + this.cleanupCustomMenu(); + this.showCustomMenu(menuIndex, this.openedViaKeyboard); + } + } else { + this.focusedMenu = { index: menuIndex }; + this.openedViaKeyboard = !clicked; + this.focusState = MenubarState.OPEN; + } + } + + private onModifierKeyToggled(modifierKeyStatus: IModifierKeyStatus): void { + const allModifiersReleased = !modifierKeyStatus.altKey && !modifierKeyStatus.ctrlKey && !modifierKeyStatus.shiftKey; + + if (this.options.visibility === 'hidden') { + return; + } + + // Alt key pressed while menu is focused. This should return focus away from the menubar + if (this.isFocused && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.altKey) { + this.setUnfocusedState(); + this.mnemonicsInUse = false; + this.awaitingAltRelease = true; + } + + // Clean alt key press and release + if (allModifiersReleased && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.lastKeyReleased === 'alt') { + if (!this.awaitingAltRelease) { + if (!this.isFocused) { + this.mnemonicsInUse = true; + this.focusedMenu = { index: this.numMenusShown > 0 ? 0 : MenuBar.OVERFLOW_INDEX }; + this.focusState = MenubarState.FOCUSED; + } else if (!this.isOpen) { + this.setUnfocusedState(); + } + } + } + + // Alt key released + if (!modifierKeyStatus.altKey && modifierKeyStatus.lastKeyReleased === 'alt') { + this.awaitingAltRelease = false; + } + + if (this.options.enableMnemonics && this.menuCache && !this.isOpen) { + this.updateMnemonicVisibility((!this.awaitingAltRelease && modifierKeyStatus.altKey) || this.mnemonicsInUse); + } + } + + private isCurrentMenu(menuIndex: number): boolean { + if (!this.focusedMenu) { + return false; + } + + return this.focusedMenu.index === menuIndex; + } + + private cleanupCustomMenu(): void { + if (this.focusedMenu) { + // Remove focus from the menus first + if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) { + this.overflowMenu.buttonElement.focus(); + } else { + this.menuCache[this.focusedMenu.index].buttonElement.focus(); + } + + if (this.focusedMenu.holder) { + DOM.removeClass(this.focusedMenu.holder.parentElement, 'open'); + this.focusedMenu.holder.remove(); + } + + if (this.focusedMenu.widget) { + this.focusedMenu.widget.dispose(); + } + + this.focusedMenu = { index: this.focusedMenu.index }; + } + } + + private showCustomMenu(menuIndex: number, selectFirst = true): void { + const actualMenuIndex = menuIndex >= this.numMenusShown ? MenuBar.OVERFLOW_INDEX : menuIndex; + const customMenu = actualMenuIndex === MenuBar.OVERFLOW_INDEX ? this.overflowMenu : this.menuCache[actualMenuIndex]; + const menuHolder = $('div.menubar-menu-items-holder'); + + DOM.addClass(customMenu.buttonElement, 'open'); + menuHolder.style.top = `${this.container.clientHeight}px`; + menuHolder.style.left = `${customMenu.buttonElement.getBoundingClientRect().left}px`; + + customMenu.buttonElement.appendChild(menuHolder); + + let menuOptions: IMenuOptions = { + getKeyBinding: this.options.getKeybinding, + actionRunner: this.actionRunner, + enableMnemonics: this.mnemonicsInUse && this.options.enableMnemonics, + ariaLabel: customMenu.buttonElement.attributes['aria-label'].value + }; + + let menuWidget = this._register(new Menu(menuHolder, customMenu.actions, menuOptions)); + menuWidget.style(this.menuStyle); + + this._register(menuWidget.onDidCancel(() => { + this.focusState = MenubarState.FOCUSED; + })); + + this._register(menuWidget.onDidBlur(() => { + setTimeout(() => { + this.cleanupCustomMenu(); + }, 100); + })); + + if (actualMenuIndex !== menuIndex) { + menuWidget.trigger(menuIndex - this.numMenusShown); + } else { + menuWidget.focus(selectFirst); + } + + this.focusedMenu = { + index: actualMenuIndex, + holder: menuHolder, + widget: menuWidget + }; + } +} + +type ModifierKey = 'alt' | 'ctrl' | 'shift'; + +interface IModifierKeyStatus { + altKey: boolean; + shiftKey: boolean; + ctrlKey: boolean; + lastKeyPressed?: ModifierKey; + lastKeyReleased?: ModifierKey; +} + + +class ModifierKeyEmitter extends Emitter { + + private _subscriptions: IDisposable[] = []; + private _keyStatus: IModifierKeyStatus; + private static instance: ModifierKeyEmitter; + + private constructor() { + super(); + + this._keyStatus = { + altKey: false, + shiftKey: false, + ctrlKey: false + }; + + this._subscriptions.push(domEvent(document.body, 'keydown')(e => { + const event = new StandardKeyboardEvent(e); + + if (e.altKey && !this._keyStatus.altKey) { + this._keyStatus.lastKeyPressed = 'alt'; + } else if (e.ctrlKey && !this._keyStatus.ctrlKey) { + this._keyStatus.lastKeyPressed = 'ctrl'; + } else if (e.shiftKey && !this._keyStatus.shiftKey) { + this._keyStatus.lastKeyPressed = 'shift'; + } else if (event.keyCode !== KeyCode.Alt) { + this._keyStatus.lastKeyPressed = undefined; + } else { + return; + } + + this._keyStatus.altKey = e.altKey; + this._keyStatus.ctrlKey = e.ctrlKey; + this._keyStatus.shiftKey = e.shiftKey; + + if (this._keyStatus.lastKeyPressed) { + this.fire(this._keyStatus); + } + })); + this._subscriptions.push(domEvent(document.body, 'keyup')(e => { + if (!e.altKey && this._keyStatus.altKey) { + this._keyStatus.lastKeyReleased = 'alt'; + } else if (!e.ctrlKey && this._keyStatus.ctrlKey) { + this._keyStatus.lastKeyReleased = 'ctrl'; + } else if (!e.shiftKey && this._keyStatus.shiftKey) { + this._keyStatus.lastKeyReleased = 'shift'; + } else { + this._keyStatus.lastKeyReleased = undefined; + } + + if (this._keyStatus.lastKeyPressed !== this._keyStatus.lastKeyReleased) { + this._keyStatus.lastKeyPressed = undefined; + } + + this._keyStatus.altKey = e.altKey; + this._keyStatus.ctrlKey = e.ctrlKey; + this._keyStatus.shiftKey = e.shiftKey; + + if (this._keyStatus.lastKeyReleased) { + this.fire(this._keyStatus); + } + })); + this._subscriptions.push(domEvent(document.body, 'mousedown')(e => { + this._keyStatus.lastKeyPressed = undefined; + })); + + + this._subscriptions.push(domEvent(window, 'blur')(e => { + this._keyStatus.lastKeyPressed = undefined; + this._keyStatus.lastKeyReleased = undefined; + this._keyStatus.altKey = false; + this._keyStatus.shiftKey = false; + this._keyStatus.shiftKey = false; + + this.fire(this._keyStatus); + })); + } + + static getInstance() { + if (!ModifierKeyEmitter.instance) { + ModifierKeyEmitter.instance = new ModifierKeyEmitter(); + } + + return ModifierKeyEmitter.instance; + } + + dispose() { + super.dispose(); + this._subscriptions = dispose(this._subscriptions); + } +} \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css index dad60fe5ab3..6a0f061d4dc 100644 --- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css +++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css @@ -156,49 +156,4 @@ .monaco-workbench > .part.titlebar > .window-controls-container .window-icon.window-close:hover { background-color: white; -} - -/* Menubar styles */ - -.monaco-workbench .menubar { - display: flex; - flex-shrink: 1; - box-sizing: border-box; - height: 30px; - -webkit-app-region: no-drag; - overflow: hidden; - flex-wrap: wrap; -} - -.monaco-workbench.fullscreen .menubar { - margin: 0px; - padding: 0px 5px; -} - -.monaco-workbench .menubar > .menubar-menu-button { - align-items: center; - box-sizing: border-box; - padding: 0px 8px; - cursor: default; - -webkit-app-region: no-drag; - zoom: 1; - white-space: nowrap; - outline: 0; -} - -.monaco-workbench .menubar .menubar-menu-items-holder { - position: absolute; - left: 0px; - opacity: 1; - z-index: 2000; -} - -.monaco-workbench .menubar .menubar-menu-items-holder.monaco-menu-container { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif; - outline: 0; - border: none; -} - -.monaco-workbench .menubar .menubar-menu-items-holder.monaco-menu-container :focus { - outline: 0; } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index 8363e7a9057..a44177c55c0 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -4,25 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as browser from 'vs/base/browser/browser'; -import * as strings from 'vs/base/common/strings'; import { IMenubarMenu, IMenubarMenuItemAction, IMenubarMenuItemSubmenu, IMenubarKeybinding, IMenubarService, IMenubarData } from 'vs/platform/menubar/common/menubar'; import { IMenuService, MenuId, IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions'; import { registerThemingParticipant, ITheme, ICssStyleCollector, IThemeService } from 'vs/platform/theme/common/themeService'; import { IWindowService, MenuBarVisibility, IWindowsService, getTitleBarStyle } from 'vs/platform/windows/common/windows'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ActionRunner, IActionRunner, IAction, Action } from 'vs/base/common/actions'; +import { IAction, Action } from 'vs/base/common/actions'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import * as DOM from 'vs/base/browser/dom'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { isMacintosh, isLinux } from 'vs/base/common/platform'; -import { Menu, IMenuOptions, SubmenuAction, MENU_MNEMONIC_REGEX, cleanMnemonic, MENU_ESCAPED_MNEMONIC_REGEX } from 'vs/base/browser/ui/menu/menu'; -import { KeyCode, KeyCodeUtils } from 'vs/base/common/keyCodes'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { Event, Emitter } from 'vs/base/common/event'; -import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle'; -import { domEvent } from 'vs/base/browser/event'; +import { Disposable } from 'vs/base/common/lifecycle'; import { IRecentlyOpened } from 'vs/platform/history/common/history'; import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -30,28 +24,13 @@ import { MENUBAR_SELECTION_FOREGROUND, MENUBAR_SELECTION_BACKGROUND, MENUBAR_SEL import { URI } from 'vs/base/common/uri'; import { ILabelService } from 'vs/platform/label/common/label'; import { IUpdateService, StateType } from 'vs/platform/update/common/update'; -import { Gesture, EventType, GestureEvent } from 'vs/base/browser/touch'; -import { attachMenuStyler } from 'vs/platform/theme/common/styler'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; - -const $ = DOM.$; - -interface CustomMenu { - title: string; - buttonElement: HTMLElement; - titleElement: HTMLElement; - actions?: IAction[]; -} - -enum MenubarState { - HIDDEN, - VISIBLE, - FOCUSED, - OPEN -} +import { MenuBar } from 'vs/base/browser/ui/menu/menubar'; +import { SubmenuAction } from 'vs/base/browser/ui/menu/menu'; +import { attachMenuStyler } from 'vs/platform/theme/common/styler'; export class MenubarControl extends Disposable { @@ -90,28 +69,10 @@ export class MenubarControl extends Disposable { 'Help': nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help") }; - private focusedMenu: { - index: number; - holder?: HTMLElement; - widget?: Menu; - }; - - private customMenus: CustomMenu[]; - + private menubar: MenuBar; private menuUpdater: RunOnceScheduler; - private actionRunner: IActionRunner; - private focusToReturn: HTMLElement; private container: HTMLElement; private recentlyOpened: IRecentlyOpened; - private updatePending: boolean; - private _focusState: MenubarState; - - // Input-related - private _mnemonicsInUse: boolean; - private openedViaKeyboard: boolean; - private awaitingAltRelease: boolean; - private ignoreNextMouseUp: boolean; - private mnemonics: Map; private _onVisibilityChange: Emitter; private _onFocusStateChange: Emitter; @@ -152,24 +113,18 @@ export class MenubarControl extends Disposable { this.topLevelMenus['Preferences'] = this._register(this.menuService.createMenu(MenuId.MenubarPreferencesMenu, this.contextKeyService)); } - this.menuUpdater = this._register(new RunOnceScheduler(() => this.doSetupMenubar(), 200)); - - this.actionRunner = this._register(new ActionRunner()); - this._register(this.actionRunner.onDidBeforeRun(() => { - this.setUnfocusedState(); - })); - - this._onVisibilityChange = this._register(new Emitter()); - this._onFocusStateChange = this._register(new Emitter()); + this.menuUpdater = this._register(new RunOnceScheduler(() => this.doUpdateMenubar(false), 200)); if (isMacintosh || this.currentTitlebarStyleSetting !== 'custom') { for (let topLevelMenuName of Object.keys(this.topLevelMenus)) { - this._register(this.topLevelMenus[topLevelMenuName].onDidChange(() => this.setupMenubar())); + this._register(this.topLevelMenus[topLevelMenuName].onDidChange(() => this.updateMenubar())); } - this.doSetupMenubar(); + + this.doUpdateMenubar(true); } - this._focusState = MenubarState.HIDDEN; + this._onVisibilityChange = this._register(new Emitter()); + this._onFocusStateChange = this._register(new Emitter()); this.windowService.getRecentlyOpened().then((recentlyOpened) => { this.recentlyOpened = recentlyOpened; @@ -219,231 +174,31 @@ export class MenubarControl extends Disposable { return getTitleBarStyle(this.configurationService, this.environmentService); } - private get focusState(): MenubarState { - return this._focusState; - } - - private set focusState(value: MenubarState) { - if (this._focusState >= MenubarState.FOCUSED && value < MenubarState.FOCUSED) { - // Losing focus, update the menu if needed - - if (this.updatePending) { - this.menuUpdater.schedule(); - this.updatePending = false; - } - } - - if (value === this._focusState) { - return; - } - - const isVisible = this.isVisible; - const isOpen = this.isOpen; - const isFocused = this.isFocused; - - this._focusState = value; - - switch (value) { - case MenubarState.HIDDEN: - if (isVisible) { - this.hideMenubar(); - } - - if (isOpen) { - this.cleanupCustomMenu(); - } - - if (isFocused) { - this.focusedMenu = null; - - if (this.focusToReturn) { - this.focusToReturn.focus(); - this.focusToReturn = null; - } - } - - - break; - case MenubarState.VISIBLE: - if (!isVisible) { - this.showMenubar(); - } - - if (isOpen) { - this.cleanupCustomMenu(); - } - - if (isFocused) { - if (this.focusedMenu) { - this.customMenus[this.focusedMenu.index].buttonElement.blur(); - } - - this.focusedMenu = null; - - if (this.focusToReturn) { - this.focusToReturn.focus(); - this.focusToReturn = null; - } - } - - break; - case MenubarState.FOCUSED: - if (!isVisible) { - this.showMenubar(); - } - - if (isOpen) { - this.cleanupCustomMenu(); - } - - if (this.focusedMenu) { - this.customMenus[this.focusedMenu.index].buttonElement.focus(); - } - break; - case MenubarState.OPEN: - if (!isVisible) { - this.showMenubar(); - } - - if (this.focusedMenu) { - this.showCustomMenu(this.focusedMenu.index, this.openedViaKeyboard); - } - break; - } - - this._focusState = value; - this._onFocusStateChange.fire(this.focusState >= MenubarState.FOCUSED); - } - - private get mnemonicsInUse(): boolean { - return this._mnemonicsInUse; - } - - private set mnemonicsInUse(value: boolean) { - this._mnemonicsInUse = value; - } - - private get isVisible(): boolean { - return this.focusState >= MenubarState.VISIBLE; - } - - private get isFocused(): boolean { - return this.focusState >= MenubarState.FOCUSED; - } - - private get isOpen(): boolean { - return this.focusState >= MenubarState.OPEN; - } - - private onDidChangeFullscreen(): void { - this.setUnfocusedState(); - } - private onDidChangeWindowFocus(hasFocus: boolean): void { if (this.container) { if (hasFocus) { DOM.removeClass(this.container, 'inactive'); } else { DOM.addClass(this.container, 'inactive'); - this.setUnfocusedState(); - this.awaitingAltRelease = false; + this.menubar.blur(); } } } private onConfigurationUpdated(event: IConfigurationChangeEvent): void { if (this.keys.some(key => event.affectsConfiguration(key))) { - this.setupMenubar(); + this.updateMenubar(); } if (event.affectsConfiguration('window.menuBarVisibility')) { - this.setUnfocusedState(); this.detectAndRecommendCustomTitlebar(); } } - private setUnfocusedState(): void { - if (this.currentMenubarVisibility === 'toggle' || this.currentMenubarVisibility === 'hidden') { - this.focusState = MenubarState.HIDDEN; - } else if (this.currentMenubarVisibility === 'default' && browser.isFullscreen()) { - this.focusState = MenubarState.HIDDEN; - } else { - this.focusState = MenubarState.VISIBLE; - } - - this.ignoreNextMouseUp = false; - this.mnemonicsInUse = false; - this.updateMnemonicVisibility(false); - } - - private hideMenubar(): void { - if (this.container.style.display !== 'none') { - this.container.style.display = 'none'; - this._onVisibilityChange.fire(false); - } - } - - private showMenubar(): void { - if (this.container.style.display !== 'flex') { - this.container.style.display = 'flex'; - this._onVisibilityChange.fire(true); - } - } - - private onModifierKeyToggled(modifierKeyStatus: IModifierKeyStatus): void { - const allModifiersReleased = !modifierKeyStatus.altKey && !modifierKeyStatus.ctrlKey && !modifierKeyStatus.shiftKey; - - if (this.currentMenubarVisibility === 'hidden') { - return; - } - - // Alt key pressed while menu is focused. This should return focus away from the menubar - if (this.isFocused && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.altKey) { - this.setUnfocusedState(); - this.mnemonicsInUse = false; - this.awaitingAltRelease = true; - } - - // Clean alt key press and release - if (allModifiersReleased && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.lastKeyReleased === 'alt') { - if (!this.awaitingAltRelease) { - if (!this.isFocused) { - this.mnemonicsInUse = true; - this.focusedMenu = { index: 0 }; - this.focusState = MenubarState.FOCUSED; - } else if (!this.isOpen) { - this.setUnfocusedState(); - } - } - } - - // Alt key released - if (!modifierKeyStatus.altKey && modifierKeyStatus.lastKeyReleased === 'alt') { - this.awaitingAltRelease = false; - } - - if (this.currentEnableMenuBarMnemonics && this.customMenus && !this.isOpen) { - this.updateMnemonicVisibility((!this.awaitingAltRelease && modifierKeyStatus.altKey) || this.mnemonicsInUse); - } - } - - private updateMnemonicVisibility(visible: boolean): void { - if (this.customMenus) { - this.customMenus.forEach(customMenu => { - if (customMenu.titleElement.children.length) { - let child = customMenu.titleElement.children.item(0) as HTMLElement; - if (child) { - child.style.textDecoration = visible ? 'underline' : null; - } - } - }); - } - } - private onRecentlyOpenedChange(): void { this.windowService.getRecentlyOpened().then(recentlyOpened => { this.recentlyOpened = recentlyOpened; - this.setupMenubar(); + this.updateMenubar(); }); } @@ -487,30 +242,30 @@ export class MenubarControl extends Disposable { this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e))); // Listen to update service - this.updateService.onStateChange(() => this.setupMenubar()); + this.updateService.onStateChange(() => this.updateMenubar()); // Listen for changes in recently opened menu this._register(this.windowsService.onRecentlyOpenedChange(() => { this.onRecentlyOpenedChange(); })); // Listen to keybindings change - this._register(this.keybindingService.onDidUpdateKeybindings(() => this.setupMenubar())); + this._register(this.keybindingService.onDidUpdateKeybindings(() => this.updateMenubar())); // These listeners only apply when the custom menubar is being used if (!isMacintosh && this.currentTitlebarStyleSetting === 'custom') { - // Listen to fullscreen changes - this._register(browser.onDidChangeFullscreen(() => this.onDidChangeFullscreen())); - - // Listen for alt key presses - this._register(ModifierKeyEmitter.getInstance(this.windowService).event(this.onModifierKeyToggled, this)); - // Listen for window focus changes this._register(this.windowService.onDidChangeFocus(e => this.onDidChangeWindowFocus(e))); + + this._register(this.windowService.onDidChangeMaximize(e => this.updateMenubar())); + + this._register(DOM.addDisposableListener(window, DOM.EventType.RESIZE, () => { + this.menubar.blur(); + })); } } - private doSetupMenubar(): void { + private doUpdateMenubar(firstTime: boolean): void { if (!isMacintosh && this.currentTitlebarStyleSetting === 'custom') { - this.setupCustomMenubar(); + this.setupCustomMenubar(firstTime); } else { // Send menus to main process to be rendered by Electron const menubarData = { menus: {}, keybindings: {} }; @@ -520,14 +275,10 @@ export class MenubarControl extends Disposable { } } - private setupMenubar(): void { + private updateMenubar(): void { this.menuUpdater.schedule(); } - private registerMnemonic(menuIndex: number, mnemonic: string): void { - this.mnemonics.set(KeyCodeUtils.fromString(mnemonic), menuIndex); - } - private calculateActionLabel(action: IAction | IMenubarMenuItemAction): string { let label = action.label; switch (action.id) { @@ -673,301 +424,69 @@ export class MenubarControl extends Disposable { } } - private setupCustomMenubar(): void { - // Don't update while using the menu - if (this.isFocused) { - this.updatePending = true; - return; - } - - this.container.attributes['role'] = 'menubar'; - - const firstTimeSetup = this.customMenus === undefined; - if (firstTimeSetup) { - this.customMenus = []; - this.mnemonics = new Map(); - } - - let idx = 0; - - for (let menuTitle of Object.keys(this.topLevelMenus)) { - const menu: IMenu = this.topLevelMenus[menuTitle]; - let menuIndex = idx++; - const cleanMenuLabel = cleanMnemonic(this.topLevelTitles[menuTitle]); - - // Create the top level menu button element - if (firstTimeSetup) { - - const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': -1, 'aria-label': cleanMenuLabel, 'aria-haspopup': true }); - const titleElement = $('div.menubar-menu-title', { 'role': 'none', 'aria-hidden': true }); - - buttonElement.appendChild(titleElement); - this.container.appendChild(buttonElement); - - this.customMenus.push({ - title: menuTitle, - buttonElement: buttonElement, - titleElement: titleElement - }); - } - - // Update the button label to reflect mnemonics - this.customMenus[menuIndex].titleElement.innerHTML = this.currentEnableMenuBarMnemonics ? - strings.escape(this.topLevelTitles[menuTitle]).replace(MENU_ESCAPED_MNEMONIC_REGEX, '') : - cleanMenuLabel; - - let mnemonicMatches = MENU_MNEMONIC_REGEX.exec(this.topLevelTitles[menuTitle]); - - // Register mnemonics - if (mnemonicMatches) { - let mnemonic = !!mnemonicMatches[1] ? mnemonicMatches[1] : mnemonicMatches[2]; - - if (firstTimeSetup) { - this.registerMnemonic(menuIndex, mnemonic); + private setupCustomMenubar(firstTime: boolean): void { + if (firstTime) { + this.menubar = this._register(new MenuBar( + this.container, { + enableMnemonics: this.currentEnableMenuBarMnemonics, + visibility: this.currentMenubarVisibility, + getKeybinding: (action) => this.keybindingService.lookupKeybinding(action.id), } + )); - if (this.currentEnableMenuBarMnemonics) { - this.customMenus[menuIndex].buttonElement.setAttribute('aria-keyshortcuts', 'Alt+' + mnemonic.toLocaleLowerCase()); - } else { - this.customMenus[menuIndex].buttonElement.removeAttribute('aria-keyshortcuts'); - } - } + this._register(this.menubar.onFocusStateChange(e => this._onFocusStateChange.fire(e))); + this._register(this.menubar.onVisibilityChange(e => this._onVisibilityChange.fire(e))); - // Update the menu actions - const updateActions = (menu: IMenu, target: IAction[]) => { - target.splice(0); - let groups = menu.getActions(); - for (let group of groups) { - const [, actions] = group; - - for (let action of actions) { - this.insertActionsBefore(action, target); - if (action instanceof SubmenuItemAction) { - const submenu = this.menuService.createMenu(action.item.submenu, this.contextKeyService); - const submenuActions: SubmenuAction[] = []; - updateActions(submenu, submenuActions); - target.push(new SubmenuAction(action.label, submenuActions)); - submenu.dispose(); - } else { - action.label = this.calculateActionLabel(action); - target.push(action); - } + this._register(attachMenuStyler(this.menubar, this.themeService)); + } else { + this.menubar.update({ enableMnemonics: this.currentEnableMenuBarMnemonics, visibility: this.currentMenubarVisibility, getKeybinding: (action) => this.keybindingService.lookupKeybinding(action.id) }); + } + + // Update the menu actions + const updateActions = (menu: IMenu, target: IAction[]) => { + target.splice(0); + let groups = menu.getActions(); + for (let group of groups) { + const [, actions] = group; + + for (let action of actions) { + this.insertActionsBefore(action, target); + if (action instanceof SubmenuItemAction) { + const submenu = this.menuService.createMenu(action.item.submenu, this.contextKeyService); + const submenuActions: SubmenuAction[] = []; + updateActions(submenu, submenuActions); + target.push(new SubmenuAction(action.label, submenuActions)); + submenu.dispose(); + } else { + action.label = this.calculateActionLabel(action); + target.push(action); } - - target.push(new Separator()); } - target.pop(); - }; - - this.customMenus[menuIndex].actions = []; - if (firstTimeSetup) { - this._register(menu.onDidChange(() => updateActions(menu, this.customMenus[menuIndex].actions))); + target.push(new Separator()); } - updateActions(menu, this.customMenus[menuIndex].actions); - - if (firstTimeSetup) { - this._register(DOM.addDisposableListener(this.customMenus[menuIndex].buttonElement, DOM.EventType.KEY_UP, (e) => { - let event = new StandardKeyboardEvent(e); - let eventHandled = true; - - if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter)) && !this.isOpen) { - this.focusedMenu = { index: menuIndex }; - this.openedViaKeyboard = true; - this.focusState = MenubarState.OPEN; - } else { - eventHandled = false; - } - - if (eventHandled) { - event.preventDefault(); - event.stopPropagation(); - } - })); - - Gesture.addTarget(this.customMenus[menuIndex].buttonElement); - this._register(DOM.addDisposableListener(this.customMenus[menuIndex].buttonElement, EventType.Tap, (e: GestureEvent) => { - // Ignore this touch if the menu is touched - if (this.isOpen && this.focusedMenu.holder && DOM.isAncestor(e.initialTarget as HTMLElement, this.focusedMenu.holder)) { - return; - } - - this.ignoreNextMouseUp = false; - this.onMenuTriggered(menuIndex, true); - - e.preventDefault(); - e.stopPropagation(); - })); - - this._register(DOM.addDisposableListener(this.customMenus[menuIndex].buttonElement, DOM.EventType.MOUSE_DOWN, (e) => { - if (!this.isOpen) { - // Open the menu with mouse down and ignore the following mouse up event - this.ignoreNextMouseUp = true; - this.onMenuTriggered(menuIndex, true); - } else { - this.ignoreNextMouseUp = false; - } - - e.preventDefault(); - e.stopPropagation(); - })); - - this._register(DOM.addDisposableListener(this.customMenus[menuIndex].buttonElement, DOM.EventType.MOUSE_UP, (e) => { - if (!this.ignoreNextMouseUp) { - if (this.isFocused) { - this.onMenuTriggered(menuIndex, true); - } - } else { - this.ignoreNextMouseUp = false; - } - })); + target.pop(); + }; - this._register(DOM.addDisposableListener(this.customMenus[menuIndex].buttonElement, DOM.EventType.MOUSE_ENTER, () => { - if (this.isOpen && !this.isCurrentMenu(menuIndex)) { - this.customMenus[menuIndex].buttonElement.focus(); - this.cleanupCustomMenu(); - this.showCustomMenu(menuIndex, false); - } else if (this.isFocused && !this.isOpen) { - this.focusedMenu = { index: menuIndex }; - this.customMenus[menuIndex].buttonElement.focus(); - } + for (let title of Object.keys(this.topLevelMenus)) { + const menu = this.topLevelMenus[title]; + if (firstTime) { + this._register(menu.onDidChange(() => { + const actions = []; + updateActions(menu, actions); + this.menubar.updateMenu({ actions: actions, label: this.topLevelTitles[title] }); })); } - } - - if (firstTimeSetup) { - this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_DOWN, (e) => { - let event = new StandardKeyboardEvent(e); - let eventHandled = true; - const key = !!e.key ? KeyCodeUtils.fromString(e.key) : KeyCode.Unknown; - - if (event.equals(KeyCode.LeftArrow)) { - this.focusPrevious(); - } else if (event.equals(KeyCode.RightArrow)) { - this.focusNext(); - } else if (event.equals(KeyCode.Escape) && this.isFocused && !this.isOpen) { - this.setUnfocusedState(); - } else if (!this.isOpen && !event.ctrlKey && this.currentEnableMenuBarMnemonics && this.mnemonicsInUse && this.mnemonics.has(key)) { - const menuIndex = this.mnemonics.get(key); - this.onMenuTriggered(menuIndex, false); - } else { - eventHandled = false; - } - - if (eventHandled) { - event.preventDefault(); - event.stopPropagation(); - } - })); - - this._register(DOM.addDisposableListener(window, DOM.EventType.MOUSE_DOWN, () => { - // This mouse event is outside the menubar so it counts as a focus out - if (this.isFocused) { - this.setUnfocusedState(); - } - })); - - this._register(DOM.addDisposableListener(this.container, DOM.EventType.FOCUS_IN, (e) => { - let event = e as FocusEvent; - - if (event.relatedTarget) { - if (!this.container.contains(event.relatedTarget as HTMLElement)) { - this.focusToReturn = event.relatedTarget as HTMLElement; - } - } - })); - this._register(DOM.addDisposableListener(this.container, DOM.EventType.FOCUS_OUT, (e) => { - let event = e as FocusEvent; + const actions = []; + updateActions(menu, actions); - if (event.relatedTarget) { - if (!this.container.contains(event.relatedTarget as HTMLElement)) { - this.focusToReturn = null; - this.setUnfocusedState(); - } - } - })); - - this._register(DOM.addDisposableListener(window, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => { - if (!this.currentEnableMenuBarMnemonics || !e.altKey || e.ctrlKey || e.defaultPrevented) { - return; - } - - const key = KeyCodeUtils.fromString(e.key); - if (!this.mnemonics.has(key)) { - return; - } - - // Prevent conflicts with keybindings - const standardKeyboardEvent = new StandardKeyboardEvent(e); - const resolvedResult = this.keybindingService.softDispatch(standardKeyboardEvent, standardKeyboardEvent.target); - if (resolvedResult) { - return; - } - - this.mnemonicsInUse = true; - this.updateMnemonicVisibility(true); - - const menuIndex = this.mnemonics.get(key); - this.onMenuTriggered(menuIndex, false); - })); - } - } - - private onMenuTriggered(menuIndex: number, clicked: boolean) { - if (this.isOpen) { - if (this.isCurrentMenu(menuIndex)) { - this.setUnfocusedState(); + if (!firstTime) { + this.menubar.updateMenu({ actions: actions, label: this.topLevelTitles[title] }); } else { - this.cleanupCustomMenu(); - this.showCustomMenu(menuIndex, this.openedViaKeyboard); + this.menubar.push({ actions: actions, label: this.topLevelTitles[title] }); } - } else { - this.focusedMenu = { index: menuIndex }; - this.openedViaKeyboard = !clicked; - this.focusState = MenubarState.OPEN; - } - } - - private focusPrevious(): void { - - if (!this.focusedMenu) { - return; - } - - let newFocusedIndex = (this.focusedMenu.index - 1 + this.customMenus.length) % this.customMenus.length; - - if (newFocusedIndex === this.focusedMenu.index) { - return; - } - - if (this.isOpen) { - this.cleanupCustomMenu(); - this.showCustomMenu(newFocusedIndex); - } else if (this.isFocused) { - this.focusedMenu.index = newFocusedIndex; - this.customMenus[newFocusedIndex].buttonElement.focus(); - } - } - - private focusNext(): void { - if (!this.focusedMenu) { - return; - } - - let newFocusedIndex = (this.focusedMenu.index + 1) % this.customMenus.length; - - if (newFocusedIndex === this.focusedMenu.index) { - return; - } - - if (this.isOpen) { - this.cleanupCustomMenu(); - this.showCustomMenu(newFocusedIndex); - } else if (this.isFocused) { - this.focusedMenu.index = newFocusedIndex; - this.customMenus[newFocusedIndex].buttonElement.focus(); } } @@ -1069,71 +588,6 @@ export class MenubarControl extends Disposable { return true; } - private isCurrentMenu(menuIndex: number): boolean { - if (!this.focusedMenu) { - return false; - } - - return this.focusedMenu.index === menuIndex; - } - - private cleanupCustomMenu(): void { - if (this.focusedMenu) { - // Remove focus from the menus first - this.customMenus[this.focusedMenu.index].buttonElement.focus(); - - if (this.focusedMenu.holder) { - DOM.removeClass(this.focusedMenu.holder.parentElement, 'open'); - this.focusedMenu.holder.remove(); - } - - if (this.focusedMenu.widget) { - this.focusedMenu.widget.dispose(); - } - - this.focusedMenu = { index: this.focusedMenu.index }; - } - } - - private showCustomMenu(menuIndex: number, selectFirst = true): void { - const customMenu = this.customMenus[menuIndex]; - const menuHolder = $('div.menubar-menu-items-holder'); - - DOM.addClass(customMenu.buttonElement, 'open'); - menuHolder.style.top = `${this.container.clientHeight}px`; - menuHolder.style.left = `${customMenu.buttonElement.getBoundingClientRect().left}px`; - - customMenu.buttonElement.appendChild(menuHolder); - - let menuOptions: IMenuOptions = { - getKeyBinding: (action) => this.keybindingService.lookupKeybinding(action.id), - actionRunner: this.actionRunner, - enableMnemonics: this.mnemonicsInUse && this.currentEnableMenuBarMnemonics, - ariaLabel: customMenu.buttonElement.attributes['aria-label'].value - }; - - let menuWidget = this._register(new Menu(menuHolder, customMenu.actions, menuOptions)); - this._register(attachMenuStyler(menuWidget, this.themeService)); - - this._register(menuWidget.onDidCancel(() => { - this.focusState = MenubarState.FOCUSED; - })); - - this._register(menuWidget.onDidBlur(() => { - setTimeout(() => { - this.cleanupCustomMenu(); - }, 100); - })); - - menuWidget.focus(selectFirst); - - this.focusedMenu = { - index: menuIndex, - holder: menuHolder, - widget: menuWidget - }; - } - public get onVisibilityChange(): Event { return this._onVisibilityChange.event; } @@ -1147,21 +601,11 @@ export class MenubarControl extends Disposable { this.container.style.height = `${dimension.height}px`; } - if (!this.isVisible) { - this.hideMenubar(); - } else { - this.showMenubar(); - } + this.menubar.update({ enableMnemonics: this.currentEnableMenuBarMnemonics, visibility: this.currentMenubarVisibility, getKeybinding: (action) => this.keybindingService.lookupKeybinding(action.id) }); } public getMenubarItemsDimensions(): DOM.Dimension { - if (this.customMenus) { - const left = this.customMenus[0].buttonElement.getBoundingClientRect().left; - const right = this.customMenus[this.customMenus.length - 1].buttonElement.getBoundingClientRect().right; - return new DOM.Dimension(right - left, this.container.clientHeight); - } - - return new DOM.Dimension(0, 0); + return new DOM.Dimension(this.menubar.getWidth(), this.menubar.getHeight()); } public create(parent: HTMLElement): HTMLElement { @@ -1169,10 +613,9 @@ export class MenubarControl extends Disposable { // Build the menubar if (this.container) { - this.doSetupMenubar(); if (!isMacintosh && this.currentTitlebarStyleSetting === 'custom') { - this.setUnfocusedState(); + this.doUpdateMenubar(true); } } @@ -1187,6 +630,10 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { .monaco-workbench .menubar > .menubar-menu-button { color: ${menubarActiveWindowFgColor}; } + + .monaco-workbench .menubar .toolbar-toggle-more { + background-color: ${menubarActiveWindowFgColor} + } `); } @@ -1196,6 +643,10 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { .monaco-workbench .menubar.inactive > .menubar-menu-button { color: ${menubarInactiveWindowFgColor}; } + + .monaco-workbench .menubar.inactive > .menubar-menu-button .toolbar-toggle-more { + background-color: ${menubarInactiveWindowFgColor} + } `); } @@ -1208,6 +659,12 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { .monaco-workbench .menubar:not(:focus-within) > .menubar-menu-button:hover { color: ${menubarSelectedFgColor}; } + + .monaco-workbench .menubar > .menubar-menu-button.open .toolbar-toggle-more, + .monaco-workbench .menubar > .menubar-menu-button:focus .toolbar-toggle-more, + .monaco-workbench .menubar:not(:focus-within) > .menubar-menu-button:hover .toolbar-toggle-more { + background-color: ${menubarSelectedFgColor} + } `); } @@ -1243,106 +700,3 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { `); } }); - -type ModifierKey = 'alt' | 'ctrl' | 'shift'; - -interface IModifierKeyStatus { - altKey: boolean; - shiftKey: boolean; - ctrlKey: boolean; - lastKeyPressed?: ModifierKey; - lastKeyReleased?: ModifierKey; -} - - -class ModifierKeyEmitter extends Emitter { - - private _subscriptions: IDisposable[] = []; - private _keyStatus: IModifierKeyStatus; - private static instance: ModifierKeyEmitter; - - private constructor(windowService: IWindowService) { - super(); - - this._keyStatus = { - altKey: false, - shiftKey: false, - ctrlKey: false - }; - - this._subscriptions.push(domEvent(document.body, 'keydown')(e => { - const event = new StandardKeyboardEvent(e); - - if (e.altKey && !this._keyStatus.altKey) { - this._keyStatus.lastKeyPressed = 'alt'; - } else if (e.ctrlKey && !this._keyStatus.ctrlKey) { - this._keyStatus.lastKeyPressed = 'ctrl'; - } else if (e.shiftKey && !this._keyStatus.shiftKey) { - this._keyStatus.lastKeyPressed = 'shift'; - } else if (event.keyCode !== KeyCode.Alt) { - this._keyStatus.lastKeyPressed = undefined; - } else { - return; - } - - this._keyStatus.altKey = e.altKey; - this._keyStatus.ctrlKey = e.ctrlKey; - this._keyStatus.shiftKey = e.shiftKey; - - if (this._keyStatus.lastKeyPressed) { - this.fire(this._keyStatus); - } - })); - this._subscriptions.push(domEvent(document.body, 'keyup')(e => { - if (!e.altKey && this._keyStatus.altKey) { - this._keyStatus.lastKeyReleased = 'alt'; - } else if (!e.ctrlKey && this._keyStatus.ctrlKey) { - this._keyStatus.lastKeyReleased = 'ctrl'; - } else if (!e.shiftKey && this._keyStatus.shiftKey) { - this._keyStatus.lastKeyReleased = 'shift'; - } else { - this._keyStatus.lastKeyReleased = undefined; - } - - if (this._keyStatus.lastKeyPressed !== this._keyStatus.lastKeyReleased) { - this._keyStatus.lastKeyPressed = undefined; - } - - this._keyStatus.altKey = e.altKey; - this._keyStatus.ctrlKey = e.ctrlKey; - this._keyStatus.shiftKey = e.shiftKey; - - if (this._keyStatus.lastKeyReleased) { - this.fire(this._keyStatus); - } - })); - this._subscriptions.push(domEvent(document.body, 'mousedown')(e => { - this._keyStatus.lastKeyPressed = undefined; - })); - - this._subscriptions.push(windowService.onDidChangeFocus(focused => { - if (!focused) { - this._keyStatus.lastKeyPressed = undefined; - this._keyStatus.lastKeyReleased = undefined; - this._keyStatus.altKey = false; - this._keyStatus.shiftKey = false; - this._keyStatus.shiftKey = false; - - this.fire(this._keyStatus); - } - })); - } - - static getInstance(windowService: IWindowService) { - if (!ModifierKeyEmitter.instance) { - ModifierKeyEmitter.instance = new ModifierKeyEmitter(windowService); - } - - return ModifierKeyEmitter.instance; - } - - dispose() { - super.dispose(); - this._subscriptions = dispose(this._subscriptions); - } -} -- GitLab