Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/electron-browser/parts/titlebar/titlebarPart.ts
5334 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Event } from '../../../../base/common/event.js';
7
import { getZoomFactor } from '../../../../base/browser/browser.js';
8
import { $, addDisposableListener, append, EventType, getWindow, getWindowId, hide, show } from '../../../../base/browser/dom.js';
9
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
10
import { IConfigurationService, IConfigurationChangeEvent } from '../../../../platform/configuration/common/configuration.js';
11
import { IStorageService } from '../../../../platform/storage/common/storage.js';
12
import { INativeWorkbenchEnvironmentService } from '../../../services/environment/electron-browser/environmentService.js';
13
import { IHostService } from '../../../services/host/browser/host.js';
14
import { isMacintosh, isWindows, isLinux, isTahoeOrNewer } from '../../../../base/common/platform.js';
15
import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';
16
import { BrowserTitlebarPart, BrowserTitleService, IAuxiliaryTitlebarPart } from '../../../browser/parts/titlebar/titlebarPart.js';
17
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
18
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
19
import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js';
20
import { INativeHostService } from '../../../../platform/native/common/native.js';
21
import { hasNativeTitlebar, useWindowControlsOverlay, DEFAULT_CUSTOM_TITLEBAR_HEIGHT, hasNativeMenu } from '../../../../platform/window/common/window.js';
22
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
23
import { Codicon } from '../../../../base/common/codicons.js';
24
import { ThemeIcon } from '../../../../base/common/themables.js';
25
import { NativeMenubarControl } from './menubarControl.js';
26
import { IEditorGroupsContainer, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
27
import { IEditorService } from '../../../services/editor/common/editorService.js';
28
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
29
import { CodeWindow, mainWindow } from '../../../../base/browser/window.js';
30
import { IsWindowAlwaysOnTopContext } from '../../../common/contextkeys.js';
31
32
export class NativeTitlebarPart extends BrowserTitlebarPart {
33
34
//#region IView
35
36
override get minimumHeight(): number {
37
if (!isMacintosh) {
38
return super.minimumHeight;
39
}
40
41
return (this.isCommandCenterVisible ? DEFAULT_CUSTOM_TITLEBAR_HEIGHT : this.macTitlebarSize) / (this.preventZoom ? getZoomFactor(getWindow(this.element)) : 1);
42
}
43
override get maximumHeight(): number { return this.minimumHeight; }
44
45
private tahoeOrNewer: boolean;
46
private get macTitlebarSize() {
47
if (this.tahoeOrNewer) {
48
return 32;
49
}
50
51
return 28;
52
}
53
54
//#endregion
55
56
private maxRestoreControl: HTMLElement | undefined;
57
private resizer: HTMLElement | undefined;
58
59
private cachedWindowControlStyles: { bgColor: string; fgColor: string } | undefined;
60
private cachedWindowControlHeight: number | undefined;
61
62
constructor(
63
id: string,
64
targetWindow: CodeWindow,
65
editorGroupsContainer: IEditorGroupsContainer,
66
@IContextMenuService contextMenuService: IContextMenuService,
67
@IConfigurationService configurationService: IConfigurationService,
68
@INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService,
69
@IInstantiationService instantiationService: IInstantiationService,
70
@IThemeService themeService: IThemeService,
71
@IStorageService storageService: IStorageService,
72
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
73
@IContextKeyService contextKeyService: IContextKeyService,
74
@IHostService hostService: IHostService,
75
@INativeHostService private readonly nativeHostService: INativeHostService,
76
@IEditorGroupsService editorGroupService: IEditorGroupsService,
77
@IEditorService editorService: IEditorService,
78
@IMenuService menuService: IMenuService,
79
@IKeybindingService keybindingService: IKeybindingService
80
) {
81
super(id, targetWindow, editorGroupsContainer, contextMenuService, configurationService, environmentService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService, editorService, menuService, keybindingService);
82
83
this.tahoeOrNewer = isTahoeOrNewer(environmentService.os.release);
84
85
this.handleWindowsAlwaysOnTop(targetWindow.vscodeWindowId);
86
}
87
88
private async handleWindowsAlwaysOnTop(targetWindowId: number): Promise<void> {
89
const isWindowAlwaysOnTopContext = IsWindowAlwaysOnTopContext.bindTo(this.contextKeyService);
90
91
this._register(this.nativeHostService.onDidChangeWindowAlwaysOnTop(({ windowId, alwaysOnTop }) => {
92
if (windowId === targetWindowId) {
93
isWindowAlwaysOnTopContext.set(alwaysOnTop);
94
}
95
}));
96
97
isWindowAlwaysOnTopContext.set(await this.nativeHostService.isWindowAlwaysOnTop({ targetWindowId }));
98
}
99
100
protected override onMenubarVisibilityChanged(visible: boolean): void {
101
102
// Hide title when toggling menu bar
103
if ((isWindows || isLinux) && this.currentMenubarVisibility === 'toggle' && visible) {
104
105
// Hack to fix issue #52522 with layered webkit-app-region elements appearing under cursor
106
if (this.dragRegion) {
107
hide(this.dragRegion);
108
setTimeout(() => show(this.dragRegion!), 50);
109
}
110
}
111
112
super.onMenubarVisibilityChanged(visible);
113
}
114
115
protected override onConfigurationChanged(event: IConfigurationChangeEvent): void {
116
super.onConfigurationChanged(event);
117
118
if (event.affectsConfiguration('window.doubleClickIconToClose')) {
119
if (this.appIcon) {
120
this.onUpdateAppIconDragBehavior();
121
}
122
}
123
}
124
125
private onUpdateAppIconDragBehavior(): void {
126
const setting = this.configurationService.getValue('window.doubleClickIconToClose');
127
if (setting && this.appIcon) {
128
(this.appIcon.style as CSSStyleDeclaration & { '-webkit-app-region': string })['-webkit-app-region'] = 'no-drag';
129
} else if (this.appIcon) {
130
(this.appIcon.style as CSSStyleDeclaration & { '-webkit-app-region': string })['-webkit-app-region'] = 'drag';
131
}
132
}
133
134
protected override installMenubar(): void {
135
super.installMenubar();
136
137
if (this.menubar) {
138
return;
139
}
140
141
if (this.customMenubar.value) {
142
this._register(this.customMenubar.value.onFocusStateChange(e => this.onMenubarFocusChanged(e)));
143
}
144
}
145
146
private onMenubarFocusChanged(focused: boolean): void {
147
if ((isWindows || isLinux) && this.currentMenubarVisibility !== 'compact' && this.dragRegion) {
148
if (focused) {
149
hide(this.dragRegion);
150
} else {
151
show(this.dragRegion);
152
}
153
}
154
}
155
156
protected override createContentArea(parent: HTMLElement): HTMLElement {
157
const result = super.createContentArea(parent);
158
const targetWindow = getWindow(parent);
159
const targetWindowId = getWindowId(targetWindow);
160
161
// Native menu controller
162
if (isMacintosh || hasNativeMenu(this.configurationService)) {
163
this._register(this.instantiationService.createInstance(NativeMenubarControl));
164
}
165
166
// App Icon (Native Windows/Linux)
167
if (this.appIcon) {
168
this.onUpdateAppIconDragBehavior();
169
170
this._register(addDisposableListener(this.appIcon, EventType.DBLCLICK, (() => {
171
this.nativeHostService.closeWindow({ targetWindowId });
172
})));
173
}
174
175
// Custom Window Controls (Native Windows/Linux)
176
if (
177
!hasNativeTitlebar(this.configurationService) && // not for native title bars
178
!useWindowControlsOverlay(this.configurationService) && // not when controls are natively drawn
179
this.windowControlsContainer
180
) {
181
182
// Minimize
183
const minimizeIcon = append(this.windowControlsContainer, $('div.window-icon.window-minimize' + ThemeIcon.asCSSSelector(Codicon.chromeMinimize)));
184
this._register(addDisposableListener(minimizeIcon, EventType.CLICK, () => {
185
this.nativeHostService.minimizeWindow({ targetWindowId });
186
}));
187
188
// Restore
189
this.maxRestoreControl = append(this.windowControlsContainer, $('div.window-icon.window-max-restore'));
190
this._register(addDisposableListener(this.maxRestoreControl, EventType.CLICK, async () => {
191
const maximized = await this.nativeHostService.isMaximized({ targetWindowId });
192
if (maximized) {
193
return this.nativeHostService.unmaximizeWindow({ targetWindowId });
194
}
195
196
return this.nativeHostService.maximizeWindow({ targetWindowId });
197
}));
198
199
// Close
200
const closeIcon = append(this.windowControlsContainer, $('div.window-icon.window-close' + ThemeIcon.asCSSSelector(Codicon.chromeClose)));
201
this._register(addDisposableListener(closeIcon, EventType.CLICK, () => {
202
this.nativeHostService.closeWindow({ targetWindowId });
203
}));
204
205
// Resizer
206
this.resizer = append(this.rootContainer, $('div.resizer'));
207
this._register(Event.runAndSubscribe(this.layoutService.onDidChangeWindowMaximized, ({ windowId, maximized }) => {
208
if (windowId === targetWindowId) {
209
this.onDidChangeWindowMaximized(maximized);
210
}
211
}, { windowId: targetWindowId, maximized: this.layoutService.isWindowMaximized(targetWindow) }));
212
}
213
214
// Window System Context Menu
215
// See https://github.com/electron/electron/issues/24893
216
if (isWindows && !hasNativeTitlebar(this.configurationService)) {
217
this._register(this.nativeHostService.onDidTriggerWindowSystemContextMenu(({ windowId, x, y }) => {
218
if (targetWindowId !== windowId) {
219
return;
220
}
221
222
const zoomFactor = getZoomFactor(getWindow(this.element));
223
this.onContextMenu(new MouseEvent(EventType.MOUSE_UP, { clientX: x / zoomFactor, clientY: y / zoomFactor }), MenuId.TitleBarContext);
224
}));
225
}
226
227
return result;
228
}
229
230
private onDidChangeWindowMaximized(maximized: boolean): void {
231
if (this.maxRestoreControl) {
232
if (maximized) {
233
this.maxRestoreControl.classList.remove(...ThemeIcon.asClassNameArray(Codicon.chromeMaximize));
234
this.maxRestoreControl.classList.add(...ThemeIcon.asClassNameArray(Codicon.chromeRestore));
235
} else {
236
this.maxRestoreControl.classList.remove(...ThemeIcon.asClassNameArray(Codicon.chromeRestore));
237
this.maxRestoreControl.classList.add(...ThemeIcon.asClassNameArray(Codicon.chromeMaximize));
238
}
239
}
240
241
if (this.resizer) {
242
if (maximized) {
243
hide(this.resizer);
244
} else {
245
show(this.resizer);
246
}
247
}
248
}
249
250
override updateStyles(): void {
251
super.updateStyles();
252
253
// Part container
254
if (this.element) {
255
if (useWindowControlsOverlay(this.configurationService)) {
256
if (
257
!this.cachedWindowControlStyles ||
258
this.cachedWindowControlStyles.bgColor !== this.element.style.backgroundColor ||
259
this.cachedWindowControlStyles.fgColor !== this.element.style.color
260
) {
261
this.nativeHostService.updateWindowControls({
262
targetWindowId: getWindowId(getWindow(this.element)),
263
backgroundColor: this.element.style.backgroundColor,
264
foregroundColor: this.element.style.color
265
});
266
}
267
}
268
}
269
}
270
271
override layout(width: number, height: number): void {
272
super.layout(width, height);
273
274
if (useWindowControlsOverlay(this.configurationService)) {
275
const newHeight = Math.round(height * getZoomFactor(getWindow(this.element)));
276
if (newHeight !== this.cachedWindowControlHeight) {
277
this.cachedWindowControlHeight = newHeight;
278
this.nativeHostService.updateWindowControls({
279
targetWindowId: getWindowId(getWindow(this.element)),
280
height: newHeight
281
});
282
}
283
}
284
}
285
}
286
287
export class MainNativeTitlebarPart extends NativeTitlebarPart {
288
289
constructor(
290
@IContextMenuService contextMenuService: IContextMenuService,
291
@IConfigurationService configurationService: IConfigurationService,
292
@INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService,
293
@IInstantiationService instantiationService: IInstantiationService,
294
@IThemeService themeService: IThemeService,
295
@IStorageService storageService: IStorageService,
296
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
297
@IContextKeyService contextKeyService: IContextKeyService,
298
@IHostService hostService: IHostService,
299
@INativeHostService nativeHostService: INativeHostService,
300
@IEditorGroupsService editorGroupService: IEditorGroupsService,
301
@IEditorService editorService: IEditorService,
302
@IMenuService menuService: IMenuService,
303
@IKeybindingService keybindingService: IKeybindingService
304
) {
305
super(Parts.TITLEBAR_PART, mainWindow, editorGroupService.mainPart, contextMenuService, configurationService, environmentService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService, nativeHostService, editorGroupService, editorService, menuService, keybindingService);
306
}
307
}
308
309
export class AuxiliaryNativeTitlebarPart extends NativeTitlebarPart implements IAuxiliaryTitlebarPart {
310
311
private static COUNTER = 1;
312
313
get height() { return this.minimumHeight; }
314
315
constructor(
316
readonly container: HTMLElement,
317
editorGroupsContainer: IEditorGroupsContainer,
318
private readonly mainTitlebar: BrowserTitlebarPart,
319
@IContextMenuService contextMenuService: IContextMenuService,
320
@IConfigurationService configurationService: IConfigurationService,
321
@INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService,
322
@IInstantiationService instantiationService: IInstantiationService,
323
@IThemeService themeService: IThemeService,
324
@IStorageService storageService: IStorageService,
325
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
326
@IContextKeyService contextKeyService: IContextKeyService,
327
@IHostService hostService: IHostService,
328
@INativeHostService nativeHostService: INativeHostService,
329
@IEditorGroupsService editorGroupService: IEditorGroupsService,
330
@IEditorService editorService: IEditorService,
331
@IMenuService menuService: IMenuService,
332
@IKeybindingService keybindingService: IKeybindingService
333
) {
334
const id = AuxiliaryNativeTitlebarPart.COUNTER++;
335
super(`workbench.parts.auxiliaryTitle.${id}`, getWindow(container), editorGroupsContainer, contextMenuService, configurationService, environmentService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService, nativeHostService, editorGroupService, editorService, menuService, keybindingService);
336
}
337
338
override get preventZoom(): boolean {
339
340
// Prevent zooming behavior if any of the following conditions are met:
341
// 1. Shrinking below the window control size (zoom < 1)
342
// 2. No custom items are present in the main title bar
343
// The auxiliary title bar never contains any zoomable items itself,
344
// but we want to match the behavior of the main title bar.
345
346
return getZoomFactor(getWindow(this.element)) < 1 || !this.mainTitlebar.hasZoomableElements;
347
}
348
}
349
350
export class NativeTitleService extends BrowserTitleService {
351
352
protected override createMainTitlebarPart(): MainNativeTitlebarPart {
353
return this.instantiationService.createInstance(MainNativeTitlebarPart);
354
}
355
356
protected override doCreateAuxiliaryTitlebarPart(container: HTMLElement, editorGroupsContainer: IEditorGroupsContainer, instantiationService: IInstantiationService): AuxiliaryNativeTitlebarPart {
357
return instantiationService.createInstance(AuxiliaryNativeTitlebarPart, container, editorGroupsContainer, this.mainPart);
358
}
359
}
360
361