未验证 提交 aedd4402 编写于 作者: S SteVen Batten 提交者: GitHub

Make menubar its own widget and add overflow (#63954)

* working state

* fix keybindings and add compact submenu mode

* fix blur

* fix mnemonics in overflow menu

* fix issues with window resizing

* switch to ellipsis
上级 99b2fca4
......@@ -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();
}
}
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>Ellipsis_bold_16x</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M6,7.5A2.5,2.5,0,1,1,3.5,5,2.5,2.5,0,0,1,6,7.5ZM8.5,5A2.5,2.5,0,1,0,11,7.5,2.5,2.5,0,0,0,8.5,5Zm5,0A2.5,2.5,0,1,0,16,7.5,2.5,2.5,0,0,0,13.5,5Z" style="display: none;"/></g><g id="iconBg"><path class="icon-vs-bg" d="M5,7.5A1.5,1.5,0,1,1,3.5,6,1.5,1.5,0,0,1,5,7.5ZM8.5,6A1.5,1.5,0,1,0,10,7.5,1.5,1.5,0,0,0,8.5,6Zm5,0A1.5,1.5,0,1,0,15,7.5,1.5,1.5,0,0,0,13.5,6Z"/></g></svg>
\ No newline at end of file
......@@ -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
......@@ -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<void> {
get onScroll(): Event<void> {
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();
......
/*---------------------------------------------------------------------------------------------
* 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<KeyCode, number>;
private updatePending: boolean;
private _focusState: MenubarState;
private actionRunner: IActionRunner;
private _onVisibilityChange: Emitter<boolean>;
private _onFocusStateChange: Emitter<boolean>;
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<KeyCode, number>();
this._onVisibilityChange = this._register(new Emitter<boolean>());
this._onFocusStateChange = this._register(new Emitter<boolean>());
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, '<mnemonic aria-hidden="true">$1</mnemonic>') :
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<boolean> {
return this._onVisibilityChange.event;
}
public get onFocusStateChange(): Event<boolean> {
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<IModifierKeyStatus> {
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
......@@ -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
......@@ -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<KeyCode, number>;
private _onVisibilityChange: Emitter<boolean>;
private _onFocusStateChange: Emitter<boolean>;
......@@ -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<boolean>());
this._onFocusStateChange = this._register(new Emitter<boolean>());
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<boolean>());
this._onFocusStateChange = this._register(new Emitter<boolean>());
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<KeyCode, number>();
}
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, '<mnemonic aria-hidden="true">$1</mnemonic>') :
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<boolean> {
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<IModifierKeyStatus> {
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);
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册