Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts
3296 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 './media/titlebarpart.css';
7
import { localize, localize2 } from '../../../../nls.js';
8
import { MultiWindowParts, Part } from '../../part.js';
9
import { ITitleService } from '../../../services/title/browser/titleService.js';
10
import { getWCOTitlebarAreaRect, getZoomFactor, isWCOEnabled } from '../../../../base/browser/browser.js';
11
import { MenuBarVisibility, getTitleBarStyle, getMenuBarVisibility, hasCustomTitlebar, hasNativeTitlebar, DEFAULT_CUSTOM_TITLEBAR_HEIGHT, getWindowControlsStyle, WindowControlsStyle, TitlebarStyle, MenuSettings, hasNativeMenu } from '../../../../platform/window/common/window.js';
12
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
13
import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';
14
import { IConfigurationService, IConfigurationChangeEvent } from '../../../../platform/configuration/common/configuration.js';
15
import { DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
16
import { IBrowserWorkbenchEnvironmentService } from '../../../services/environment/browser/environmentService.js';
17
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
18
import { TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_BACKGROUND, TITLE_BAR_BORDER, WORKBENCH_BACKGROUND } from '../../../common/theme.js';
19
import { isMacintosh, isWindows, isLinux, isWeb, isNative, platformLocale } from '../../../../base/common/platform.js';
20
import { Color } from '../../../../base/common/color.js';
21
import { EventType, EventHelper, Dimension, append, $, addDisposableListener, prepend, reset, getWindow, getWindowId, isAncestor, getActiveDocument, isHTMLElement } from '../../../../base/browser/dom.js';
22
import { CustomMenubarControl } from './menubarControl.js';
23
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
24
import { Emitter, Event } from '../../../../base/common/event.js';
25
import { IStorageService, StorageScope } from '../../../../platform/storage/common/storage.js';
26
import { Parts, IWorkbenchLayoutService, ActivityBarPosition, LayoutSettings, EditorActionsLocation, EditorTabsMode } from '../../../services/layout/browser/layoutService.js';
27
import { createActionViewItem, fillInActionBarActions as fillInActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
28
import { Action2, IMenu, IMenuService, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
29
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
30
import { IHostService } from '../../../services/host/browser/host.js';
31
import { WindowTitle } from './windowTitle.js';
32
import { CommandCenterControl } from './commandCenterControl.js';
33
import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';
34
import { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
35
import { ACCOUNTS_ACTIVITY_ID, GLOBAL_ACTIVITY_ID } from '../../../common/activity.js';
36
import { AccountsActivityActionViewItem, isAccountsActionVisible, SimpleAccountActivityActionViewItem, SimpleGlobalActivityActionViewItem } from '../globalCompositeBar.js';
37
import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js';
38
import { IEditorGroupsContainer, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
39
import { ActionRunner, IAction } from '../../../../base/common/actions.js';
40
import { IEditorService } from '../../../services/editor/common/editorService.js';
41
import { ActionsOrientation, IActionViewItem, prepareActions } from '../../../../base/browser/ui/actionbar/actionbar.js';
42
import { EDITOR_CORE_NAVIGATION_COMMANDS } from '../editor/editorCommands.js';
43
import { AnchorAlignment } from '../../../../base/browser/ui/contextview/contextview.js';
44
import { EditorPane } from '../editor/editorPane.js';
45
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
46
import { ResolvedKeybinding } from '../../../../base/common/keybindings.js';
47
import { EditorCommandsContextActionRunner } from '../editor/editorTabsControl.js';
48
import { IEditorCommandsContext, IEditorPartOptionsChangeEvent, IToolbarActions } from '../../../common/editor.js';
49
import { CodeWindow, mainWindow } from '../../../../base/browser/window.js';
50
import { ACCOUNTS_ACTIVITY_TILE_ACTION, GLOBAL_ACTIVITY_TITLE_ACTION } from './titlebarActions.js';
51
import { IView } from '../../../../base/browser/ui/grid/grid.js';
52
import { createInstantHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
53
import { IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
54
import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js';
55
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
56
import { safeIntl } from '../../../../base/common/date.js';
57
import { IsCompactTitleBarContext, TitleBarVisibleContext } from '../../../common/contextkeys.js';
58
59
export interface ITitleVariable {
60
readonly name: string;
61
readonly contextKey: string;
62
}
63
64
export interface ITitleProperties {
65
isPure?: boolean;
66
isAdmin?: boolean;
67
prefix?: string;
68
}
69
70
export interface ITitlebarPart extends IDisposable {
71
72
/**
73
* An event when the menubar visibility changes.
74
*/
75
readonly onMenubarVisibilityChange: Event<boolean>;
76
77
/**
78
* Update some environmental title properties.
79
*/
80
updateProperties(properties: ITitleProperties): void;
81
82
/**
83
* Adds variables to be supported in the window title.
84
*/
85
registerVariables(variables: ITitleVariable[]): void;
86
}
87
88
export class BrowserTitleService extends MultiWindowParts<BrowserTitlebarPart> implements ITitleService {
89
90
declare _serviceBrand: undefined;
91
92
readonly mainPart: BrowserTitlebarPart;
93
94
constructor(
95
@IInstantiationService protected readonly instantiationService: IInstantiationService,
96
@IStorageService storageService: IStorageService,
97
@IThemeService themeService: IThemeService
98
) {
99
super('workbench.titleService', themeService, storageService);
100
101
this.mainPart = this._register(this.createMainTitlebarPart());
102
this.onMenubarVisibilityChange = this.mainPart.onMenubarVisibilityChange;
103
this._register(this.registerPart(this.mainPart));
104
105
this.registerActions();
106
this.registerAPICommands();
107
}
108
109
protected createMainTitlebarPart(): BrowserTitlebarPart {
110
return this.instantiationService.createInstance(MainBrowserTitlebarPart);
111
}
112
113
private registerActions(): void {
114
115
// Focus action
116
const that = this;
117
this._register(registerAction2(class FocusTitleBar extends Action2 {
118
119
constructor() {
120
super({
121
id: `workbench.action.focusTitleBar`,
122
title: localize2('focusTitleBar', 'Focus Title Bar'),
123
category: Categories.View,
124
f1: true,
125
precondition: TitleBarVisibleContext
126
});
127
}
128
129
run(): void {
130
that.getPartByDocument(getActiveDocument())?.focus();
131
}
132
}));
133
}
134
135
private registerAPICommands(): void {
136
this._register(CommandsRegistry.registerCommand({
137
id: 'registerWindowTitleVariable',
138
handler: (accessor: ServicesAccessor, name: string, contextKey: string) => {
139
this.registerVariables([{ name, contextKey }]);
140
},
141
metadata: {
142
description: 'Registers a new title variable',
143
args: [
144
{ name: 'name', schema: { type: 'string' }, description: 'The name of the variable to register' },
145
{ name: 'contextKey', schema: { type: 'string' }, description: 'The context key to use for the value of the variable' }
146
]
147
}
148
}));
149
}
150
151
//#region Auxiliary Titlebar Parts
152
153
createAuxiliaryTitlebarPart(container: HTMLElement, editorGroupsContainer: IEditorGroupsContainer, instantiationService: IInstantiationService): IAuxiliaryTitlebarPart {
154
const titlebarPartContainer = $('.part.titlebar', { role: 'none' });
155
titlebarPartContainer.style.position = 'relative';
156
container.insertBefore(titlebarPartContainer, container.firstChild); // ensure we are first element
157
158
const disposables = new DisposableStore();
159
160
const titlebarPart = this.doCreateAuxiliaryTitlebarPart(titlebarPartContainer, editorGroupsContainer, instantiationService);
161
disposables.add(this.registerPart(titlebarPart));
162
163
disposables.add(Event.runAndSubscribe(titlebarPart.onDidChange, () => titlebarPartContainer.style.height = `${titlebarPart.height}px`));
164
titlebarPart.create(titlebarPartContainer);
165
166
if (this.properties) {
167
titlebarPart.updateProperties(this.properties);
168
}
169
170
if (this.variables.size) {
171
titlebarPart.registerVariables(Array.from(this.variables.values()));
172
}
173
174
Event.once(titlebarPart.onWillDispose)(() => disposables.dispose());
175
176
return titlebarPart;
177
}
178
179
protected doCreateAuxiliaryTitlebarPart(container: HTMLElement, editorGroupsContainer: IEditorGroupsContainer, instantiationService: IInstantiationService): BrowserTitlebarPart & IAuxiliaryTitlebarPart {
180
return instantiationService.createInstance(AuxiliaryBrowserTitlebarPart, container, editorGroupsContainer, this.mainPart);
181
}
182
183
//#endregion
184
185
186
//#region Service Implementation
187
188
readonly onMenubarVisibilityChange: Event<boolean>;
189
190
private properties: ITitleProperties | undefined = undefined;
191
192
updateProperties(properties: ITitleProperties): void {
193
this.properties = properties;
194
195
for (const part of this.parts) {
196
part.updateProperties(properties);
197
}
198
}
199
200
private readonly variables = new Map<string, ITitleVariable>();
201
202
registerVariables(variables: ITitleVariable[]): void {
203
const newVariables: ITitleVariable[] = [];
204
205
for (const variable of variables) {
206
if (!this.variables.has(variable.name)) {
207
this.variables.set(variable.name, variable);
208
newVariables.push(variable);
209
}
210
}
211
212
for (const part of this.parts) {
213
part.registerVariables(newVariables);
214
}
215
}
216
217
//#endregion
218
}
219
220
export class BrowserTitlebarPart extends Part implements ITitlebarPart {
221
222
//#region IView
223
224
readonly minimumWidth: number = 0;
225
readonly maximumWidth: number = Number.POSITIVE_INFINITY;
226
227
get minimumHeight(): number {
228
const wcoEnabled = isWeb && isWCOEnabled();
229
let value = this.isCommandCenterVisible || wcoEnabled ? DEFAULT_CUSTOM_TITLEBAR_HEIGHT : 30;
230
if (wcoEnabled) {
231
value = Math.max(value, getWCOTitlebarAreaRect(getWindow(this.element))?.height ?? 0);
232
}
233
234
return value / (this.preventZoom ? getZoomFactor(getWindow(this.element)) : 1);
235
}
236
237
get maximumHeight(): number { return this.minimumHeight; }
238
239
//#endregion
240
241
//#region Events
242
243
private _onMenubarVisibilityChange = this._register(new Emitter<boolean>());
244
readonly onMenubarVisibilityChange = this._onMenubarVisibilityChange.event;
245
246
private readonly _onWillDispose = this._register(new Emitter<void>());
247
readonly onWillDispose = this._onWillDispose.event;
248
249
//#endregion
250
251
protected rootContainer!: HTMLElement;
252
protected windowControlsContainer: HTMLElement | undefined;
253
254
protected dragRegion: HTMLElement | undefined;
255
private title!: HTMLElement;
256
257
private leftContent!: HTMLElement;
258
private centerContent!: HTMLElement;
259
private rightContent!: HTMLElement;
260
261
protected readonly customMenubar = this._register(new MutableDisposable<CustomMenubarControl>());
262
protected appIcon: HTMLElement | undefined;
263
private appIconBadge: HTMLElement | undefined;
264
protected menubar?: HTMLElement;
265
private lastLayoutDimensions: Dimension | undefined;
266
267
private actionToolBar!: WorkbenchToolBar;
268
private readonly actionToolBarDisposable = this._register(new DisposableStore());
269
private readonly editorActionsChangeDisposable = this._register(new DisposableStore());
270
private actionToolBarElement!: HTMLElement;
271
272
private globalToolbarMenu: IMenu | undefined;
273
private layoutToolbarMenu: IMenu | undefined;
274
275
private readonly globalToolbarMenuDisposables = this._register(new DisposableStore());
276
private readonly editorToolbarMenuDisposables = this._register(new DisposableStore());
277
private readonly layoutToolbarMenuDisposables = this._register(new DisposableStore());
278
private readonly activityToolbarDisposables = this._register(new DisposableStore());
279
280
private readonly hoverDelegate: IHoverDelegate;
281
282
private readonly titleDisposables = this._register(new DisposableStore());
283
private titleBarStyle: TitlebarStyle;
284
285
private isInactive: boolean = false;
286
287
private readonly isAuxiliary: boolean;
288
private isCompact = false;
289
290
private readonly isCompactContextKey: IContextKey<boolean>;
291
292
private readonly windowTitle: WindowTitle;
293
294
constructor(
295
id: string,
296
targetWindow: CodeWindow,
297
private readonly editorGroupsContainer: IEditorGroupsContainer,
298
@IContextMenuService private readonly contextMenuService: IContextMenuService,
299
@IConfigurationService protected readonly configurationService: IConfigurationService,
300
@IBrowserWorkbenchEnvironmentService protected readonly environmentService: IBrowserWorkbenchEnvironmentService,
301
@IInstantiationService protected readonly instantiationService: IInstantiationService,
302
@IThemeService themeService: IThemeService,
303
@IStorageService private readonly storageService: IStorageService,
304
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
305
@IContextKeyService protected readonly contextKeyService: IContextKeyService,
306
@IHostService private readonly hostService: IHostService,
307
@IEditorService private readonly editorService: IEditorService,
308
@IMenuService private readonly menuService: IMenuService,
309
@IKeybindingService private readonly keybindingService: IKeybindingService
310
) {
311
super(id, { hasTitle: false }, themeService, storageService, layoutService);
312
313
this.isAuxiliary = targetWindow.vscodeWindowId !== mainWindow.vscodeWindowId;
314
315
this.isCompactContextKey = IsCompactTitleBarContext.bindTo(this.contextKeyService);
316
317
this.titleBarStyle = getTitleBarStyle(this.configurationService);
318
319
this.windowTitle = this._register(instantiationService.createInstance(WindowTitle, targetWindow));
320
321
this.hoverDelegate = this._register(createInstantHoverDelegate());
322
323
this.registerListeners(getWindowId(targetWindow));
324
}
325
326
private registerListeners(targetWindowId: number): void {
327
this._register(this.hostService.onDidChangeFocus(focused => focused ? this.onFocus() : this.onBlur()));
328
this._register(this.hostService.onDidChangeActiveWindow(windowId => windowId === targetWindowId ? this.onFocus() : this.onBlur()));
329
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChanged(e)));
330
this._register(this.editorGroupsContainer.onDidChangeEditorPartOptions(e => this.onEditorPartConfigurationChange(e)));
331
}
332
333
private onBlur(): void {
334
this.isInactive = true;
335
336
this.updateStyles();
337
}
338
339
private onFocus(): void {
340
this.isInactive = false;
341
342
this.updateStyles();
343
}
344
345
private onEditorPartConfigurationChange({ oldPartOptions, newPartOptions }: IEditorPartOptionsChangeEvent): void {
346
if (
347
oldPartOptions.editorActionsLocation !== newPartOptions.editorActionsLocation ||
348
oldPartOptions.showTabs !== newPartOptions.showTabs
349
) {
350
if (hasCustomTitlebar(this.configurationService, this.titleBarStyle) && this.actionToolBar) {
351
this.createActionToolBar();
352
this.createActionToolBarMenus({ editorActions: true });
353
this._onDidChange.fire(undefined);
354
}
355
}
356
}
357
358
protected onConfigurationChanged(event: IConfigurationChangeEvent): void {
359
360
// Custom menu bar (disabled if auxiliary)
361
if (!this.isAuxiliary && !hasNativeMenu(this.configurationService, this.titleBarStyle) && (!isMacintosh || isWeb)) {
362
if (event.affectsConfiguration(MenuSettings.MenuBarVisibility)) {
363
if (this.currentMenubarVisibility === 'compact') {
364
this.uninstallMenubar();
365
} else {
366
this.installMenubar();
367
}
368
}
369
}
370
371
// Actions
372
if (hasCustomTitlebar(this.configurationService, this.titleBarStyle) && this.actionToolBar) {
373
const affectsLayoutControl = event.affectsConfiguration(LayoutSettings.LAYOUT_ACTIONS);
374
const affectsActivityControl = event.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION);
375
376
if (affectsLayoutControl || affectsActivityControl) {
377
this.createActionToolBarMenus({ layoutActions: affectsLayoutControl, activityActions: affectsActivityControl });
378
379
this._onDidChange.fire(undefined);
380
}
381
}
382
383
// Command Center
384
if (event.affectsConfiguration(LayoutSettings.COMMAND_CENTER)) {
385
this.recreateTitle();
386
}
387
}
388
389
private recreateTitle(): void {
390
this.createTitle();
391
392
this._onDidChange.fire(undefined);
393
}
394
395
updateOptions(options: { compact: boolean }): void {
396
const oldIsCompact = this.isCompact;
397
this.isCompact = options.compact;
398
399
this.isCompactContextKey.set(this.isCompact);
400
401
if (oldIsCompact !== this.isCompact) {
402
this.recreateTitle();
403
this.createActionToolBarMenus(true);
404
}
405
}
406
407
protected installMenubar(): void {
408
if (this.menubar) {
409
return; // If the menubar is already installed, skip
410
}
411
412
this.customMenubar.value = this.instantiationService.createInstance(CustomMenubarControl);
413
414
this.menubar = append(this.leftContent, $('div.menubar'));
415
this.menubar.setAttribute('role', 'menubar');
416
417
this._register(this.customMenubar.value.onVisibilityChange(e => this.onMenubarVisibilityChanged(e)));
418
419
this.customMenubar.value.create(this.menubar);
420
}
421
422
private uninstallMenubar(): void {
423
this.customMenubar.value = undefined;
424
425
this.menubar?.remove();
426
this.menubar = undefined;
427
428
this.onMenubarVisibilityChanged(false);
429
}
430
431
protected onMenubarVisibilityChanged(visible: boolean): void {
432
if (isWeb || isWindows || isLinux) {
433
if (this.lastLayoutDimensions) {
434
this.layout(this.lastLayoutDimensions.width, this.lastLayoutDimensions.height);
435
}
436
437
this._onMenubarVisibilityChange.fire(visible);
438
}
439
}
440
441
updateProperties(properties: ITitleProperties): void {
442
this.windowTitle.updateProperties(properties);
443
}
444
445
registerVariables(variables: ITitleVariable[]): void {
446
this.windowTitle.registerVariables(variables);
447
}
448
449
protected override createContentArea(parent: HTMLElement): HTMLElement {
450
this.element = parent;
451
this.rootContainer = append(parent, $('.titlebar-container'));
452
453
this.leftContent = append(this.rootContainer, $('.titlebar-left'));
454
this.centerContent = append(this.rootContainer, $('.titlebar-center'));
455
this.rightContent = append(this.rootContainer, $('.titlebar-right'));
456
457
// App Icon (Windows, Linux)
458
if ((isWindows || isLinux) && !hasNativeTitlebar(this.configurationService, this.titleBarStyle)) {
459
this.appIcon = prepend(this.leftContent, $('a.window-appicon'));
460
}
461
462
// Draggable region that we can manipulate for #52522
463
this.dragRegion = prepend(this.rootContainer, $('div.titlebar-drag-region'));
464
465
// Menubar: install a custom menu bar depending on configuration
466
if (
467
!this.isAuxiliary &&
468
!hasNativeMenu(this.configurationService, this.titleBarStyle) &&
469
(!isMacintosh || isWeb) &&
470
this.currentMenubarVisibility !== 'compact'
471
) {
472
this.installMenubar();
473
}
474
475
// Title
476
this.title = append(this.centerContent, $('div.window-title'));
477
this.createTitle();
478
479
// Create Toolbar Actions
480
if (hasCustomTitlebar(this.configurationService, this.titleBarStyle)) {
481
this.actionToolBarElement = append(this.rightContent, $('div.action-toolbar-container'));
482
this.createActionToolBar();
483
this.createActionToolBarMenus();
484
}
485
486
// Window Controls Container
487
if (!hasNativeTitlebar(this.configurationService, this.titleBarStyle)) {
488
let primaryWindowControlsLocation = isMacintosh ? 'left' : 'right';
489
if (isMacintosh && isNative) {
490
491
// Check if the locale is RTL, macOS will move traffic lights in RTL locales
492
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/textInfo
493
494
const localeInfo = safeIntl.Locale(platformLocale).value as any;
495
if (localeInfo?.textInfo?.direction === 'rtl') {
496
primaryWindowControlsLocation = 'right';
497
}
498
}
499
500
if (isMacintosh && isNative && primaryWindowControlsLocation === 'left') {
501
// macOS native: controls are on the left and the container is not needed to make room
502
// for something, except for web where a custom menu being supported). not putting the
503
// container helps with allowing to move the window when clicking very close to the
504
// window control buttons.
505
} else if (getWindowControlsStyle(this.configurationService) === WindowControlsStyle.HIDDEN) {
506
// Linux/Windows: controls are explicitly disabled
507
} else {
508
this.windowControlsContainer = append(primaryWindowControlsLocation === 'left' ? this.leftContent : this.rightContent, $('div.window-controls-container'));
509
if (isWeb) {
510
// Web: its possible to have control overlays on both sides, for example on macOS
511
// with window controls on the left and PWA controls on the right.
512
append(primaryWindowControlsLocation === 'left' ? this.rightContent : this.leftContent, $('div.window-controls-container'));
513
}
514
515
if (isWCOEnabled()) {
516
this.windowControlsContainer.classList.add('wco-enabled');
517
}
518
}
519
}
520
521
// Context menu over title bar: depending on the OS and the location of the click this will either be
522
// the overall context menu for the entire title bar or a specific title context menu.
523
// Windows / Linux: we only support the overall context menu on the title bar
524
// macOS: we support both the overall context menu and the title context menu.
525
// in addition, we allow Cmd+click to bring up the title context menu.
526
{
527
this._register(addDisposableListener(this.rootContainer, EventType.CONTEXT_MENU, e => {
528
EventHelper.stop(e);
529
530
let targetMenu: MenuId;
531
if (isMacintosh && isHTMLElement(e.target) && isAncestor(e.target, this.title)) {
532
targetMenu = MenuId.TitleBarTitleContext;
533
} else {
534
targetMenu = MenuId.TitleBarContext;
535
}
536
537
this.onContextMenu(e, targetMenu);
538
}));
539
540
if (isMacintosh) {
541
this._register(addDisposableListener(this.title, EventType.MOUSE_DOWN, e => {
542
if (e.metaKey) {
543
EventHelper.stop(e, true /* stop bubbling to prevent command center from opening */);
544
545
this.onContextMenu(e, MenuId.TitleBarTitleContext);
546
}
547
}, true /* capture phase to prevent command center from opening */));
548
}
549
}
550
551
this.updateStyles();
552
553
return this.element;
554
}
555
556
private createTitle(): void {
557
this.titleDisposables.clear();
558
559
const isShowingTitleInNativeTitlebar = hasNativeTitlebar(this.configurationService, this.titleBarStyle);
560
561
// Text Title
562
if (!this.isCommandCenterVisible) {
563
if (!isShowingTitleInNativeTitlebar) {
564
this.title.textContent = this.windowTitle.value;
565
this.titleDisposables.add(this.windowTitle.onDidChange(() => {
566
this.title.textContent = this.windowTitle.value;
567
if (this.lastLayoutDimensions) {
568
this.updateLayout(this.lastLayoutDimensions); // layout menubar and other renderings in the titlebar
569
}
570
}));
571
} else {
572
reset(this.title);
573
}
574
}
575
576
// Menu Title
577
else {
578
const commandCenter = this.instantiationService.createInstance(CommandCenterControl, this.windowTitle, this.hoverDelegate);
579
reset(this.title, commandCenter.element);
580
this.titleDisposables.add(commandCenter);
581
}
582
}
583
584
private actionViewItemProvider(action: IAction, options: IBaseActionViewItemOptions): IActionViewItem | undefined {
585
586
// --- Activity Actions
587
if (!this.isAuxiliary) {
588
if (action.id === GLOBAL_ACTIVITY_ID) {
589
return this.instantiationService.createInstance(SimpleGlobalActivityActionViewItem, { position: () => HoverPosition.BELOW }, options);
590
}
591
if (action.id === ACCOUNTS_ACTIVITY_ID) {
592
return this.instantiationService.createInstance(SimpleAccountActivityActionViewItem, { position: () => HoverPosition.BELOW }, options);
593
}
594
}
595
596
// --- Editor Actions
597
const activeEditorPane = this.editorGroupsContainer.activeGroup?.activeEditorPane;
598
if (activeEditorPane && activeEditorPane instanceof EditorPane) {
599
const result = activeEditorPane.getActionViewItem(action, options);
600
601
if (result) {
602
return result;
603
}
604
}
605
606
// Check extensions
607
return createActionViewItem(this.instantiationService, action, { ...options, menuAsChild: false });
608
}
609
610
private getKeybinding(action: IAction): ResolvedKeybinding | undefined {
611
const editorPaneAwareContextKeyService = this.editorGroupsContainer.activeGroup?.activeEditorPane?.scopedContextKeyService ?? this.contextKeyService;
612
613
return this.keybindingService.lookupKeybinding(action.id, editorPaneAwareContextKeyService);
614
}
615
616
private createActionToolBar(): void {
617
618
// Creates the action tool bar. Depends on the configuration of the title bar menus
619
// Requires to be recreated whenever editor actions enablement changes
620
621
this.actionToolBarDisposable.clear();
622
623
this.actionToolBar = this.actionToolBarDisposable.add(this.instantiationService.createInstance(WorkbenchToolBar, this.actionToolBarElement, {
624
contextMenu: MenuId.TitleBarContext,
625
orientation: ActionsOrientation.HORIZONTAL,
626
ariaLabel: localize('ariaLabelTitleActions', "Title actions"),
627
getKeyBinding: action => this.getKeybinding(action),
628
overflowBehavior: { maxItems: 9, exempted: [ACCOUNTS_ACTIVITY_ID, GLOBAL_ACTIVITY_ID, ...EDITOR_CORE_NAVIGATION_COMMANDS] },
629
anchorAlignmentProvider: () => AnchorAlignment.RIGHT,
630
telemetrySource: 'titlePart',
631
highlightToggledItems: this.editorActionsEnabled || this.isAuxiliary, // Only show toggled state for editor actions or auxiliary title bars
632
actionViewItemProvider: (action, options) => this.actionViewItemProvider(action, options),
633
hoverDelegate: this.hoverDelegate
634
}));
635
636
if (this.editorActionsEnabled) {
637
this.actionToolBarDisposable.add(this.editorGroupsContainer.onDidChangeActiveGroup(() => this.createActionToolBarMenus({ editorActions: true })));
638
}
639
}
640
641
private createActionToolBarMenus(update: true | { editorActions?: boolean; layoutActions?: boolean; globalActions?: boolean; activityActions?: boolean } = true): void {
642
if (update === true) {
643
update = { editorActions: true, layoutActions: true, globalActions: true, activityActions: true };
644
}
645
646
const updateToolBarActions = () => {
647
const actions: IToolbarActions = { primary: [], secondary: [] };
648
649
// --- Editor Actions
650
if (this.editorActionsEnabled) {
651
this.editorActionsChangeDisposable.clear();
652
653
const activeGroup = this.editorGroupsContainer.activeGroup;
654
if (activeGroup) {
655
const editorActions = activeGroup.createEditorActions(this.editorActionsChangeDisposable, this.isAuxiliary && this.isCompact ? MenuId.CompactWindowEditorTitle : MenuId.EditorTitle);
656
657
actions.primary.push(...editorActions.actions.primary);
658
actions.secondary.push(...editorActions.actions.secondary);
659
660
this.editorActionsChangeDisposable.add(editorActions.onDidChange(() => updateToolBarActions()));
661
}
662
}
663
664
// --- Global Actions
665
if (this.globalToolbarMenu) {
666
fillInActionBarActions(
667
this.globalToolbarMenu.getActions(),
668
actions
669
);
670
}
671
672
// --- Layout Actions
673
if (this.layoutToolbarMenu) {
674
fillInActionBarActions(
675
this.layoutToolbarMenu.getActions(),
676
actions,
677
() => !this.editorActionsEnabled || this.isCompact // layout actions move to "..." if editor actions are enabled unless compact
678
);
679
}
680
681
// --- Activity Actions (always at the end)
682
if (this.activityActionsEnabled) {
683
if (isAccountsActionVisible(this.storageService)) {
684
actions.primary.push(ACCOUNTS_ACTIVITY_TILE_ACTION);
685
}
686
687
actions.primary.push(GLOBAL_ACTIVITY_TITLE_ACTION);
688
}
689
690
this.actionToolBar.setActions(prepareActions(actions.primary), prepareActions(actions.secondary));
691
};
692
693
// Create/Update the menus which should be in the title tool bar
694
695
if (update.editorActions) {
696
this.editorToolbarMenuDisposables.clear();
697
698
// The editor toolbar menu is handled by the editor group so we do not need to manage it here.
699
// However, depending on the active editor, we need to update the context and action runner of the toolbar menu.
700
if (this.editorActionsEnabled && this.editorService.activeEditor !== undefined) {
701
const context: IEditorCommandsContext = { groupId: this.editorGroupsContainer.activeGroup.id };
702
703
this.actionToolBar.actionRunner = this.editorToolbarMenuDisposables.add(new EditorCommandsContextActionRunner(context));
704
this.actionToolBar.context = context;
705
} else {
706
this.actionToolBar.actionRunner = this.editorToolbarMenuDisposables.add(new ActionRunner());
707
this.actionToolBar.context = undefined;
708
}
709
}
710
711
if (update.layoutActions) {
712
this.layoutToolbarMenuDisposables.clear();
713
714
if (this.layoutControlEnabled) {
715
this.layoutToolbarMenu = this.menuService.createMenu(MenuId.LayoutControlMenu, this.contextKeyService);
716
717
this.layoutToolbarMenuDisposables.add(this.layoutToolbarMenu);
718
this.layoutToolbarMenuDisposables.add(this.layoutToolbarMenu.onDidChange(() => updateToolBarActions()));
719
} else {
720
this.layoutToolbarMenu = undefined;
721
}
722
}
723
724
if (update.globalActions) {
725
this.globalToolbarMenuDisposables.clear();
726
727
if (this.globalActionsEnabled) {
728
this.globalToolbarMenu = this.menuService.createMenu(MenuId.TitleBar, this.contextKeyService);
729
730
this.globalToolbarMenuDisposables.add(this.globalToolbarMenu);
731
this.globalToolbarMenuDisposables.add(this.globalToolbarMenu.onDidChange(() => updateToolBarActions()));
732
} else {
733
this.globalToolbarMenu = undefined;
734
}
735
}
736
737
if (update.activityActions) {
738
this.activityToolbarDisposables.clear();
739
if (this.activityActionsEnabled) {
740
this.activityToolbarDisposables.add(this.storageService.onDidChangeValue(StorageScope.PROFILE, AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, this._store)(() => updateToolBarActions()));
741
}
742
}
743
744
updateToolBarActions();
745
}
746
747
override updateStyles(): void {
748
super.updateStyles();
749
750
// Part container
751
if (this.element) {
752
if (this.isInactive) {
753
this.element.classList.add('inactive');
754
} else {
755
this.element.classList.remove('inactive');
756
}
757
758
const titleBackground = this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_BACKGROUND : TITLE_BAR_ACTIVE_BACKGROUND, (color, theme) => {
759
// LCD Rendering Support: the title bar part is a defining its own GPU layer.
760
// To benefit from LCD font rendering, we must ensure that we always set an
761
// opaque background color. As such, we compute an opaque color given we know
762
// the background color is the workbench background.
763
return color.isOpaque() ? color : color.makeOpaque(WORKBENCH_BACKGROUND(theme));
764
}) || '';
765
this.element.style.backgroundColor = titleBackground;
766
767
if (this.appIconBadge) {
768
this.appIconBadge.style.backgroundColor = titleBackground;
769
}
770
771
if (titleBackground && Color.fromHex(titleBackground).isLighter()) {
772
this.element.classList.add('light');
773
} else {
774
this.element.classList.remove('light');
775
}
776
777
const titleForeground = this.getColor(this.isInactive ? TITLE_BAR_INACTIVE_FOREGROUND : TITLE_BAR_ACTIVE_FOREGROUND);
778
this.element.style.color = titleForeground || '';
779
780
const titleBorder = this.getColor(TITLE_BAR_BORDER);
781
this.element.style.borderBottom = titleBorder ? `1px solid ${titleBorder}` : '';
782
}
783
}
784
785
protected onContextMenu(e: MouseEvent, menuId: MenuId): void {
786
const event = new StandardMouseEvent(getWindow(this.element), e);
787
788
// Show it
789
this.contextMenuService.showContextMenu({
790
getAnchor: () => event,
791
menuId,
792
contextKeyService: this.contextKeyService,
793
domForShadowRoot: isMacintosh && isNative ? event.target : undefined
794
});
795
}
796
797
protected get currentMenubarVisibility(): MenuBarVisibility {
798
if (this.isAuxiliary) {
799
return 'hidden';
800
}
801
802
return getMenuBarVisibility(this.configurationService);
803
}
804
805
private get layoutControlEnabled(): boolean {
806
return this.configurationService.getValue<boolean>(LayoutSettings.LAYOUT_ACTIONS) !== false;
807
}
808
809
protected get isCommandCenterVisible() {
810
return !this.isCompact && this.configurationService.getValue<boolean>(LayoutSettings.COMMAND_CENTER) !== false;
811
}
812
813
private get editorActionsEnabled(): boolean {
814
return (this.editorGroupsContainer.partOptions.editorActionsLocation === EditorActionsLocation.TITLEBAR ||
815
(
816
this.editorGroupsContainer.partOptions.editorActionsLocation === EditorActionsLocation.DEFAULT &&
817
this.editorGroupsContainer.partOptions.showTabs === EditorTabsMode.NONE
818
));
819
}
820
821
private get activityActionsEnabled(): boolean {
822
const activityBarPosition = this.configurationService.getValue<ActivityBarPosition>(LayoutSettings.ACTIVITY_BAR_LOCATION);
823
return !this.isCompact && !this.isAuxiliary && (activityBarPosition === ActivityBarPosition.TOP || activityBarPosition === ActivityBarPosition.BOTTOM);
824
}
825
826
private get globalActionsEnabled(): boolean {
827
return !this.isCompact;
828
}
829
830
get hasZoomableElements(): boolean {
831
const hasMenubar = !(this.currentMenubarVisibility === 'hidden' || this.currentMenubarVisibility === 'compact' || (!isWeb && isMacintosh));
832
const hasCommandCenter = this.isCommandCenterVisible;
833
const hasToolBarActions = this.globalActionsEnabled || this.layoutControlEnabled || this.editorActionsEnabled || this.activityActionsEnabled;
834
return hasMenubar || hasCommandCenter || hasToolBarActions;
835
}
836
837
get preventZoom(): boolean {
838
// Prevent zooming behavior if any of the following conditions are met:
839
// 1. Shrinking below the window control size (zoom < 1)
840
// 2. No custom items are present in the title bar
841
842
return getZoomFactor(getWindow(this.element)) < 1 || !this.hasZoomableElements;
843
}
844
845
override layout(width: number, height: number): void {
846
this.updateLayout(new Dimension(width, height));
847
848
super.layoutContents(width, height);
849
}
850
851
private updateLayout(dimension: Dimension): void {
852
this.lastLayoutDimensions = dimension;
853
854
if (!hasCustomTitlebar(this.configurationService, this.titleBarStyle)) {
855
return;
856
}
857
858
const zoomFactor = getZoomFactor(getWindow(this.element));
859
860
this.element.style.setProperty('--zoom-factor', zoomFactor.toString());
861
this.rootContainer.classList.toggle('counter-zoom', this.preventZoom);
862
863
if (this.customMenubar.value) {
864
const menubarDimension = new Dimension(0, dimension.height);
865
this.customMenubar.value.layout(menubarDimension);
866
}
867
868
const hasCenter = this.isCommandCenterVisible || this.title.textContent !== '';
869
this.rootContainer.classList.toggle('has-center', hasCenter);
870
}
871
872
focus(): void {
873
if (this.customMenubar.value) {
874
this.customMenubar.value.toggleFocus();
875
} else {
876
(this.element.querySelector('[tabindex]:not([tabindex="-1"])') as HTMLElement | null)?.focus();
877
}
878
}
879
880
toJSON(): object {
881
return {
882
type: Parts.TITLEBAR_PART
883
};
884
}
885
886
override dispose(): void {
887
this._onWillDispose.fire();
888
889
super.dispose();
890
}
891
}
892
893
export class MainBrowserTitlebarPart extends BrowserTitlebarPart {
894
895
constructor(
896
@IContextMenuService contextMenuService: IContextMenuService,
897
@IConfigurationService configurationService: IConfigurationService,
898
@IBrowserWorkbenchEnvironmentService environmentService: IBrowserWorkbenchEnvironmentService,
899
@IInstantiationService instantiationService: IInstantiationService,
900
@IThemeService themeService: IThemeService,
901
@IStorageService storageService: IStorageService,
902
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
903
@IContextKeyService contextKeyService: IContextKeyService,
904
@IHostService hostService: IHostService,
905
@IEditorGroupsService editorGroupService: IEditorGroupsService,
906
@IEditorService editorService: IEditorService,
907
@IMenuService menuService: IMenuService,
908
@IKeybindingService keybindingService: IKeybindingService,
909
) {
910
super(Parts.TITLEBAR_PART, mainWindow, editorGroupService.mainPart, contextMenuService, configurationService, environmentService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService, editorService, menuService, keybindingService);
911
}
912
}
913
914
export interface IAuxiliaryTitlebarPart extends ITitlebarPart, IView {
915
readonly container: HTMLElement;
916
readonly height: number;
917
918
updateOptions(options: { compact: boolean }): void;
919
}
920
921
export class AuxiliaryBrowserTitlebarPart extends BrowserTitlebarPart implements IAuxiliaryTitlebarPart {
922
923
private static COUNTER = 1;
924
925
get height() { return this.minimumHeight; }
926
927
constructor(
928
readonly container: HTMLElement,
929
editorGroupsContainer: IEditorGroupsContainer,
930
private readonly mainTitlebar: BrowserTitlebarPart,
931
@IContextMenuService contextMenuService: IContextMenuService,
932
@IConfigurationService configurationService: IConfigurationService,
933
@IBrowserWorkbenchEnvironmentService environmentService: IBrowserWorkbenchEnvironmentService,
934
@IInstantiationService instantiationService: IInstantiationService,
935
@IThemeService themeService: IThemeService,
936
@IStorageService storageService: IStorageService,
937
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
938
@IContextKeyService contextKeyService: IContextKeyService,
939
@IHostService hostService: IHostService,
940
@IEditorGroupsService editorGroupService: IEditorGroupsService,
941
@IEditorService editorService: IEditorService,
942
@IMenuService menuService: IMenuService,
943
@IKeybindingService keybindingService: IKeybindingService,
944
) {
945
const id = AuxiliaryBrowserTitlebarPart.COUNTER++;
946
super(`workbench.parts.auxiliaryTitle.${id}`, getWindow(container), editorGroupsContainer, contextMenuService, configurationService, environmentService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService, editorService, menuService, keybindingService);
947
}
948
949
override get preventZoom(): boolean {
950
951
// Prevent zooming behavior if any of the following conditions are met:
952
// 1. Shrinking below the window control size (zoom < 1)
953
// 2. No custom items are present in the main title bar
954
// The auxiliary title bar never contains any zoomable items itself,
955
// but we want to match the behavior of the main title bar.
956
957
return getZoomFactor(getWindow(this.element)) < 1 || !this.mainTitlebar.hasZoomableElements;
958
}
959
}
960
961