Path: blob/main/src/vs/workbench/electron-browser/parts/titlebar/titlebarPart.ts
5334 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Event } from '../../../../base/common/event.js';6import { getZoomFactor } from '../../../../base/browser/browser.js';7import { $, addDisposableListener, append, EventType, getWindow, getWindowId, hide, show } from '../../../../base/browser/dom.js';8import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';9import { IConfigurationService, IConfigurationChangeEvent } from '../../../../platform/configuration/common/configuration.js';10import { IStorageService } from '../../../../platform/storage/common/storage.js';11import { INativeWorkbenchEnvironmentService } from '../../../services/environment/electron-browser/environmentService.js';12import { IHostService } from '../../../services/host/browser/host.js';13import { isMacintosh, isWindows, isLinux, isTahoeOrNewer } from '../../../../base/common/platform.js';14import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';15import { BrowserTitlebarPart, BrowserTitleService, IAuxiliaryTitlebarPart } from '../../../browser/parts/titlebar/titlebarPart.js';16import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';17import { IThemeService } from '../../../../platform/theme/common/themeService.js';18import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js';19import { INativeHostService } from '../../../../platform/native/common/native.js';20import { hasNativeTitlebar, useWindowControlsOverlay, DEFAULT_CUSTOM_TITLEBAR_HEIGHT, hasNativeMenu } from '../../../../platform/window/common/window.js';21import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';22import { Codicon } from '../../../../base/common/codicons.js';23import { ThemeIcon } from '../../../../base/common/themables.js';24import { NativeMenubarControl } from './menubarControl.js';25import { IEditorGroupsContainer, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';26import { IEditorService } from '../../../services/editor/common/editorService.js';27import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';28import { CodeWindow, mainWindow } from '../../../../base/browser/window.js';29import { IsWindowAlwaysOnTopContext } from '../../../common/contextkeys.js';3031export class NativeTitlebarPart extends BrowserTitlebarPart {3233//#region IView3435override get minimumHeight(): number {36if (!isMacintosh) {37return super.minimumHeight;38}3940return (this.isCommandCenterVisible ? DEFAULT_CUSTOM_TITLEBAR_HEIGHT : this.macTitlebarSize) / (this.preventZoom ? getZoomFactor(getWindow(this.element)) : 1);41}42override get maximumHeight(): number { return this.minimumHeight; }4344private tahoeOrNewer: boolean;45private get macTitlebarSize() {46if (this.tahoeOrNewer) {47return 32;48}4950return 28;51}5253//#endregion5455private maxRestoreControl: HTMLElement | undefined;56private resizer: HTMLElement | undefined;5758private cachedWindowControlStyles: { bgColor: string; fgColor: string } | undefined;59private cachedWindowControlHeight: number | undefined;6061constructor(62id: string,63targetWindow: CodeWindow,64editorGroupsContainer: IEditorGroupsContainer,65@IContextMenuService contextMenuService: IContextMenuService,66@IConfigurationService configurationService: IConfigurationService,67@INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService,68@IInstantiationService instantiationService: IInstantiationService,69@IThemeService themeService: IThemeService,70@IStorageService storageService: IStorageService,71@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,72@IContextKeyService contextKeyService: IContextKeyService,73@IHostService hostService: IHostService,74@INativeHostService private readonly nativeHostService: INativeHostService,75@IEditorGroupsService editorGroupService: IEditorGroupsService,76@IEditorService editorService: IEditorService,77@IMenuService menuService: IMenuService,78@IKeybindingService keybindingService: IKeybindingService79) {80super(id, targetWindow, editorGroupsContainer, contextMenuService, configurationService, environmentService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService, editorService, menuService, keybindingService);8182this.tahoeOrNewer = isTahoeOrNewer(environmentService.os.release);8384this.handleWindowsAlwaysOnTop(targetWindow.vscodeWindowId);85}8687private async handleWindowsAlwaysOnTop(targetWindowId: number): Promise<void> {88const isWindowAlwaysOnTopContext = IsWindowAlwaysOnTopContext.bindTo(this.contextKeyService);8990this._register(this.nativeHostService.onDidChangeWindowAlwaysOnTop(({ windowId, alwaysOnTop }) => {91if (windowId === targetWindowId) {92isWindowAlwaysOnTopContext.set(alwaysOnTop);93}94}));9596isWindowAlwaysOnTopContext.set(await this.nativeHostService.isWindowAlwaysOnTop({ targetWindowId }));97}9899protected override onMenubarVisibilityChanged(visible: boolean): void {100101// Hide title when toggling menu bar102if ((isWindows || isLinux) && this.currentMenubarVisibility === 'toggle' && visible) {103104// Hack to fix issue #52522 with layered webkit-app-region elements appearing under cursor105if (this.dragRegion) {106hide(this.dragRegion);107setTimeout(() => show(this.dragRegion!), 50);108}109}110111super.onMenubarVisibilityChanged(visible);112}113114protected override onConfigurationChanged(event: IConfigurationChangeEvent): void {115super.onConfigurationChanged(event);116117if (event.affectsConfiguration('window.doubleClickIconToClose')) {118if (this.appIcon) {119this.onUpdateAppIconDragBehavior();120}121}122}123124private onUpdateAppIconDragBehavior(): void {125const setting = this.configurationService.getValue('window.doubleClickIconToClose');126if (setting && this.appIcon) {127(this.appIcon.style as CSSStyleDeclaration & { '-webkit-app-region': string })['-webkit-app-region'] = 'no-drag';128} else if (this.appIcon) {129(this.appIcon.style as CSSStyleDeclaration & { '-webkit-app-region': string })['-webkit-app-region'] = 'drag';130}131}132133protected override installMenubar(): void {134super.installMenubar();135136if (this.menubar) {137return;138}139140if (this.customMenubar.value) {141this._register(this.customMenubar.value.onFocusStateChange(e => this.onMenubarFocusChanged(e)));142}143}144145private onMenubarFocusChanged(focused: boolean): void {146if ((isWindows || isLinux) && this.currentMenubarVisibility !== 'compact' && this.dragRegion) {147if (focused) {148hide(this.dragRegion);149} else {150show(this.dragRegion);151}152}153}154155protected override createContentArea(parent: HTMLElement): HTMLElement {156const result = super.createContentArea(parent);157const targetWindow = getWindow(parent);158const targetWindowId = getWindowId(targetWindow);159160// Native menu controller161if (isMacintosh || hasNativeMenu(this.configurationService)) {162this._register(this.instantiationService.createInstance(NativeMenubarControl));163}164165// App Icon (Native Windows/Linux)166if (this.appIcon) {167this.onUpdateAppIconDragBehavior();168169this._register(addDisposableListener(this.appIcon, EventType.DBLCLICK, (() => {170this.nativeHostService.closeWindow({ targetWindowId });171})));172}173174// Custom Window Controls (Native Windows/Linux)175if (176!hasNativeTitlebar(this.configurationService) && // not for native title bars177!useWindowControlsOverlay(this.configurationService) && // not when controls are natively drawn178this.windowControlsContainer179) {180181// Minimize182const minimizeIcon = append(this.windowControlsContainer, $('div.window-icon.window-minimize' + ThemeIcon.asCSSSelector(Codicon.chromeMinimize)));183this._register(addDisposableListener(minimizeIcon, EventType.CLICK, () => {184this.nativeHostService.minimizeWindow({ targetWindowId });185}));186187// Restore188this.maxRestoreControl = append(this.windowControlsContainer, $('div.window-icon.window-max-restore'));189this._register(addDisposableListener(this.maxRestoreControl, EventType.CLICK, async () => {190const maximized = await this.nativeHostService.isMaximized({ targetWindowId });191if (maximized) {192return this.nativeHostService.unmaximizeWindow({ targetWindowId });193}194195return this.nativeHostService.maximizeWindow({ targetWindowId });196}));197198// Close199const closeIcon = append(this.windowControlsContainer, $('div.window-icon.window-close' + ThemeIcon.asCSSSelector(Codicon.chromeClose)));200this._register(addDisposableListener(closeIcon, EventType.CLICK, () => {201this.nativeHostService.closeWindow({ targetWindowId });202}));203204// Resizer205this.resizer = append(this.rootContainer, $('div.resizer'));206this._register(Event.runAndSubscribe(this.layoutService.onDidChangeWindowMaximized, ({ windowId, maximized }) => {207if (windowId === targetWindowId) {208this.onDidChangeWindowMaximized(maximized);209}210}, { windowId: targetWindowId, maximized: this.layoutService.isWindowMaximized(targetWindow) }));211}212213// Window System Context Menu214// See https://github.com/electron/electron/issues/24893215if (isWindows && !hasNativeTitlebar(this.configurationService)) {216this._register(this.nativeHostService.onDidTriggerWindowSystemContextMenu(({ windowId, x, y }) => {217if (targetWindowId !== windowId) {218return;219}220221const zoomFactor = getZoomFactor(getWindow(this.element));222this.onContextMenu(new MouseEvent(EventType.MOUSE_UP, { clientX: x / zoomFactor, clientY: y / zoomFactor }), MenuId.TitleBarContext);223}));224}225226return result;227}228229private onDidChangeWindowMaximized(maximized: boolean): void {230if (this.maxRestoreControl) {231if (maximized) {232this.maxRestoreControl.classList.remove(...ThemeIcon.asClassNameArray(Codicon.chromeMaximize));233this.maxRestoreControl.classList.add(...ThemeIcon.asClassNameArray(Codicon.chromeRestore));234} else {235this.maxRestoreControl.classList.remove(...ThemeIcon.asClassNameArray(Codicon.chromeRestore));236this.maxRestoreControl.classList.add(...ThemeIcon.asClassNameArray(Codicon.chromeMaximize));237}238}239240if (this.resizer) {241if (maximized) {242hide(this.resizer);243} else {244show(this.resizer);245}246}247}248249override updateStyles(): void {250super.updateStyles();251252// Part container253if (this.element) {254if (useWindowControlsOverlay(this.configurationService)) {255if (256!this.cachedWindowControlStyles ||257this.cachedWindowControlStyles.bgColor !== this.element.style.backgroundColor ||258this.cachedWindowControlStyles.fgColor !== this.element.style.color259) {260this.nativeHostService.updateWindowControls({261targetWindowId: getWindowId(getWindow(this.element)),262backgroundColor: this.element.style.backgroundColor,263foregroundColor: this.element.style.color264});265}266}267}268}269270override layout(width: number, height: number): void {271super.layout(width, height);272273if (useWindowControlsOverlay(this.configurationService)) {274const newHeight = Math.round(height * getZoomFactor(getWindow(this.element)));275if (newHeight !== this.cachedWindowControlHeight) {276this.cachedWindowControlHeight = newHeight;277this.nativeHostService.updateWindowControls({278targetWindowId: getWindowId(getWindow(this.element)),279height: newHeight280});281}282}283}284}285286export class MainNativeTitlebarPart extends NativeTitlebarPart {287288constructor(289@IContextMenuService contextMenuService: IContextMenuService,290@IConfigurationService configurationService: IConfigurationService,291@INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService,292@IInstantiationService instantiationService: IInstantiationService,293@IThemeService themeService: IThemeService,294@IStorageService storageService: IStorageService,295@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,296@IContextKeyService contextKeyService: IContextKeyService,297@IHostService hostService: IHostService,298@INativeHostService nativeHostService: INativeHostService,299@IEditorGroupsService editorGroupService: IEditorGroupsService,300@IEditorService editorService: IEditorService,301@IMenuService menuService: IMenuService,302@IKeybindingService keybindingService: IKeybindingService303) {304super(Parts.TITLEBAR_PART, mainWindow, editorGroupService.mainPart, contextMenuService, configurationService, environmentService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService, nativeHostService, editorGroupService, editorService, menuService, keybindingService);305}306}307308export class AuxiliaryNativeTitlebarPart extends NativeTitlebarPart implements IAuxiliaryTitlebarPart {309310private static COUNTER = 1;311312get height() { return this.minimumHeight; }313314constructor(315readonly container: HTMLElement,316editorGroupsContainer: IEditorGroupsContainer,317private readonly mainTitlebar: BrowserTitlebarPart,318@IContextMenuService contextMenuService: IContextMenuService,319@IConfigurationService configurationService: IConfigurationService,320@INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService,321@IInstantiationService instantiationService: IInstantiationService,322@IThemeService themeService: IThemeService,323@IStorageService storageService: IStorageService,324@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,325@IContextKeyService contextKeyService: IContextKeyService,326@IHostService hostService: IHostService,327@INativeHostService nativeHostService: INativeHostService,328@IEditorGroupsService editorGroupService: IEditorGroupsService,329@IEditorService editorService: IEditorService,330@IMenuService menuService: IMenuService,331@IKeybindingService keybindingService: IKeybindingService332) {333const id = AuxiliaryNativeTitlebarPart.COUNTER++;334super(`workbench.parts.auxiliaryTitle.${id}`, getWindow(container), editorGroupsContainer, contextMenuService, configurationService, environmentService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService, nativeHostService, editorGroupService, editorService, menuService, keybindingService);335}336337override get preventZoom(): boolean {338339// Prevent zooming behavior if any of the following conditions are met:340// 1. Shrinking below the window control size (zoom < 1)341// 2. No custom items are present in the main title bar342// The auxiliary title bar never contains any zoomable items itself,343// but we want to match the behavior of the main title bar.344345return getZoomFactor(getWindow(this.element)) < 1 || !this.mainTitlebar.hasZoomableElements;346}347}348349export class NativeTitleService extends BrowserTitleService {350351protected override createMainTitlebarPart(): MainNativeTitlebarPart {352return this.instantiationService.createInstance(MainNativeTitlebarPart);353}354355protected override doCreateAuxiliaryTitlebarPart(container: HTMLElement, editorGroupsContainer: IEditorGroupsContainer, instantiationService: IInstantiationService): AuxiliaryNativeTitlebarPart {356return instantiationService.createInstance(AuxiliaryNativeTitlebarPart, container, editorGroupsContainer, this.mainPart);357}358}359360361