Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/parts/titlebar/menubarControl.ts
5303 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/menubarControl.css';
7
import { localize, localize2 } from '../../../../nls.js';
8
import { IMenuService, MenuId, IMenu, SubmenuItemAction, registerAction2, Action2, MenuItemAction, MenuRegistry } from '../../../../platform/actions/common/actions.js';
9
import { MenuBarVisibility, IWindowOpenable, getMenuBarVisibility, MenuSettings, hasNativeMenu } from '../../../../platform/window/common/window.js';
10
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
11
import { IAction, Action, SubmenuAction, Separator, IActionRunner, ActionRunner, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification, toAction } from '../../../../base/common/actions.js';
12
import { addDisposableListener, Dimension, EventType } from '../../../../base/browser/dom.js';
13
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
14
import { isMacintosh, isWeb, isIOS, isNative } from '../../../../base/common/platform.js';
15
import { IConfigurationService, IConfigurationChangeEvent } from '../../../../platform/configuration/common/configuration.js';
16
import { Event, Emitter } from '../../../../base/common/event.js';
17
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
18
import { IRecentlyOpened, isRecentFolder, IRecent, isRecentWorkspace, IWorkspacesService } from '../../../../platform/workspaces/common/workspaces.js';
19
import { RunOnceScheduler } from '../../../../base/common/async.js';
20
import { URI } from '../../../../base/common/uri.js';
21
import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js';
22
import { IUpdateService, StateType } from '../../../../platform/update/common/update.js';
23
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
24
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
25
import { IPreferencesService } from '../../../services/preferences/common/preferences.js';
26
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
27
import { MenuBar, IMenuBarOptions } from '../../../../base/browser/ui/menu/menubar.js';
28
import { HorizontalDirection, IMenuDirection, VerticalDirection } from '../../../../base/browser/ui/menu/menu.js';
29
import { mnemonicMenuLabel, unmnemonicLabel } from '../../../../base/common/labels.js';
30
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
31
import { isFullscreen, onDidChangeFullscreen } from '../../../../base/browser/browser.js';
32
import { IHostService } from '../../../services/host/browser/host.js';
33
import { BrowserFeatures } from '../../../../base/browser/canIUse.js';
34
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
35
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
36
import { IsMacNativeContext, IsWebContext } from '../../../../platform/contextkey/common/contextkeys.js';
37
import { ICommandService } from '../../../../platform/commands/common/commands.js';
38
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
39
import { OpenRecentAction } from '../../actions/windowActions.js';
40
import { isICommandActionToggleInfo } from '../../../../platform/action/common/action.js';
41
import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
42
import { defaultMenuStyles } from '../../../../platform/theme/browser/defaultStyles.js';
43
import { mainWindow } from '../../../../base/browser/window.js';
44
import { ActivityBarPosition } from '../../../services/layout/browser/layoutService.js';
45
46
export type IOpenRecentAction = IAction & { uri: URI; remoteAuthority?: string };
47
48
MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, {
49
submenu: MenuId.MenubarFileMenu,
50
title: {
51
value: 'File',
52
original: 'File',
53
mnemonicTitle: localize({ key: 'mFile', comment: ['&& denotes a mnemonic'] }, "&&File"),
54
},
55
order: 1
56
});
57
58
MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, {
59
submenu: MenuId.MenubarEditMenu,
60
title: {
61
value: 'Edit',
62
original: 'Edit',
63
mnemonicTitle: localize({ key: 'mEdit', comment: ['&& denotes a mnemonic'] }, "&&Edit")
64
},
65
order: 2
66
});
67
68
MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, {
69
submenu: MenuId.MenubarSelectionMenu,
70
title: {
71
value: 'Selection',
72
original: 'Selection',
73
mnemonicTitle: localize({ key: 'mSelection', comment: ['&& denotes a mnemonic'] }, "&&Selection")
74
},
75
order: 3
76
});
77
78
MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, {
79
submenu: MenuId.MenubarViewMenu,
80
title: {
81
value: 'View',
82
original: 'View',
83
mnemonicTitle: localize({ key: 'mView', comment: ['&& denotes a mnemonic'] }, "&&View")
84
},
85
order: 4
86
});
87
88
MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, {
89
submenu: MenuId.MenubarGoMenu,
90
title: {
91
value: 'Go',
92
original: 'Go',
93
mnemonicTitle: localize({ key: 'mGoto', comment: ['&& denotes a mnemonic'] }, "&&Go")
94
},
95
order: 5
96
});
97
98
MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, {
99
submenu: MenuId.MenubarTerminalMenu,
100
title: {
101
value: 'Terminal',
102
original: 'Terminal',
103
mnemonicTitle: localize({ key: 'mTerminal', comment: ['&& denotes a mnemonic'] }, "&&Terminal")
104
},
105
order: 7
106
});
107
108
MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, {
109
submenu: MenuId.MenubarHelpMenu,
110
title: {
111
value: 'Help',
112
original: 'Help',
113
mnemonicTitle: localize({ key: 'mHelp', comment: ['&& denotes a mnemonic'] }, "&&Help")
114
},
115
order: 8
116
});
117
118
MenuRegistry.appendMenuItem(MenuId.MenubarMainMenu, {
119
submenu: MenuId.MenubarPreferencesMenu,
120
title: {
121
value: 'Preferences',
122
original: 'Preferences',
123
mnemonicTitle: localize({ key: 'mPreferences', comment: ['&& denotes a mnemonic'] }, "Preferences")
124
},
125
when: IsMacNativeContext,
126
order: 9
127
});
128
129
export abstract class MenubarControl extends Disposable {
130
131
protected keys = [
132
MenuSettings.MenuBarVisibility,
133
'window.enableMenuBarMnemonics',
134
'window.customMenuBarAltFocus',
135
'workbench.sideBar.location',
136
'window.nativeTabs'
137
];
138
139
protected mainMenu: IMenu;
140
protected menus: {
141
[index: string]: IMenu | undefined;
142
} = {};
143
144
protected topLevelTitles: { [menu: string]: string } = {};
145
146
protected readonly mainMenuDisposables: DisposableStore;
147
148
protected recentlyOpened: IRecentlyOpened = { files: [], workspaces: [] };
149
150
protected menuUpdater: RunOnceScheduler;
151
152
protected static readonly MAX_MENU_RECENT_ENTRIES = 10;
153
154
constructor(
155
protected readonly menuService: IMenuService,
156
protected readonly workspacesService: IWorkspacesService,
157
protected readonly contextKeyService: IContextKeyService,
158
protected readonly keybindingService: IKeybindingService,
159
protected readonly configurationService: IConfigurationService,
160
protected readonly labelService: ILabelService,
161
protected readonly updateService: IUpdateService,
162
protected readonly storageService: IStorageService,
163
protected readonly notificationService: INotificationService,
164
protected readonly preferencesService: IPreferencesService,
165
protected readonly environmentService: IWorkbenchEnvironmentService,
166
protected readonly accessibilityService: IAccessibilityService,
167
protected readonly hostService: IHostService,
168
protected readonly commandService: ICommandService
169
) {
170
171
super();
172
173
this.mainMenu = this._register(this.menuService.createMenu(MenuId.MenubarMainMenu, this.contextKeyService));
174
this.mainMenuDisposables = this._register(new DisposableStore());
175
176
this.setupMainMenu();
177
178
this.menuUpdater = this._register(new RunOnceScheduler(() => this.doUpdateMenubar(false), 200));
179
180
this.notifyUserOfCustomMenubarAccessibility();
181
}
182
183
protected abstract doUpdateMenubar(firstTime: boolean): void;
184
185
protected registerListeners(): void {
186
// Listen for window focus changes
187
this._register(this.hostService.onDidChangeFocus(e => this.onDidChangeWindowFocus(e)));
188
189
// Update when config changes
190
this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e)));
191
192
// Listen to update service
193
this._register(this.updateService.onStateChange(() => this.onUpdateStateChange()));
194
195
// Listen for changes in recently opened menu
196
this._register(this.workspacesService.onDidChangeRecentlyOpened(() => { this.onDidChangeRecentlyOpened(); }));
197
198
// Listen to keybindings change
199
this._register(this.keybindingService.onDidUpdateKeybindings(() => this.updateMenubar()));
200
201
// Update recent menu items on formatter registration
202
this._register(this.labelService.onDidChangeFormatters(() => { this.onDidChangeRecentlyOpened(); }));
203
204
// Listen for changes on the main menu
205
this._register(this.mainMenu.onDidChange(() => { this.setupMainMenu(); this.doUpdateMenubar(true); }));
206
}
207
208
protected setupMainMenu(): void {
209
this.mainMenuDisposables.clear();
210
this.menus = {};
211
this.topLevelTitles = {};
212
213
const [, mainMenuActions] = this.mainMenu.getActions()[0];
214
for (const mainMenuAction of mainMenuActions) {
215
if (mainMenuAction instanceof SubmenuItemAction && typeof mainMenuAction.item.title !== 'string') {
216
this.menus[mainMenuAction.item.title.original] = this.mainMenuDisposables.add(this.menuService.createMenu(mainMenuAction.item.submenu, this.contextKeyService, { emitEventsForSubmenuChanges: true }));
217
this.topLevelTitles[mainMenuAction.item.title.original] = mainMenuAction.item.title.mnemonicTitle ?? mainMenuAction.item.title.value;
218
}
219
}
220
}
221
222
protected updateMenubar(): void {
223
this.menuUpdater.schedule();
224
}
225
226
protected calculateActionLabel(action: { id: string; label: string }): string {
227
const label = action.label;
228
switch (action.id) {
229
default:
230
break;
231
}
232
233
return label;
234
}
235
236
protected onUpdateStateChange(): void {
237
this.updateMenubar();
238
}
239
240
protected onUpdateKeybindings(): void {
241
this.updateMenubar();
242
}
243
244
protected getOpenRecentActions(): (Separator | IOpenRecentAction)[] {
245
if (!this.recentlyOpened) {
246
return [];
247
}
248
249
const { workspaces, files } = this.recentlyOpened;
250
251
const result = [];
252
253
if (workspaces.length > 0) {
254
for (let i = 0; i < MenubarControl.MAX_MENU_RECENT_ENTRIES && i < workspaces.length; i++) {
255
result.push(this.createOpenRecentMenuAction(workspaces[i]));
256
}
257
258
result.push(new Separator());
259
}
260
261
if (files.length > 0) {
262
for (let i = 0; i < MenubarControl.MAX_MENU_RECENT_ENTRIES && i < files.length; i++) {
263
result.push(this.createOpenRecentMenuAction(files[i]));
264
}
265
266
result.push(new Separator());
267
}
268
269
return result;
270
}
271
272
protected onDidChangeWindowFocus(hasFocus: boolean): void {
273
// When we regain focus, update the recent menu items
274
if (hasFocus) {
275
this.onDidChangeRecentlyOpened();
276
}
277
}
278
279
private onConfigurationUpdated(event: IConfigurationChangeEvent): void {
280
if (this.keys.some(key => event.affectsConfiguration(key))) {
281
this.updateMenubar();
282
}
283
284
if (event.affectsConfiguration('editor.accessibilitySupport')) {
285
this.notifyUserOfCustomMenubarAccessibility();
286
}
287
288
// Since we try not update when hidden, we should
289
// try to update the recently opened list on visibility changes
290
if (event.affectsConfiguration(MenuSettings.MenuBarVisibility)) {
291
this.onDidChangeRecentlyOpened();
292
}
293
}
294
295
private get menubarHidden(): boolean {
296
return isMacintosh && isNative ? false : getMenuBarVisibility(this.configurationService) === 'hidden';
297
}
298
299
protected onDidChangeRecentlyOpened(): void {
300
301
// Do not update recently opened when the menubar is hidden #108712
302
if (!this.menubarHidden) {
303
this.workspacesService.getRecentlyOpened().then(recentlyOpened => {
304
this.recentlyOpened = recentlyOpened;
305
this.updateMenubar();
306
});
307
}
308
}
309
310
private createOpenRecentMenuAction(recent: IRecent): IOpenRecentAction {
311
312
let label: string;
313
let uri: URI;
314
let commandId: string;
315
let openable: IWindowOpenable;
316
const remoteAuthority = recent.remoteAuthority;
317
318
if (isRecentFolder(recent)) {
319
uri = recent.folderUri;
320
label = recent.label || this.labelService.getWorkspaceLabel(uri, { verbose: Verbosity.LONG });
321
commandId = 'openRecentFolder';
322
openable = { folderUri: uri };
323
} else if (isRecentWorkspace(recent)) {
324
uri = recent.workspace.configPath;
325
label = recent.label || this.labelService.getWorkspaceLabel(recent.workspace, { verbose: Verbosity.LONG });
326
commandId = 'openRecentWorkspace';
327
openable = { workspaceUri: uri };
328
} else {
329
uri = recent.fileUri;
330
label = recent.label || this.labelService.getUriLabel(uri, { appendWorkspaceSuffix: true });
331
commandId = 'openRecentFile';
332
openable = { fileUri: uri };
333
}
334
335
const ret = toAction({
336
id: commandId, label: unmnemonicLabel(label), run: (browserEvent: KeyboardEvent) => {
337
const openInNewWindow = browserEvent && ((!isMacintosh && (browserEvent.ctrlKey || browserEvent.shiftKey)) || (isMacintosh && (browserEvent.metaKey || browserEvent.altKey)));
338
339
return this.hostService.openWindow([openable], {
340
forceNewWindow: !!openInNewWindow,
341
remoteAuthority: remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable
342
});
343
}
344
});
345
346
return Object.assign(ret, { uri, remoteAuthority });
347
}
348
349
private notifyUserOfCustomMenubarAccessibility(): void {
350
if (isWeb || isMacintosh) {
351
return;
352
}
353
354
const hasBeenNotified = this.storageService.getBoolean('menubar/accessibleMenubarNotified', StorageScope.APPLICATION, false);
355
const usingCustomMenubar = !hasNativeMenu(this.configurationService);
356
357
if (hasBeenNotified || usingCustomMenubar || !this.accessibilityService.isScreenReaderOptimized()) {
358
return;
359
}
360
361
const message = localize('menubar.customTitlebarAccessibilityNotification', "Accessibility support is enabled for you. For the most accessible experience, we recommend the custom menu style.");
362
this.notificationService.prompt(Severity.Info, message, [
363
{
364
label: localize('goToSetting', "Open Settings"),
365
run: () => {
366
return this.preferencesService.openUserSettings({ query: MenuSettings.MenuStyle });
367
}
368
}
369
]);
370
371
this.storageService.store('menubar/accessibleMenubarNotified', true, StorageScope.APPLICATION, StorageTarget.USER);
372
}
373
}
374
375
// This is a bit complex due to the issue https://github.com/microsoft/vscode/issues/205836
376
let focusMenuBarEmitter: Emitter<void> | undefined = undefined;
377
function enableFocusMenuBarAction(): Emitter<void> {
378
if (!focusMenuBarEmitter) {
379
focusMenuBarEmitter = new Emitter<void>();
380
381
registerAction2(class extends Action2 {
382
constructor() {
383
super({
384
id: `workbench.actions.menubar.focus`,
385
title: localize2('focusMenu', 'Focus Application Menu'),
386
keybinding: {
387
primary: KeyMod.Alt | KeyCode.F10,
388
weight: KeybindingWeight.WorkbenchContrib,
389
when: IsWebContext
390
},
391
f1: true
392
});
393
}
394
395
async run(): Promise<void> {
396
focusMenuBarEmitter?.fire();
397
}
398
});
399
}
400
401
return focusMenuBarEmitter;
402
}
403
404
export class CustomMenubarControl extends MenubarControl {
405
private menubar: MenuBar | undefined;
406
private container: HTMLElement | undefined;
407
private alwaysOnMnemonics: boolean = false;
408
private focusInsideMenubar: boolean = false;
409
private pendingFirstTimeUpdate: boolean = false;
410
private visible: boolean = true;
411
private actionRunner: IActionRunner;
412
private readonly webNavigationMenu = this._register(this.menuService.createMenu(MenuId.MenubarHomeMenu, this.contextKeyService));
413
414
private readonly _onVisibilityChange: Emitter<boolean>;
415
private readonly _onFocusStateChange: Emitter<boolean>;
416
417
constructor(
418
@IMenuService menuService: IMenuService,
419
@IWorkspacesService workspacesService: IWorkspacesService,
420
@IContextKeyService contextKeyService: IContextKeyService,
421
@IKeybindingService keybindingService: IKeybindingService,
422
@IConfigurationService configurationService: IConfigurationService,
423
@ILabelService labelService: ILabelService,
424
@IUpdateService updateService: IUpdateService,
425
@IStorageService storageService: IStorageService,
426
@INotificationService notificationService: INotificationService,
427
@IPreferencesService preferencesService: IPreferencesService,
428
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
429
@IAccessibilityService accessibilityService: IAccessibilityService,
430
@ITelemetryService private readonly telemetryService: ITelemetryService,
431
@IHostService hostService: IHostService,
432
@ICommandService commandService: ICommandService
433
) {
434
super(menuService, workspacesService, contextKeyService, keybindingService, configurationService, labelService, updateService, storageService, notificationService, preferencesService, environmentService, accessibilityService, hostService, commandService);
435
436
this._onVisibilityChange = this._register(new Emitter<boolean>());
437
this._onFocusStateChange = this._register(new Emitter<boolean>());
438
439
this.actionRunner = this._register(new ActionRunner());
440
this._register(this.actionRunner.onDidRun(e => {
441
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: e.action.id, from: 'menu' });
442
}));
443
444
this.workspacesService.getRecentlyOpened().then((recentlyOpened) => {
445
this.recentlyOpened = recentlyOpened;
446
});
447
448
this.registerListeners();
449
}
450
451
protected doUpdateMenubar(firstTime: boolean): void {
452
if (!this.focusInsideMenubar) {
453
this.setupCustomMenubar(firstTime);
454
}
455
456
if (firstTime) {
457
this.pendingFirstTimeUpdate = true;
458
}
459
}
460
461
private getUpdateAction(): IAction | null {
462
const state = this.updateService.state;
463
464
switch (state.type) {
465
case StateType.Idle:
466
return toAction({
467
id: 'update.check', label: localize({ key: 'checkForUpdates', comment: ['&& denotes a mnemonic'] }, "Check for &&Updates..."), enabled: true, run: () =>
468
this.updateService.checkForUpdates(true)
469
});
470
471
case StateType.CheckingForUpdates:
472
return toAction({ id: 'update.checking', label: localize('checkingForUpdates', "Checking for Updates..."), enabled: false, run: () => { } });
473
474
case StateType.AvailableForDownload:
475
return toAction({
476
id: 'update.downloadNow', label: localize({ key: 'download now', comment: ['&& denotes a mnemonic'] }, "D&&ownload Update"), enabled: true, run: () =>
477
this.updateService.downloadUpdate(true)
478
});
479
480
case StateType.Downloading:
481
case StateType.Overwriting:
482
return toAction({ id: 'update.downloading', label: localize('DownloadingUpdate', "Downloading Update..."), enabled: false, run: () => { } });
483
484
case StateType.Downloaded:
485
return isMacintosh ? null : toAction({
486
id: 'update.install', label: localize({ key: 'installUpdate...', comment: ['&& denotes a mnemonic'] }, "Install &&Update..."), enabled: true, run: () =>
487
this.updateService.applyUpdate()
488
});
489
490
case StateType.Updating:
491
return toAction({ id: 'update.updating', label: localize('installingUpdate', "Installing Update..."), enabled: false, run: () => { } });
492
493
case StateType.Ready:
494
return toAction({
495
id: 'update.restart', label: localize({ key: 'restartToUpdate', comment: ['&& denotes a mnemonic'] }, "Restart to &&Update"), enabled: true, run: () =>
496
this.updateService.quitAndInstall()
497
});
498
499
default:
500
return null;
501
}
502
}
503
504
private get currentMenubarVisibility(): MenuBarVisibility {
505
return getMenuBarVisibility(this.configurationService);
506
}
507
508
private get currentDisableMenuBarAltFocus(): boolean {
509
const settingValue = this.configurationService.getValue<boolean>('window.customMenuBarAltFocus');
510
511
let disableMenuBarAltBehavior = false;
512
if (typeof settingValue === 'boolean') {
513
disableMenuBarAltBehavior = !settingValue;
514
}
515
516
return disableMenuBarAltBehavior;
517
}
518
519
private insertActionsBefore(nextAction: IAction, target: IAction[]): void {
520
switch (nextAction.id) {
521
case OpenRecentAction.ID:
522
target.push(...this.getOpenRecentActions());
523
break;
524
525
case 'workbench.action.showAboutDialog':
526
if (!isMacintosh && !isWeb) {
527
const updateAction = this.getUpdateAction();
528
if (updateAction) {
529
updateAction.label = mnemonicMenuLabel(updateAction.label);
530
target.push(updateAction);
531
target.push(new Separator());
532
}
533
}
534
535
break;
536
537
default:
538
break;
539
}
540
}
541
542
private get currentEnableMenuBarMnemonics(): boolean {
543
let enableMenuBarMnemonics = this.configurationService.getValue<boolean>('window.enableMenuBarMnemonics');
544
if (typeof enableMenuBarMnemonics !== 'boolean') {
545
enableMenuBarMnemonics = true;
546
}
547
548
return enableMenuBarMnemonics && (!isWeb || isFullscreen(mainWindow));
549
}
550
551
private get currentCompactMenuMode(): IMenuDirection | undefined {
552
if (this.currentMenubarVisibility !== 'compact') {
553
return undefined;
554
}
555
556
// Menu bar lives in activity bar and should flow based on its location
557
const currentSidebarLocation = this.configurationService.getValue<string>('workbench.sideBar.location');
558
const horizontalDirection = currentSidebarLocation === 'right' ? HorizontalDirection.Left : HorizontalDirection.Right;
559
560
const activityBarLocation = this.configurationService.getValue<string>('workbench.activityBar.location');
561
const verticalDirection = activityBarLocation === ActivityBarPosition.BOTTOM ? VerticalDirection.Above : VerticalDirection.Below;
562
563
return { horizontal: horizontalDirection, vertical: verticalDirection };
564
}
565
566
private onDidVisibilityChange(visible: boolean): void {
567
this.visible = visible;
568
this.onDidChangeRecentlyOpened();
569
this._onVisibilityChange.fire(visible);
570
}
571
572
private toActionsArray(menu: IMenu): IAction[] {
573
return getFlatContextMenuActions(menu.getActions({ shouldForwardArgs: true }));
574
}
575
576
private readonly reinstallDisposables = this._register(new DisposableStore());
577
private readonly updateActionsDisposables = this._register(new DisposableStore());
578
private setupCustomMenubar(firstTime: boolean): void {
579
// If there is no container, we cannot setup the menubar
580
if (!this.container) {
581
return;
582
}
583
584
if (firstTime) {
585
// Reset and create new menubar
586
if (this.menubar) {
587
this.reinstallDisposables.clear();
588
}
589
590
this.menubar = this.reinstallDisposables.add(new MenuBar(this.container, this.getMenuBarOptions(), defaultMenuStyles));
591
592
this.accessibilityService.alwaysUnderlineAccessKeys().then(val => {
593
this.alwaysOnMnemonics = val;
594
this.menubar?.update(this.getMenuBarOptions());
595
});
596
597
this.reinstallDisposables.add(this.menubar.onFocusStateChange(focused => {
598
this._onFocusStateChange.fire(focused);
599
600
// When the menubar loses focus, update it to clear any pending updates
601
if (!focused) {
602
if (this.pendingFirstTimeUpdate) {
603
this.setupCustomMenubar(true);
604
this.pendingFirstTimeUpdate = false;
605
} else {
606
this.updateMenubar();
607
}
608
609
this.focusInsideMenubar = false;
610
}
611
}));
612
613
this.reinstallDisposables.add(this.menubar.onVisibilityChange(e => this.onDidVisibilityChange(e)));
614
615
// Before we focus the menubar, stop updates to it so that focus-related context keys will work
616
this.reinstallDisposables.add(addDisposableListener(this.container, EventType.FOCUS_IN, () => {
617
this.focusInsideMenubar = true;
618
}));
619
620
this.reinstallDisposables.add(addDisposableListener(this.container, EventType.FOCUS_OUT, () => {
621
this.focusInsideMenubar = false;
622
}));
623
624
// Fire visibility change for the first install if menu is shown
625
if (this.menubar.isVisible) {
626
this.onDidVisibilityChange(true);
627
}
628
} else {
629
this.menubar?.update(this.getMenuBarOptions());
630
}
631
632
// Update the menu actions
633
const updateActions = (menuActions: readonly IAction[], target: IAction[], topLevelTitle: string, store: DisposableStore) => {
634
target.splice(0);
635
636
for (const menuItem of menuActions) {
637
this.insertActionsBefore(menuItem, target);
638
639
if (menuItem instanceof Separator) {
640
target.push(menuItem);
641
} else if (menuItem instanceof SubmenuItemAction || menuItem instanceof MenuItemAction) {
642
// use mnemonicTitle whenever possible
643
let title = typeof menuItem.item.title === 'string'
644
? menuItem.item.title
645
: menuItem.item.title.mnemonicTitle ?? menuItem.item.title.value;
646
647
if (menuItem instanceof SubmenuItemAction) {
648
const submenuActions: SubmenuAction[] = [];
649
updateActions(menuItem.actions, submenuActions, topLevelTitle, store);
650
651
if (submenuActions.length > 0) {
652
target.push(new SubmenuAction(menuItem.id, mnemonicMenuLabel(title), submenuActions));
653
}
654
} else {
655
if (isICommandActionToggleInfo(menuItem.item.toggled)) {
656
title = menuItem.item.toggled.mnemonicTitle ?? menuItem.item.toggled.title ?? title;
657
}
658
659
const newAction = store.add(new Action(menuItem.id, mnemonicMenuLabel(title), menuItem.class, menuItem.enabled, () => this.commandService.executeCommand(menuItem.id)));
660
newAction.tooltip = menuItem.tooltip;
661
newAction.checked = menuItem.checked;
662
target.push(newAction);
663
}
664
}
665
666
}
667
668
// Append web navigation menu items to the file menu when not compact
669
if (topLevelTitle === 'File' && this.currentCompactMenuMode === undefined) {
670
const webActions = this.getWebNavigationActions();
671
if (webActions.length) {
672
target.push(...webActions);
673
}
674
}
675
};
676
677
for (const title of Object.keys(this.topLevelTitles)) {
678
const menu = this.menus[title];
679
if (firstTime && menu) {
680
const menuChangedDisposable = this.reinstallDisposables.add(new DisposableStore());
681
this.reinstallDisposables.add(menu.onDidChange(() => {
682
if (!this.focusInsideMenubar) {
683
const actions: IAction[] = [];
684
menuChangedDisposable.clear();
685
updateActions(this.toActionsArray(menu), actions, title, menuChangedDisposable);
686
this.menubar?.updateMenu({ actions, label: mnemonicMenuLabel(this.topLevelTitles[title]) });
687
}
688
}));
689
690
// For the file menu, we need to update if the web nav menu updates as well
691
if (menu === this.menus.File) {
692
const webMenuChangedDisposable = this.reinstallDisposables.add(new DisposableStore());
693
this.reinstallDisposables.add(this.webNavigationMenu.onDidChange(() => {
694
if (!this.focusInsideMenubar) {
695
const actions: IAction[] = [];
696
webMenuChangedDisposable.clear();
697
updateActions(this.toActionsArray(menu), actions, title, webMenuChangedDisposable);
698
this.menubar?.updateMenu({ actions, label: mnemonicMenuLabel(this.topLevelTitles[title]) });
699
}
700
}));
701
}
702
}
703
704
const actions: IAction[] = [];
705
if (menu) {
706
this.updateActionsDisposables.clear();
707
updateActions(this.toActionsArray(menu), actions, title, this.updateActionsDisposables);
708
}
709
710
if (this.menubar) {
711
if (!firstTime) {
712
this.menubar.updateMenu({ actions, label: mnemonicMenuLabel(this.topLevelTitles[title]) });
713
} else {
714
this.menubar.push({ actions, label: mnemonicMenuLabel(this.topLevelTitles[title]) });
715
}
716
}
717
}
718
}
719
720
private getWebNavigationActions(): IAction[] {
721
if (!isWeb) {
722
return []; // only for web
723
}
724
725
const webNavigationActions = [];
726
for (const groups of this.webNavigationMenu.getActions()) {
727
const [, actions] = groups;
728
for (const action of actions) {
729
if (action instanceof MenuItemAction) {
730
const title = typeof action.item.title === 'string'
731
? action.item.title
732
: action.item.title.mnemonicTitle ?? action.item.title.value;
733
webNavigationActions.push(toAction({
734
id: action.id, label: mnemonicMenuLabel(title), class: action.class, enabled: action.enabled, run: async (event?: unknown) => {
735
this.commandService.executeCommand(action.id, event);
736
}
737
}));
738
}
739
}
740
741
webNavigationActions.push(new Separator());
742
}
743
744
if (webNavigationActions.length) {
745
webNavigationActions.pop();
746
}
747
748
return webNavigationActions;
749
}
750
751
private getMenuBarOptions(): IMenuBarOptions {
752
return {
753
enableMnemonics: this.currentEnableMenuBarMnemonics,
754
disableAltFocus: this.currentDisableMenuBarAltFocus,
755
visibility: this.currentMenubarVisibility,
756
actionRunner: this.actionRunner,
757
getKeybinding: (action) => this.keybindingService.lookupKeybinding(action.id),
758
alwaysOnMnemonics: this.alwaysOnMnemonics,
759
compactMode: this.currentCompactMenuMode,
760
getCompactMenuActions: () => {
761
if (!isWeb) {
762
return []; // only for web
763
}
764
765
return this.getWebNavigationActions();
766
}
767
};
768
}
769
770
protected override onDidChangeWindowFocus(hasFocus: boolean): void {
771
if (!this.visible) {
772
return;
773
}
774
775
super.onDidChangeWindowFocus(hasFocus);
776
777
if (this.container) {
778
if (hasFocus) {
779
this.container.classList.remove('inactive');
780
} else {
781
this.container.classList.add('inactive');
782
this.menubar?.blur();
783
}
784
}
785
}
786
787
protected override onUpdateStateChange(): void {
788
if (!this.visible) {
789
return;
790
}
791
792
super.onUpdateStateChange();
793
}
794
795
protected override onDidChangeRecentlyOpened(): void {
796
if (!this.visible) {
797
return;
798
}
799
800
super.onDidChangeRecentlyOpened();
801
}
802
803
protected override onUpdateKeybindings(): void {
804
if (!this.visible) {
805
return;
806
}
807
808
super.onUpdateKeybindings();
809
}
810
811
protected override registerListeners(): void {
812
super.registerListeners();
813
814
this._register(addDisposableListener(mainWindow, EventType.RESIZE, () => {
815
if (this.menubar && !(isIOS && BrowserFeatures.pointerEvents)) {
816
this.menubar.blur();
817
}
818
}));
819
820
// Mnemonics require fullscreen in web
821
if (isWeb) {
822
this._register(onDidChangeFullscreen(windowId => {
823
if (windowId === mainWindow.vscodeWindowId) {
824
this.updateMenubar();
825
}
826
}));
827
this._register(this.webNavigationMenu.onDidChange(() => this.updateMenubar()));
828
this._register(enableFocusMenuBarAction().event(() => this.menubar?.toggleFocus()));
829
}
830
}
831
832
get onVisibilityChange(): Event<boolean> {
833
return this._onVisibilityChange.event;
834
}
835
836
get onFocusStateChange(): Event<boolean> {
837
return this._onFocusStateChange.event;
838
}
839
840
getMenubarItemsDimensions(): Dimension {
841
if (this.menubar) {
842
return new Dimension(this.menubar.getWidth(), this.menubar.getHeight());
843
}
844
845
return new Dimension(0, 0);
846
}
847
848
create(parent: HTMLElement): HTMLElement {
849
this.container = parent;
850
851
// Build the menubar
852
if (this.container) {
853
this.doUpdateMenubar(true);
854
}
855
856
return this.container;
857
}
858
859
layout(dimension: Dimension) {
860
this.menubar?.update(this.getMenuBarOptions());
861
}
862
863
toggleFocus() {
864
this.menubar?.toggleFocus();
865
}
866
}
867
868