diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts
index c6c24af7aee..08a67171915 100644
--- a/src/vs/base/browser/ui/actionbar/actionbar.ts
+++ b/src/vs/base/browser/ui/actionbar/actionbar.ts
@@ -675,28 +675,12 @@ export class ActionBar extends Disposable implements IActionRunner {
return this.items.length === 0;
}
- focus(index?: number): void;
- focus(selectFirst?: boolean): void;
- focus(arg?: any): void {
- let selectFirst: boolean = false;
- let index: number | undefined = void 0;
- if (arg === undefined) {
- selectFirst = true;
- } else if (typeof arg === 'number') {
- index = arg;
- } else if (typeof arg === 'boolean') {
- selectFirst = arg;
- }
-
+ focus(selectFirst?: boolean): void {
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
deleted file mode 100644
index e3f85623356..00000000000
--- a/src/vs/base/browser/ui/menu/ellipsis.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ 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 b896e7bed28..c24040ba3a3 100644
--- a/src/vs/base/browser/ui/menu/menu.css
+++ b/src/vs/base/browser/ui/menu/menu.css
@@ -145,64 +145,4 @@
.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 99aa8227783..5abc7332d4a 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, removeClasses } from 'vs/base/browser/dom';
+import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, hasClass, addDisposableListener, removeClass, append, $, addClasses, getClientArea, 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();
}
- get onScroll(): Event {
+ public get onScroll(): Event {
return this._onScroll.event;
}
@@ -217,20 +217,6 @@ 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);
@@ -299,6 +285,10 @@ export class Menu extends ActionBar {
return menuActionItem;
}
}
+
+ public focus(selectFirst = true) {
+ super.focus(selectFirst);
+ }
}
interface IMenuItemOptions extends IActionItemOptions {
@@ -581,11 +571,6 @@ 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);
@@ -610,22 +595,8 @@ 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.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.submenuContainer.style.left = `${getClientArea(this.element).width}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);
@@ -648,6 +619,10 @@ 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
deleted file mode 100644
index 5d916ccaf05..00000000000
--- a/src/vs/base/browser/ui/menu/menubar.ts
+++ /dev/null
@@ -1,964 +0,0 @@
-/*---------------------------------------------------------------------------------------------
- * 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, '$1') :
- 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 6a0f061d4dc..dad60fe5ab3 100644
--- a/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css
+++ b/src/vs/workbench/browser/parts/titlebar/media/titlebarpart.css
@@ -156,4 +156,49 @@
.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 a44177c55c0..8363e7a9057 100644
--- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts
+++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts
@@ -4,19 +4,25 @@
*--------------------------------------------------------------------------------------------*/
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 { IAction, Action } from 'vs/base/common/actions';
+import { ActionRunner, IActionRunner, 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 { Disposable } from 'vs/base/common/lifecycle';
+import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle';
+import { domEvent } from 'vs/base/browser/event';
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';
@@ -24,13 +30,28 @@ 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';
-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';
+
+const $ = DOM.$;
+
+interface CustomMenu {
+ title: string;
+ buttonElement: HTMLElement;
+ titleElement: HTMLElement;
+ actions?: IAction[];
+}
+
+enum MenubarState {
+ HIDDEN,
+ VISIBLE,
+ FOCUSED,
+ OPEN
+}
export class MenubarControl extends Disposable {
@@ -69,10 +90,28 @@ export class MenubarControl extends Disposable {
'Help': nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help")
};
- private menubar: MenuBar;
+ private focusedMenu: {
+ index: number;
+ holder?: HTMLElement;
+ widget?: Menu;
+ };
+
+ private customMenus: CustomMenu[];
+
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;
@@ -113,19 +152,25 @@ export class MenubarControl extends Disposable {
this.topLevelMenus['Preferences'] = this._register(this.menuService.createMenu(MenuId.MenubarPreferencesMenu, this.contextKeyService));
}
- this.menuUpdater = this._register(new RunOnceScheduler(() => this.doUpdateMenubar(false), 200));
+ this.menuUpdater = this._register(new RunOnceScheduler(() => this.doSetupMenubar(), 200));
- if (isMacintosh || this.currentTitlebarStyleSetting !== 'custom') {
- for (let topLevelMenuName of Object.keys(this.topLevelMenus)) {
- this._register(this.topLevelMenus[topLevelMenuName].onDidChange(() => this.updateMenubar()));
- }
-
- this.doUpdateMenubar(true);
- }
+ 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());
+ if (isMacintosh || this.currentTitlebarStyleSetting !== 'custom') {
+ for (let topLevelMenuName of Object.keys(this.topLevelMenus)) {
+ this._register(this.topLevelMenus[topLevelMenuName].onDidChange(() => this.setupMenubar()));
+ }
+ this.doSetupMenubar();
+ }
+
+ this._focusState = MenubarState.HIDDEN;
+
this.windowService.getRecentlyOpened().then((recentlyOpened) => {
this.recentlyOpened = recentlyOpened;
});
@@ -174,31 +219,231 @@ 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.menubar.blur();
+ this.setUnfocusedState();
+ this.awaitingAltRelease = false;
}
}
}
private onConfigurationUpdated(event: IConfigurationChangeEvent): void {
if (this.keys.some(key => event.affectsConfiguration(key))) {
- this.updateMenubar();
+ this.setupMenubar();
}
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.updateMenubar();
+ this.setupMenubar();
});
}
@@ -242,30 +487,30 @@ export class MenubarControl extends Disposable {
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e)));
// Listen to update service
- this.updateService.onStateChange(() => this.updateMenubar());
+ this.updateService.onStateChange(() => this.setupMenubar());
// Listen for changes in recently opened menu
this._register(this.windowsService.onRecentlyOpenedChange(() => { this.onRecentlyOpenedChange(); }));
// Listen to keybindings change
- this._register(this.keybindingService.onDidUpdateKeybindings(() => this.updateMenubar()));
+ this._register(this.keybindingService.onDidUpdateKeybindings(() => this.setupMenubar()));
// 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 doUpdateMenubar(firstTime: boolean): void {
+ private doSetupMenubar(): void {
if (!isMacintosh && this.currentTitlebarStyleSetting === 'custom') {
- this.setupCustomMenubar(firstTime);
+ this.setupCustomMenubar();
} else {
// Send menus to main process to be rendered by Electron
const menubarData = { menus: {}, keybindings: {} };
@@ -275,10 +520,14 @@ export class MenubarControl extends Disposable {
}
}
- private updateMenubar(): void {
+ private setupMenubar(): 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) {
@@ -424,69 +673,301 @@ export class MenubarControl extends Disposable {
}
}
- 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),
- }
- ));
-
- this._register(this.menubar.onFocusStateChange(e => this._onFocusStateChange.fire(e)));
- this._register(this.menubar.onVisibilityChange(e => this._onVisibilityChange.fire(e)));
-
- this._register(attachMenuStyler(this.menubar, this.themeService));
- } else {
- this.menubar.update({ enableMnemonics: this.currentEnableMenuBarMnemonics, visibility: this.currentMenubarVisibility, getKeybinding: (action) => this.keybindingService.lookupKeybinding(action.id) });
+ private setupCustomMenubar(): void {
+ // Don't update while using the menu
+ if (this.isFocused) {
+ this.updatePending = true;
+ return;
}
- // 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;
+ this.container.attributes['role'] = 'menubar';
- 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);
- }
+ 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, '$1') :
+ 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);
}
- target.push(new Separator());
+ if (this.currentEnableMenuBarMnemonics) {
+ this.customMenus[menuIndex].buttonElement.setAttribute('aria-keyshortcuts', 'Alt+' + mnemonic.toLocaleLowerCase());
+ } else {
+ this.customMenus[menuIndex].buttonElement.removeAttribute('aria-keyshortcuts');
+ }
}
- target.pop();
- };
+ // 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 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] });
+ 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)));
+ }
+
+ 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;
+ }
+ }));
+
+ 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();
+ }
}));
}
+ }
- const actions = [];
- updateActions(menu, actions);
+ 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 (!firstTime) {
- this.menubar.updateMenu({ actions: actions, label: this.topLevelTitles[title] });
+ 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;
+
+ 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();
} else {
- this.menubar.push({ actions: actions, label: this.topLevelTitles[title] });
+ this.cleanupCustomMenu();
+ this.showCustomMenu(menuIndex, this.openedViaKeyboard);
}
+ } 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();
}
}
@@ -588,6 +1069,71 @@ 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;
}
@@ -601,11 +1147,21 @@ export class MenubarControl extends Disposable {
this.container.style.height = `${dimension.height}px`;
}
- this.menubar.update({ enableMnemonics: this.currentEnableMenuBarMnemonics, visibility: this.currentMenubarVisibility, getKeybinding: (action) => this.keybindingService.lookupKeybinding(action.id) });
+ if (!this.isVisible) {
+ this.hideMenubar();
+ } else {
+ this.showMenubar();
+ }
}
public getMenubarItemsDimensions(): DOM.Dimension {
- return new DOM.Dimension(this.menubar.getWidth(), this.menubar.getHeight());
+ 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);
}
public create(parent: HTMLElement): HTMLElement {
@@ -613,9 +1169,10 @@ export class MenubarControl extends Disposable {
// Build the menubar
if (this.container) {
+ this.doSetupMenubar();
if (!isMacintosh && this.currentTitlebarStyleSetting === 'custom') {
- this.doUpdateMenubar(true);
+ this.setUnfocusedState();
}
}
@@ -630,10 +1187,6 @@ registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
.monaco-workbench .menubar > .menubar-menu-button {
color: ${menubarActiveWindowFgColor};
}
-
- .monaco-workbench .menubar .toolbar-toggle-more {
- background-color: ${menubarActiveWindowFgColor}
- }
`);
}
@@ -643,10 +1196,6 @@ 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}
- }
`);
}
@@ -659,12 +1208,6 @@ 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}
- }
`);
}
@@ -700,3 +1243,106 @@ 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);
+ }
+}