diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts
index 08a671719156c8c45f1556b5dda4094f63a42b3b..c93f88cc8db35b5274bf447fa2640eebeee8c738 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 0000000000000000000000000000000000000000..e3f856233564c4ddf808e26a634c0d6be56a3fd9
--- /dev/null
+++ b/src/vs/base/browser/ui/menu/ellipsis.svg
@@ -0,0 +1 @@
+
\ 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 c24040ba3a385f88e3a5720c2cd90bd04d282b28..b896e7bed2831385ccf6dc1d20b62136b19a4863 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 5abc7332d4a3e0d7537fc5a369fa148fe53b6ca9..99aa822778377a4e46d9d78dfc624a4ea3fa09c9 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 0000000000000000000000000000000000000000..5d916ccaf05c477b1fb412366747cddd16db4557
--- /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, '$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 dad60fe5ab34fdfdc06fddf438e53e8f3e49f11f..6a0f061d4dc52612bbcbdb31674b6aaa48da2362 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 8363e7a905723af03451aa5cb760bbf6077c573a..a44177c55c00bfd843bc013f8a5cad222d3a8364 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, '$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);
+ 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);
- }
-}