Merge pull request #75218 from sbatten/menubarRefactor

Separate Native and Custom Menubar Controls for clearer dependencies
This commit is contained in:
SteVen Batten 2019-06-10 14:38:47 -07:00 committed by GitHub
commit 096fdbc08a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 455 additions and 401 deletions

View file

@ -34,18 +34,16 @@ import { assign } from 'vs/base/common/objects';
import { mnemonicMenuLabel, unmnemonicLabel } from 'vs/base/common/labels';
import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility';
import { withNullAsUndefined } from 'vs/base/common/types';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
export class MenubarControl extends Disposable {
export abstract class MenubarControl extends Disposable {
private keys = [
protected keys = [
'window.menuBarVisibility',
'window.enableMenuBarMnemonics',
'window.nativeTabs'
];
private menus: {
protected menus: {
'File': IMenu;
'Edit': IMenu;
'Selection': IMenu;
@ -58,7 +56,7 @@ export class MenubarControl extends Disposable {
[index: string]: IMenu | undefined;
};
private topLevelTitles = {
protected topLevelTitles = {
'File': nls.localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File"),
'Edit': nls.localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit"),
'Selection': nls.localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection"),
@ -69,48 +67,30 @@ export class MenubarControl extends Disposable {
'Help': nls.localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help")
};
private menubar: MenuBar;
private menuUpdater: RunOnceScheduler;
private container: HTMLElement;
private recentlyOpened: IRecentlyOpened;
private alwaysOnMnemonics: boolean;
private isNative: boolean;
protected recentlyOpened: IRecentlyOpened;
private readonly _onVisibilityChange: Emitter<boolean>;
private readonly _onFocusStateChange: Emitter<boolean>;
protected menuUpdater: RunOnceScheduler;
private menubarService: IMenubarService;
private static MAX_MENU_RECENT_ENTRIES = 10;
protected static MAX_MENU_RECENT_ENTRIES = 10;
constructor(
@IThemeService private readonly themeService: IThemeService,
@IMenuService private readonly menuService: IMenuService,
@IWindowService private readonly windowService: IWindowService,
@IWindowsService private readonly windowsService: IWindowsService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IKeybindingService private readonly keybindingService: IKeybindingService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@ILabelService private readonly labelService: ILabelService,
@IUpdateService private readonly updateService: IUpdateService,
@IStorageService private readonly storageService: IStorageService,
@INotificationService private readonly notificationService: INotificationService,
@IPreferencesService private readonly preferencesService: IPreferencesService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
@IInstantiationService private readonly instantiationService: IInstantiationService
protected readonly menuService: IMenuService,
protected readonly windowService: IWindowService,
protected readonly windowsService: IWindowsService,
protected readonly contextKeyService: IContextKeyService,
protected readonly keybindingService: IKeybindingService,
protected readonly configurationService: IConfigurationService,
protected readonly labelService: ILabelService,
protected readonly updateService: IUpdateService,
protected readonly storageService: IStorageService,
protected readonly notificationService: INotificationService,
protected readonly preferencesService: IPreferencesService,
protected readonly environmentService: IEnvironmentService,
protected readonly accessibilityService: IAccessibilityService
) {
super();
this.isNative = !isWeb && (isMacintosh || this.currentTitlebarStyleSetting !== 'custom');
this.instantiationService.invokeFunction((accessor: ServicesAccessor) => {
if (this.isNative) {
this.menubarService = accessor.get(IMenubarService);
}
});
this.menus = {
'File': this._register(this.menuService.createMenu(MenuId.MenubarFileMenu, this.contextKeyService)),
'Edit': this._register(this.menuService.createMenu(MenuId.MenubarEditMenu, this.contextKeyService)),
@ -122,64 +102,72 @@ export class MenubarControl extends Disposable {
'Help': this._register(this.menuService.createMenu(MenuId.MenubarHelpMenu, this.contextKeyService))
};
if (isMacintosh && this.isNative) {
this.menus['Preferences'] = this._register(this.menuService.createMenu(MenuId.MenubarPreferencesMenu, this.contextKeyService));
this.topLevelTitles['Preferences'] = nls.localize('mPreferences', "Preferences");
}
this.menuUpdater = this._register(new RunOnceScheduler(() => this.doUpdateMenubar(false), 200));
this._onVisibilityChange = this._register(new Emitter<boolean>());
this._onFocusStateChange = this._register(new Emitter<boolean>());
if (this.isNative) {
for (const topLevelMenuName of Object.keys(this.topLevelTitles)) {
const menu = this.menus[topLevelMenuName];
if (menu) {
this._register(menu.onDidChange(() => this.updateMenubar()));
}
}
}
this.windowService.getRecentlyOpened().then((recentlyOpened) => {
this.recentlyOpened = recentlyOpened;
if (this.isNative) {
this.doUpdateMenubar(true);
}
});
this.notifyUserOfCustomMenubarAccessibility();
this.registerListeners();
}
private get currentEnableMenuBarMnemonics(): boolean {
let enableMenuBarMnemonics = this.configurationService.getValue<boolean>('window.enableMenuBarMnemonics');
if (typeof enableMenuBarMnemonics !== 'boolean') {
enableMenuBarMnemonics = true;
protected abstract doUpdateMenubar(firstTime: boolean): void;
protected registerListeners(): void {
// Update when config changes
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e)));
// Listen to update service
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.updateMenubar()));
// Update recent menu items on formatter registration
this._register(this.labelService.onDidChangeFormatters(() => { this.onRecentlyOpenedChange(); }));
}
protected updateMenubar(): void {
this.menuUpdater.schedule();
}
protected calculateActionLabel(action: IAction | IMenubarMenuItemAction): string {
let label = action.label;
switch (action.id) {
default:
break;
}
return enableMenuBarMnemonics;
return label;
}
private get currentMenubarVisibility(): MenuBarVisibility {
return this.configurationService.getValue<MenuBarVisibility>('window.menuBarVisibility');
}
protected getOpenRecentActions(): IAction[] {
if (!this.recentlyOpened) {
return [];
}
private get currentTitlebarStyleSetting(): string {
return getTitleBarStyle(this.configurationService, this.environmentService);
}
const { workspaces, files } = this.recentlyOpened;
private onDidChangeWindowFocus(hasFocus: boolean): void {
if (this.container) {
if (hasFocus) {
DOM.removeClass(this.container, 'inactive');
} else {
DOM.addClass(this.container, 'inactive');
this.menubar.blur();
const result: IAction[] = [];
if (workspaces.length > 0) {
for (let i = 0; i < MenubarControl.MAX_MENU_RECENT_ENTRIES && i < workspaces.length; i++) {
result.push(this.createOpenRecentMenuAction(workspaces[i]));
}
result.push(new Separator());
}
if (files.length > 0) {
for (let i = 0; i < MenubarControl.MAX_MENU_RECENT_ENTRIES && i < files.length; i++) {
result.push(this.createOpenRecentMenuAction(files[i]));
}
result.push(new Separator());
}
return result;
}
private onConfigurationUpdated(event: IConfigurationChangeEvent): void {
@ -199,88 +187,6 @@ export class MenubarControl extends Disposable {
});
}
private notifyUserOfCustomMenubarAccessibility(): void {
if (isWeb || isMacintosh) {
return;
}
const hasBeenNotified = this.storageService.getBoolean('menubar/accessibleMenubarNotified', StorageScope.GLOBAL, false);
const usingCustomMenubar = getTitleBarStyle(this.configurationService, this.environmentService) === 'custom';
const detected = this.accessibilityService.getAccessibilitySupport() === AccessibilitySupport.Enabled;
const config = this.configurationService.getValue('editor.accessibilitySupport');
if (hasBeenNotified || usingCustomMenubar || !(config === 'on' || (config === 'auto' && detected))) {
return;
}
const message = nls.localize('menubar.customTitlebarAccessibilityNotification', "Accessibility support is enabled for you. For the most accessible experience, we recommend the custom title bar style.");
this.notificationService.prompt(Severity.Info, message, [
{
label: nls.localize('goToSetting', "Open Settings"),
run: () => {
return this.preferencesService.openGlobalSettings(undefined, { query: 'window.titleBarStyle' });
}
}
]);
this.storageService.store('menubar/accessibleMenubarNotified', true, StorageScope.GLOBAL);
}
private registerListeners(): void {
// Update when config changes
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e)));
// Listen to update service
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.updateMenubar()));
// These listeners only apply when the custom menubar is being used
if (!this.isNative) {
// 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();
}));
}
// Update recent menu items on formatter registration
this._register(this.labelService.onDidChangeFormatters(() => { this.onRecentlyOpenedChange(); }));
}
private doUpdateMenubar(firstTime: boolean): void {
if (!this.isNative) {
this.setupCustomMenubar(firstTime);
} else {
// Send menus to main process to be rendered by Electron
const menubarData = { menus: {}, keybindings: {} };
if (this.getMenubarMenus(menubarData)) {
this.menubarService.updateMenubar(this.windowService.windowId, menubarData);
}
}
}
private updateMenubar(): void {
this.menuUpdater.schedule();
}
private calculateActionLabel(action: IAction | IMenubarMenuItemAction): string {
let label = action.label;
switch (action.id) {
default:
break;
}
return label;
}
private createOpenRecentMenuAction(recent: IRecent): IAction & { uri: URI } {
let label: string;
@ -316,33 +222,172 @@ export class MenubarControl extends Disposable {
return assign(ret, { uri: uri });
}
/* Custom Menu takes actions */
private getOpenRecentActions(): IAction[] {
if (!this.recentlyOpened) {
return [];
private notifyUserOfCustomMenubarAccessibility(): void {
if (isWeb || isMacintosh) {
return;
}
const { workspaces, files } = this.recentlyOpened;
const hasBeenNotified = this.storageService.getBoolean('menubar/accessibleMenubarNotified', StorageScope.GLOBAL, false);
const usingCustomMenubar = getTitleBarStyle(this.configurationService, this.environmentService) === 'custom';
const detected = this.accessibilityService.getAccessibilitySupport() === AccessibilitySupport.Enabled;
const config = this.configurationService.getValue('editor.accessibilitySupport');
const result: IAction[] = [];
if (hasBeenNotified || usingCustomMenubar || !(config === 'on' || (config === 'auto' && detected))) {
return;
}
if (workspaces.length > 0) {
for (let i = 0; i < MenubarControl.MAX_MENU_RECENT_ENTRIES && i < workspaces.length; i++) {
result.push(this.createOpenRecentMenuAction(workspaces[i]));
const message = nls.localize('menubar.customTitlebarAccessibilityNotification', "Accessibility support is enabled for you. For the most accessible experience, we recommend the custom title bar style.");
this.notificationService.prompt(Severity.Info, message, [
{
label: nls.localize('goToSetting', "Open Settings"),
run: () => {
return this.preferencesService.openGlobalSettings(undefined, { query: 'window.titleBarStyle' });
}
}
]);
result.push(new Separator());
this.storageService.store('menubar/accessibleMenubarNotified', true, StorageScope.GLOBAL);
}
}
export class NativeMenubarControl extends MenubarControl {
constructor(
@IMenuService menuService: IMenuService,
@IWindowService windowService: IWindowService,
@IWindowsService windowsService: IWindowsService,
@IContextKeyService contextKeyService: IContextKeyService,
@IKeybindingService keybindingService: IKeybindingService,
@IConfigurationService configurationService: IConfigurationService,
@ILabelService labelService: ILabelService,
@IUpdateService updateService: IUpdateService,
@IStorageService storageService: IStorageService,
@INotificationService notificationService: INotificationService,
@IPreferencesService preferencesService: IPreferencesService,
@IEnvironmentService environmentService: IEnvironmentService,
@IAccessibilityService accessibilityService: IAccessibilityService,
@IMenubarService private readonly menubarService: IMenubarService
) {
super(
menuService,
windowService,
windowsService,
contextKeyService,
keybindingService,
configurationService,
labelService,
updateService,
storageService,
notificationService,
preferencesService,
environmentService,
accessibilityService);
if (isMacintosh) {
this.menus['Preferences'] = this._register(this.menuService.createMenu(MenuId.MenubarPreferencesMenu, this.contextKeyService));
this.topLevelTitles['Preferences'] = nls.localize('mPreferences', "Preferences");
}
if (files.length > 0) {
for (let i = 0; i < MenubarControl.MAX_MENU_RECENT_ENTRIES && i < files.length; i++) {
result.push(this.createOpenRecentMenuAction(files[i]));
for (const topLevelMenuName of Object.keys(this.topLevelTitles)) {
const menu = this.menus[topLevelMenuName];
if (menu) {
this._register(menu.onDidChange(() => this.updateMenubar()));
}
result.push(new Separator());
}
return result;
this.windowService.getRecentlyOpened().then((recentlyOpened) => {
this.recentlyOpened = recentlyOpened;
this.doUpdateMenubar(true);
});
}
protected doUpdateMenubar(firstTime: boolean): void {
// Send menus to main process to be rendered by Electron
const menubarData = { menus: {}, keybindings: {} };
if (this.getMenubarMenus(menubarData)) {
this.menubarService.updateMenubar(this.windowService.windowId, menubarData);
}
}
private getMenubarMenus(menubarData: IMenubarData): boolean {
if (!menubarData) {
return false;
}
menubarData.keybindings = this.getAdditionalKeybindings();
for (const topLevelMenuName of Object.keys(this.topLevelTitles)) {
const menu = this.menus[topLevelMenuName];
if (menu) {
const menubarMenu: IMenubarMenu = { items: [] };
this.populateMenuItems(menu, menubarMenu, menubarData.keybindings);
if (menubarMenu.items.length === 0) {
// Menus are incomplete
return false;
}
menubarData.menus[topLevelMenuName] = menubarMenu;
}
}
return true;
}
private populateMenuItems(menu: IMenu, menuToPopulate: IMenubarMenu, keybindings: { [id: string]: IMenubarKeybinding | undefined }) {
let groups = menu.getActions();
for (let group of groups) {
const [, actions] = group;
actions.forEach(menuItem => {
if (menuItem instanceof SubmenuItemAction) {
const submenu = { items: [] };
if (!this.menus[menuItem.item.submenu]) {
this.menus[menuItem.item.submenu] = this.menuService.createMenu(menuItem.item.submenu, this.contextKeyService);
this._register(this.menus[menuItem.item.submenu]!.onDidChange(() => this.updateMenubar()));
}
const menuToDispose = this.menuService.createMenu(menuItem.item.submenu, this.contextKeyService);
this.populateMenuItems(menuToDispose, submenu, keybindings);
let menubarSubmenuItem: IMenubarMenuItemSubmenu = {
id: menuItem.id,
label: menuItem.label,
submenu: submenu
};
menuToPopulate.items.push(menubarSubmenuItem);
menuToDispose.dispose();
} else {
if (menuItem.id === 'workbench.action.openRecent') {
const actions = this.getOpenRecentActions().map(this.transformOpenRecentAction);
menuToPopulate.items.push(...actions);
}
let menubarMenuItem: IMenubarMenuItemAction = {
id: menuItem.id,
label: menuItem.label
};
if (menuItem.checked) {
menubarMenuItem.checked = true;
}
if (!menuItem.enabled) {
menubarMenuItem.enabled = false;
}
menubarMenuItem.label = this.calculateActionLabel(menubarMenuItem);
keybindings[menuItem.id] = this.getMenubarKeybinding(menuItem.id);
menuToPopulate.items.push(menubarMenuItem);
}
});
menuToPopulate.items.push({ id: 'vscode.menubar.separator' });
}
if (menuToPopulate.items.length > 0) {
menuToPopulate.items.pop();
}
}
private transformOpenRecentAction(action: Separator | (IAction & { uri: URI })): MenubarMenuItem {
@ -358,6 +403,167 @@ export class MenubarControl extends Disposable {
};
}
private getAdditionalKeybindings(): { [id: string]: IMenubarKeybinding } {
const keybindings = {};
if (isMacintosh) {
keybindings['workbench.action.quit'] = (this.getMenubarKeybinding('workbench.action.quit'));
}
return keybindings;
}
private getMenubarKeybinding(id: string): IMenubarKeybinding | undefined {
const binding = this.keybindingService.lookupKeybinding(id);
if (!binding) {
return undefined;
}
// first try to resolve a native accelerator
const electronAccelerator = binding.getElectronAccelerator();
if (electronAccelerator) {
return { label: electronAccelerator, userSettingsLabel: withNullAsUndefined(binding.getUserSettingsLabel()) };
}
// we need this fallback to support keybindings that cannot show in electron menus (e.g. chords)
const acceleratorLabel = binding.getLabel();
if (acceleratorLabel) {
return { label: acceleratorLabel, isNative: false, userSettingsLabel: withNullAsUndefined(binding.getUserSettingsLabel()) };
}
return undefined;
}
}
export class CustomMenubarControl extends MenubarControl {
private menubar: MenuBar;
private container: HTMLElement;
private alwaysOnMnemonics: boolean;
private readonly _onVisibilityChange: Emitter<boolean>;
private readonly _onFocusStateChange: Emitter<boolean>;
constructor(
@IMenuService menuService: IMenuService,
@IWindowService windowService: IWindowService,
@IWindowsService windowsService: IWindowsService,
@IContextKeyService contextKeyService: IContextKeyService,
@IKeybindingService keybindingService: IKeybindingService,
@IConfigurationService configurationService: IConfigurationService,
@ILabelService labelService: ILabelService,
@IUpdateService updateService: IUpdateService,
@IStorageService storageService: IStorageService,
@INotificationService notificationService: INotificationService,
@IPreferencesService preferencesService: IPreferencesService,
@IEnvironmentService environmentService: IEnvironmentService,
@IAccessibilityService accessibilityService: IAccessibilityService,
@IThemeService private readonly themeService: IThemeService
) {
super(
menuService,
windowService,
windowsService,
contextKeyService,
keybindingService,
configurationService,
labelService,
updateService,
storageService,
notificationService,
preferencesService,
environmentService,
accessibilityService);
this._onVisibilityChange = this._register(new Emitter<boolean>());
this._onFocusStateChange = this._register(new Emitter<boolean>());
this.windowService.getRecentlyOpened().then((recentlyOpened) => {
this.recentlyOpened = recentlyOpened;
});
registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
const menubarActiveWindowFgColor = theme.getColor(TITLE_BAR_ACTIVE_FOREGROUND);
if (menubarActiveWindowFgColor) {
collector.addRule(`
.monaco-workbench .menubar > .menubar-menu-button {
color: ${menubarActiveWindowFgColor};
}
.monaco-workbench .menubar .toolbar-toggle-more {
background-color: ${menubarActiveWindowFgColor}
}
`);
}
const menubarInactiveWindowFgColor = theme.getColor(TITLE_BAR_INACTIVE_FOREGROUND);
if (menubarInactiveWindowFgColor) {
collector.addRule(`
.monaco-workbench .menubar.inactive > .menubar-menu-button {
color: ${menubarInactiveWindowFgColor};
}
.monaco-workbench .menubar.inactive > .menubar-menu-button .toolbar-toggle-more {
background-color: ${menubarInactiveWindowFgColor}
}
`);
}
const menubarSelectedFgColor = theme.getColor(MENUBAR_SELECTION_FOREGROUND);
if (menubarSelectedFgColor) {
collector.addRule(`
.monaco-workbench .menubar > .menubar-menu-button.open,
.monaco-workbench .menubar > .menubar-menu-button:focus,
.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}
}
`);
}
const menubarSelectedBgColor = theme.getColor(MENUBAR_SELECTION_BACKGROUND);
if (menubarSelectedBgColor) {
collector.addRule(`
.monaco-workbench .menubar > .menubar-menu-button.open,
.monaco-workbench .menubar > .menubar-menu-button:focus,
.monaco-workbench .menubar:not(:focus-within) > .menubar-menu-button:hover {
background-color: ${menubarSelectedBgColor};
}
`);
}
const menubarSelectedBorderColor = theme.getColor(MENUBAR_SELECTION_BORDER);
if (menubarSelectedBorderColor) {
collector.addRule(`
.monaco-workbench .menubar > .menubar-menu-button:hover {
outline: dashed 1px;
}
.monaco-workbench .menubar > .menubar-menu-button.open,
.monaco-workbench .menubar > .menubar-menu-button:focus {
outline: solid 1px;
}
.monaco-workbench .menubar > .menubar-menu-button.open,
.monaco-workbench .menubar > .menubar-menu-button:focus,
.monaco-workbench .menubar > .menubar-menu-button:hover {
outline-offset: -1px;
outline-color: ${menubarSelectedBorderColor};
}
`);
}
});
}
protected doUpdateMenubar(firstTime: boolean): void {
this.setupCustomMenubar(firstTime);
}
private getUpdateAction(): IAction | null {
const state = this.updateService.state;
@ -393,6 +599,10 @@ export class MenubarControl extends Disposable {
}
}
private get currentMenubarVisibility(): MenuBarVisibility {
return this.configurationService.getValue<MenuBarVisibility>('window.menuBarVisibility');
}
private insertActionsBefore(nextAction: IAction, target: IAction[]): void {
switch (nextAction.id) {
case 'workbench.action.openRecent':
@ -416,6 +626,15 @@ export class MenubarControl extends Disposable {
}
}
private get currentEnableMenuBarMnemonics(): boolean {
let enableMenuBarMnemonics = this.configurationService.getValue<boolean>('window.enableMenuBarMnemonics');
if (typeof enableMenuBarMnemonics !== 'boolean') {
enableMenuBarMnemonics = true;
}
return enableMenuBarMnemonics;
}
private setupCustomMenubar(firstTime: boolean): void {
if (firstTime) {
this.menubar = this._register(new MenuBar(
@ -498,126 +717,58 @@ export class MenubarControl extends Disposable {
}
}
private getMenubarKeybinding(id: string): IMenubarKeybinding | undefined {
const binding = this.keybindingService.lookupKeybinding(id);
if (!binding) {
return undefined;
}
// first try to resolve a native accelerator
const electronAccelerator = binding.getElectronAccelerator();
if (electronAccelerator) {
return { label: electronAccelerator, userSettingsLabel: withNullAsUndefined(binding.getUserSettingsLabel()) };
}
// we need this fallback to support keybindings that cannot show in electron menus (e.g. chords)
const acceleratorLabel = binding.getLabel();
if (acceleratorLabel) {
return { label: acceleratorLabel, isNative: false, userSettingsLabel: withNullAsUndefined(binding.getUserSettingsLabel()) };
}
return undefined;
}
private populateMenuItems(menu: IMenu, menuToPopulate: IMenubarMenu, keybindings: { [id: string]: IMenubarKeybinding | undefined }) {
let groups = menu.getActions();
for (let group of groups) {
const [, actions] = group;
actions.forEach(menuItem => {
if (menuItem instanceof SubmenuItemAction) {
const submenu = { items: [] };
if (!this.menus[menuItem.item.submenu]) {
this.menus[menuItem.item.submenu] = this.menuService.createMenu(menuItem.item.submenu, this.contextKeyService);
this._register(this.menus[menuItem.item.submenu]!.onDidChange(() => this.updateMenubar()));
}
const menuToDispose = this.menuService.createMenu(menuItem.item.submenu, this.contextKeyService);
this.populateMenuItems(menuToDispose, submenu, keybindings);
let menubarSubmenuItem: IMenubarMenuItemSubmenu = {
id: menuItem.id,
label: menuItem.label,
submenu: submenu
};
menuToPopulate.items.push(menubarSubmenuItem);
menuToDispose.dispose();
} else {
if (menuItem.id === 'workbench.action.openRecent') {
const actions = this.getOpenRecentActions().map(this.transformOpenRecentAction);
menuToPopulate.items.push(...actions);
}
let menubarMenuItem: IMenubarMenuItemAction = {
id: menuItem.id,
label: menuItem.label
};
if (menuItem.checked) {
menubarMenuItem.checked = true;
}
if (!menuItem.enabled) {
menubarMenuItem.enabled = false;
}
menubarMenuItem.label = this.calculateActionLabel(menubarMenuItem);
keybindings[menuItem.id] = this.getMenubarKeybinding(menuItem.id);
menuToPopulate.items.push(menubarMenuItem);
}
});
menuToPopulate.items.push({ id: 'vscode.menubar.separator' });
}
if (menuToPopulate.items.length > 0) {
menuToPopulate.items.pop();
}
}
private getAdditionalKeybindings(): { [id: string]: IMenubarKeybinding } {
const keybindings = {};
if (isMacintosh && this.isNative) {
keybindings['workbench.action.quit'] = (this.getMenubarKeybinding('workbench.action.quit'));
}
return keybindings;
}
private getMenubarMenus(menubarData: IMenubarData): boolean {
if (!menubarData) {
return false;
}
menubarData.keybindings = this.getAdditionalKeybindings();
for (const topLevelMenuName of Object.keys(this.topLevelTitles)) {
const menu = this.menus[topLevelMenuName];
if (menu) {
const menubarMenu: IMenubarMenu = { items: [] };
this.populateMenuItems(menu, menubarMenu, menubarData.keybindings);
if (menubarMenu.items.length === 0) {
// Menus are incomplete
return false;
}
menubarData.menus[topLevelMenuName] = menubarMenu;
private onDidChangeWindowFocus(hasFocus: boolean): void {
if (this.container) {
if (hasFocus) {
DOM.removeClass(this.container, 'inactive');
} else {
DOM.addClass(this.container, 'inactive');
this.menubar.blur();
}
}
return true;
}
public get onVisibilityChange(): Event<boolean> {
protected registerListeners(): void {
super.registerListeners();
// 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();
}));
}
get onVisibilityChange(): Event<boolean> {
return this._onVisibilityChange.event;
}
public get onFocusStateChange(): Event<boolean> {
get onFocusStateChange(): Event<boolean> {
return this._onFocusStateChange.event;
}
public layout(dimension: DOM.Dimension) {
getMenubarItemsDimensions(): DOM.Dimension {
if (this.menubar) {
return new DOM.Dimension(this.menubar.getWidth(), this.menubar.getHeight());
}
return new DOM.Dimension(0, 0);
}
create(parent: HTMLElement): HTMLElement {
this.container = parent;
// Build the menubar
if (this.container) {
this.doUpdateMenubar(true);
}
return this.container;
}
layout(dimension: DOM.Dimension) {
if (this.container) {
this.container.style.height = `${dimension.height}px`;
}
@ -626,104 +777,4 @@ export class MenubarControl extends Disposable {
this.menubar.update({ enableMnemonics: this.currentEnableMenuBarMnemonics, visibility: this.currentMenubarVisibility, getKeybinding: (action) => this.keybindingService.lookupKeybinding(action.id), alwaysOnMnemonics: this.alwaysOnMnemonics });
}
}
public getMenubarItemsDimensions(): DOM.Dimension {
if (this.menubar) {
return new DOM.Dimension(this.menubar.getWidth(), this.menubar.getHeight());
}
return new DOM.Dimension(0, 0);
}
public create(parent: HTMLElement): HTMLElement {
this.container = parent;
// Build the menubar
if (this.container) {
if (!this.isNative) {
this.doUpdateMenubar(true);
}
}
return this.container;
}
}
registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
const menubarActiveWindowFgColor = theme.getColor(TITLE_BAR_ACTIVE_FOREGROUND);
if (menubarActiveWindowFgColor) {
collector.addRule(`
.monaco-workbench .menubar > .menubar-menu-button {
color: ${menubarActiveWindowFgColor};
}
.monaco-workbench .menubar .toolbar-toggle-more {
background-color: ${menubarActiveWindowFgColor}
}
`);
}
const menubarInactiveWindowFgColor = theme.getColor(TITLE_BAR_INACTIVE_FOREGROUND);
if (menubarInactiveWindowFgColor) {
collector.addRule(`
.monaco-workbench .menubar.inactive > .menubar-menu-button {
color: ${menubarInactiveWindowFgColor};
}
.monaco-workbench .menubar.inactive > .menubar-menu-button .toolbar-toggle-more {
background-color: ${menubarInactiveWindowFgColor}
}
`);
}
const menubarSelectedFgColor = theme.getColor(MENUBAR_SELECTION_FOREGROUND);
if (menubarSelectedFgColor) {
collector.addRule(`
.monaco-workbench .menubar > .menubar-menu-button.open,
.monaco-workbench .menubar > .menubar-menu-button:focus,
.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}
}
`);
}
const menubarSelectedBgColor = theme.getColor(MENUBAR_SELECTION_BACKGROUND);
if (menubarSelectedBgColor) {
collector.addRule(`
.monaco-workbench .menubar > .menubar-menu-button.open,
.monaco-workbench .menubar > .menubar-menu-button:focus,
.monaco-workbench .menubar:not(:focus-within) > .menubar-menu-button:hover {
background-color: ${menubarSelectedBgColor};
}
`);
}
const menubarSelectedBorderColor = theme.getColor(MENUBAR_SELECTION_BORDER);
if (menubarSelectedBorderColor) {
collector.addRule(`
.monaco-workbench .menubar > .menubar-menu-button:hover {
outline: dashed 1px;
}
.monaco-workbench .menubar > .menubar-menu-button.open,
.monaco-workbench .menubar > .menubar-menu-button:focus {
outline: solid 1px;
}
.monaco-workbench .menubar > .menubar-menu-button.open,
.monaco-workbench .menubar > .menubar-menu-button:focus,
.monaco-workbench .menubar > .menubar-menu-button:hover {
outline-offset: -1px;
outline-color: ${menubarSelectedBorderColor};
}
`);
}
});

View file

@ -27,7 +27,7 @@ import { URI } from 'vs/base/common/uri';
import { Color } from 'vs/base/common/color';
import { trim } from 'vs/base/common/strings';
import { EventType, EventHelper, Dimension, isAncestor, hide, show, removeClass, addClass, append, $, addDisposableListener, runAtThisOrScheduleAtNextAnimationFrame } from 'vs/base/browser/dom';
import { MenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl';
import { MenubarControl, NativeMenubarControl, CustomMenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl';
import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { template, getBaseLabel } from 'vs/base/common/labels';
import { ILabelService } from 'vs/platform/label/common/label';
@ -337,15 +337,18 @@ export class TitlebarPart extends Part implements ITitleService {
}
// Menubar: the menubar part which is responsible for populating both the custom and native menubars
this.menubarPart = this.instantiationService.createInstance(MenubarControl);
this.menubar = append(this.element, $('div.menubar'));
this.menubar.setAttribute('role', 'menubar');
if ((isMacintosh && !isWeb) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native') {
this.menubarPart = this.instantiationService.createInstance(NativeMenubarControl);
} else {
const customMenubarControl = this.instantiationService.createInstance(CustomMenubarControl);
this.menubarPart = customMenubarControl;
this.menubar = append(this.element, $('div.menubar'));
this.menubar.setAttribute('role', 'menubar');
this.menubarPart.create(this.menubar);
customMenubarControl.create(this.menubar);
if (!isMacintosh || isWeb) {
this._register(this.menubarPart.onVisibilityChange(e => this.onMenubarVisibilityChanged(e)));
this._register(this.menubarPart.onFocusStateChange(e => this.onMenubarFocusChanged(e)));
this._register(customMenubarControl.onVisibilityChange(e => this.onMenubarVisibilityChanged(e)));
this._register(customMenubarControl.onFocusStateChange(e => this.onMenubarFocusChanged(e)));
}
// Title
@ -548,7 +551,7 @@ export class TitlebarPart extends Part implements ITitleService {
}
private adjustTitleMarginToCenter(): void {
if (!isMacintosh || isWeb) {
if (this.menubarPart instanceof CustomMenubarControl) {
const leftMarker = (this.appIcon ? this.appIcon.clientWidth : 0) + this.menubar.clientWidth + 10;
const rightMarker = this.element.clientWidth - (this.windowControls ? this.windowControls.clientWidth : 0) - 10;
@ -589,7 +592,7 @@ export class TitlebarPart extends Part implements ITitleService {
runAtThisOrScheduleAtNextAnimationFrame(() => this.adjustTitleMarginToCenter());
if (this.menubarPart) {
if (this.menubarPart instanceof CustomMenubarControl) {
const menubarDimension = new Dimension(0, dimension.height);
this.menubarPart.layout(menubarDimension);
}