diff options
Diffstat (limited to 'src/vs/workbench/browser/parts/titlebar/titlebarPart.ts')
-rw-r--r-- | src/vs/workbench/browser/parts/titlebar/titlebarPart.ts | 180 |
1 files changed, 137 insertions, 43 deletions
diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 4934df5077c..2dbbcce47c8 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -12,10 +12,10 @@ import { getZoomFactor } from 'vs/base/browser/browser'; import { MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility } from 'vs/platform/window/common/window'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IAction } from 'vs/base/common/actions'; +import { IAction, toAction } from 'vs/base/common/actions'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { DisposableStore, dispose } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, toDisposable } from 'vs/base/common/lifecycle'; import { EditorResourceAccessor, Verbosity, SideBySideEditor } from 'vs/workbench/common/editor'; import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -25,7 +25,7 @@ import { isMacintosh, isWindows, isLinux, isWeb } from 'vs/base/common/platform' 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, append, $, addDisposableListener, runAtThisOrScheduleAtNextAnimationFrame, prepend } from 'vs/base/browser/dom'; +import { EventType, EventHelper, Dimension, isAncestor, append, $, addDisposableListener, runAtThisOrScheduleAtNextAnimationFrame, prepend, clearNode } from 'vs/base/browser/dom'; import { CustomMenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { template } from 'vs/base/common/labels'; @@ -34,8 +34,8 @@ import { Emitter } from 'vs/base/common/event'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { createActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IMenuService, IMenu, MenuId } from 'vs/platform/actions/common/actions'; +import { createActionViewItem, createAndFillInContextMenuActions, DropdownWithDefaultActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IMenuService, IMenu, MenuId, SubmenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -45,6 +45,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { getVirtualWorkspaceLocation } from 'vs/platform/workspace/common/virtualWorkspace'; import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export class TitlebarPart extends Part implements ITitleService { @@ -57,7 +58,7 @@ export class TitlebarPart extends Part implements ITitleService { readonly minimumWidth: number = 0; readonly maximumWidth: number = Number.POSITIVE_INFINITY; - get minimumHeight(): number { return 30 / (this.currentMenubarVisibility === 'hidden' ? getZoomFactor() : 1); } + get minimumHeight(): number { return 30 / (this.currentMenubarVisibility === 'hidden' || getZoomFactor() < 1 ? getZoomFactor() : 1); } get maximumHeight(): number { return this.minimumHeight; } //#endregion @@ -68,15 +69,19 @@ export class TitlebarPart extends Part implements ITitleService { declare readonly _serviceBrand: undefined; protected rootContainer!: HTMLElement; + protected windowControls: HTMLElement | undefined; + protected readonly titleMenuElement: HTMLElement = $('div.title-menu'); protected title!: HTMLElement; protected customMenubar: CustomMenubarControl | undefined; protected appIcon: HTMLElement | undefined; private appIconBadge: HTMLElement | undefined; protected menubar?: HTMLElement; - protected windowControls: HTMLElement | undefined; + protected layoutControls: HTMLElement | undefined; private layoutToolbar: ToolBar | undefined; protected lastLayoutDimensions: Dimension | undefined; + + private readonly titleMenuDisposables = this._register(new DisposableStore()); private titleBarStyle: 'native' | 'custom'; private pendingTitle: string | undefined; @@ -87,6 +92,7 @@ export class TitlebarPart extends Part implements ITitleService { private readonly activeEditorListeners = this._register(new DisposableStore()); private readonly titleUpdater = this._register(new RunOnceScheduler(() => this.doUpdateTitle(), 0)); + private readonly onDidUpdateTitle = new Emitter<void>(); private contextMenu: IMenu; @@ -105,6 +111,7 @@ export class TitlebarPart extends Part implements ITitleService { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IHostService private readonly hostService: IHostService, @IProductService private readonly productService: IProductService, + @IKeybindingService private readonly keybindingService: IKeybindingService, ) { super(Parts.TITLEBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); @@ -150,8 +157,12 @@ export class TitlebarPart extends Part implements ITitleService { } } - if (this.titleBarStyle !== 'native' && this.windowControls && event.affectsConfiguration('workbench.layoutControl.enabled')) { - this.windowControls.classList.toggle('show-layout-control', this.layoutControlEnabled); + if (this.titleBarStyle !== 'native' && this.layoutControls && event.affectsConfiguration('workbench.layoutControl.enabled')) { + this.layoutControls.classList.toggle('show-layout-control', this.layoutControlEnabled); + } + + if (event.affectsConfiguration('window.experimental.titleMenu')) { + this.updateTitleMenu(); } } @@ -196,11 +207,11 @@ export class TitlebarPart extends Part implements ITitleService { this.pendingTitle = title; } - if ((isWeb || isWindows || isLinux) && this.title) { - if (this.lastLayoutDimensions) { - this.updateLayout(this.lastLayoutDimensions); - } + if (this.lastLayoutDimensions) { + this.updateLayout(this.lastLayoutDimensions); } + + this.onDidUpdateTitle.fire(); } private getWindowTitle(): string { @@ -356,7 +367,7 @@ export class TitlebarPart extends Part implements ITitleService { this.customMenubar = this._register(this.instantiationService.createInstance(CustomMenubarControl)); - this.menubar = this.rootContainer.insertBefore($('div.menubar'), this.title); + this.menubar = this.rootContainer.insertBefore($('div.menubar'), this.titleMenuElement); this.menubar.setAttribute('role', 'menubar'); this.customMenubar.create(this.menubar); @@ -364,9 +375,54 @@ export class TitlebarPart extends Part implements ITitleService { this._register(this.customMenubar.onVisibilityChange(e => this.onMenubarVisibilityChanged(e))); } + private updateTitleMenu(): void { + this.titleMenuDisposables.clear(); + const enableTitleMenu = this.configurationService.getValue<boolean>('window.experimental.titleMenu'); + this.rootContainer.classList.toggle('enable-title-menu', enableTitleMenu); + if (!enableTitleMenu) { + return; + } + + + const that = this; + const titleToolbar = new ToolBar(this.titleMenuElement, this.contextMenuService, { + actionViewItemProvider: (action) => { + + if (action instanceof SubmenuItemAction && action.item.submenu === MenuId.TitleMenuQuickPick) { + class QuickInputDropDown extends DropdownWithDefaultActionViewItem { + override render(container: HTMLElement): void { + super.render(container); + container.classList.add('quickopen'); + container.title = that.getWindowTitle(); + this._store.add(that.onDidUpdateTitle.event(() => container.title = that.getWindowTitle())); + } + } + return that.instantiationService.createInstance(QuickInputDropDown, action, { + keybindingProvider: action => that.keybindingService.lookupKeybinding(action.id), + renderKeybindingWithDefaultActionLabel: true + }); + } + return undefined; + } + }); + const titleMenu = this.titleMenuDisposables.add(this.menuService.createMenu(MenuId.TitleMenu, this.contextKeyService)); + const titleMenuDisposables = this.titleMenuDisposables.add(new DisposableStore()); + const updateTitleMenu = () => { + titleMenuDisposables.clear(); + const actions: IAction[] = []; + titleMenuDisposables.add(createAndFillInContextMenuActions(titleMenu, undefined, actions)); + titleToolbar.setActions(actions); + }; + this.titleMenuDisposables.add(titleMenu.onDidChange(updateTitleMenu)); + this.titleMenuDisposables.add(this.keybindingService.onDidUpdateKeybindings(updateTitleMenu)); + this.titleMenuDisposables.add(toDisposable(() => clearNode(this.titleMenuElement))); + updateTitleMenu(); + } + override createContentArea(parent: HTMLElement): HTMLElement { this.element = parent; this.rootContainer = append(parent, $('.titlebar-container')); + append(this.rootContainer, this.titleMenuElement); // App Icon (Native Windows/Linux and Web) if (!isMacintosh || isWeb) { @@ -395,6 +451,9 @@ export class TitlebarPart extends Part implements ITitleService { this.installMenubar(); } + // Title Menu + this.updateTitleMenu(); + // Title this.title = append(this.rootContainer, $('div.window-title')); if (this.pendingTitle) { @@ -404,16 +463,22 @@ export class TitlebarPart extends Part implements ITitleService { } if (this.titleBarStyle !== 'native') { - this.windowControls = append(this.rootContainer, $('div.window-controls-container')); - this.windowControls.classList.toggle('show-layout-control', this.layoutControlEnabled); + this.layoutControls = append(this.rootContainer, $('div.layout-controls-container')); + this.layoutControls.classList.toggle('show-layout-control', this.layoutControlEnabled); - const layoutDropdownContainer = append(this.windowControls, $('div.layout-dropdown-container')); - this.layoutToolbar = new ToolBar(layoutDropdownContainer, this.contextMenuService, { + this.layoutToolbar = new ToolBar(this.layoutControls, this.contextMenuService, { actionViewItemProvider: action => { return createActionViewItem(this.instantiationService, action); - } + }, + allowContextMenu: true }); + this._register(addDisposableListener(this.layoutControls, EventType.CONTEXT_MENU, e => { + EventHelper.stop(e); + + this.onLayoutControlContextMenu(e, this.layoutControls!); + })); + const menu = this._register(this.menuService.createMenu(MenuId.LayoutControlMenu, this.contextKeyService)); const updateLayoutMenu = () => { @@ -433,6 +498,8 @@ export class TitlebarPart extends Part implements ITitleService { updateLayoutMenu(); } + this.windowControls = append(this.element, $('div.window-controls-container')); + // Context menu on title [EventType.CONTEXT_MENU, EventType.MOUSE_DOWN].forEach(event => { this._register(addDisposableListener(this.title, event, e => { @@ -455,6 +522,10 @@ export class TitlebarPart extends Part implements ITitleService { return; } + if (e.target && isAncestor(e.target as HTMLElement, this.titleMenuElement)) { + return; + } + const active = document.activeElement; setTimeout(() => { if (active instanceof HTMLElement) { @@ -507,7 +578,6 @@ export class TitlebarPart extends Part implements ITitleService { } private onContextMenu(e: MouseEvent): void { - // Find target anchor const event = new StandardMouseEvent(e); const anchor = { x: event.posx, y: event.posy }; @@ -524,20 +594,41 @@ export class TitlebarPart extends Part implements ITitleService { }); } - protected adjustTitleMarginToCenter(): void { - if (this.customMenubar && this.menubar) { - const leftMarker = (this.appIcon ? this.appIcon.clientWidth : 0) + this.menubar.clientWidth + 10; - const rightMarker = this.element.clientWidth - 10; - - // Not enough space to center the titlebar within window, - // Center between menu and window controls - if (leftMarker > (this.element.clientWidth - this.title.clientWidth) / 2 || - rightMarker < (this.element.clientWidth + this.title.clientWidth) / 2) { - this.title.style.position = ''; - this.title.style.left = ''; - this.title.style.transform = ''; - return; + private onLayoutControlContextMenu(e: MouseEvent, el: HTMLElement): void { + // Find target anchor + const event = new StandardMouseEvent(e); + const anchor = { x: event.posx, y: event.posy }; + + const actions: IAction[] = []; + actions.push(toAction({ + id: 'layoutControl.hide', + label: localize('layoutControl.hide', "Hide Layout Control"), + run: () => { + this.configurationService.updateValue('workbench.layoutControl.enabled', false); } + })); + + // Show it + this.contextMenuService.showContextMenu({ + getAnchor: () => anchor, + getActions: () => actions, + domForShadowRoot: el + }); + } + + protected adjustTitleMarginToCenter(): void { + const base = isMacintosh ? (this.windowControls?.clientWidth ?? 0) : 0; + const leftMarker = base + (this.appIcon?.clientWidth ?? 0) + (this.menubar?.clientWidth ?? 0) + 10; + const rightMarker = base + this.rootContainer.clientWidth - (this.layoutControls?.clientWidth ?? 0) - 10; + + // Not enough space to center the titlebar within window, + // Center between left and right controls + if (leftMarker > (this.rootContainer.clientWidth + (this.windowControls?.clientWidth ?? 0) - this.title.clientWidth) / 2 || + rightMarker < (this.rootContainer.clientWidth + (this.windowControls?.clientWidth ?? 0) + this.title.clientWidth) / 2) { + this.title.style.position = ''; + this.title.style.left = ''; + this.title.style.transform = ''; + return; } this.title.style.position = 'absolute'; @@ -557,16 +648,13 @@ export class TitlebarPart extends Part implements ITitleService { this.lastLayoutDimensions = dimension; if (getTitleBarStyle(this.configurationService) === 'custom') { - // Only prevent zooming behavior on macOS or when the menubar is not visible - if ((!isWeb && isMacintosh) || this.currentMenubarVisibility === 'hidden') { - this.rootContainer.style.height = `${100.0 * getZoomFactor()}%`; - this.rootContainer.style.width = `${100.0 * getZoomFactor()}%`; - this.rootContainer.style.transform = `scale(${1 / getZoomFactor()})`; - } else { - this.rootContainer.style.height = '100%'; - this.rootContainer.style.width = '100%'; - this.rootContainer.style.transform = ''; - } + // Prevent zooming behavior if any of the following conditions are met: + // 1. Native macOS + // 2. Menubar is hidden + // 3. Shrinking below the window control size (zoom < 1) + const zoomFactor = getZoomFactor(); + this.element.style.setProperty('--zoom-factor', zoomFactor.toString()); + this.rootContainer.classList.toggle('counter-zoom', zoomFactor < 1 || (!isWeb && isMacintosh) || this.currentMenubarVisibility === 'hidden'); runAtThisOrScheduleAtNextAnimationFrame(() => this.adjustTitleMarginToCenter()); @@ -609,3 +697,9 @@ registerThemingParticipant((theme, collector) => { `); } }); + +MenuRegistry.appendMenuItem(MenuId.TitleMenu, { + submenu: MenuId.TitleMenuQuickPick, + title: localize('title', "Select Mode"), + order: Number.MAX_SAFE_INTEGER +}); |