Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/parts/statusbar/statusbarPart.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/statusbarpart.css';
7
import { localize } from '../../../../nls.js';
8
import { Disposable, DisposableStore, disposeIfDisposable, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
9
import { MultiWindowParts, Part } from '../../part.js';
10
import { EventType as TouchEventType, Gesture, GestureEvent } from '../../../../base/browser/touch.js';
11
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
12
import { StatusbarAlignment, IStatusbarService, IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarStyleOverride, isStatusbarEntryLocation, IStatusbarEntryLocation, isStatusbarEntryPriority, IStatusbarEntryPriority } from '../../../services/statusbar/browser/statusbar.js';
13
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
14
import { IAction, Separator, toAction } from '../../../../base/common/actions.js';
15
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
16
import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_BORDER, STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND, STATUS_BAR_ITEM_FOCUS_BORDER, STATUS_BAR_FOCUS_BORDER } from '../../../common/theme.js';
17
import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
18
import { contrastBorder, activeContrastBorder } from '../../../../platform/theme/common/colorRegistry.js';
19
import { EventHelper, addDisposableListener, EventType, clearNode, getWindow, isHTMLElement, $ } from '../../../../base/browser/dom.js';
20
import { createStyleSheet } from '../../../../base/browser/domStylesheets.js';
21
import { IStorageService } from '../../../../platform/storage/common/storage.js';
22
import { Parts, IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js';
23
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
24
import { equals } from '../../../../base/common/arrays.js';
25
import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';
26
import { ToggleStatusbarVisibilityAction } from '../../actions/layoutActions.js';
27
import { assertReturnsDefined } from '../../../../base/common/types.js';
28
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
29
import { isHighContrast } from '../../../../platform/theme/common/theme.js';
30
import { hash } from '../../../../base/common/hash.js';
31
import { WorkbenchHoverDelegate } from '../../../../platform/hover/browser/hover.js';
32
import { HideStatusbarEntryAction, ManageExtensionAction, ToggleStatusbarEntryVisibilityAction } from './statusbarActions.js';
33
import { IStatusbarViewModelEntry, StatusbarViewModel } from './statusbarModel.js';
34
import { StatusbarEntryItem } from './statusbarItem.js';
35
import { StatusBarFocused } from '../../../common/contextkeys.js';
36
import { Emitter, Event } from '../../../../base/common/event.js';
37
import { IView } from '../../../../base/browser/ui/grid/grid.js';
38
import { isManagedHoverTooltipHTMLElement, isManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js';
39
40
export interface IStatusbarEntryContainer extends IDisposable {
41
42
/**
43
* An event that is triggered when an entry's visibility is changed.
44
*/
45
readonly onDidChangeEntryVisibility: Event<{ id: string; visible: boolean }>;
46
47
/**
48
* Adds an entry to the statusbar with the given alignment and priority. Use the returned accessor
49
* to update or remove the statusbar entry.
50
*
51
* @param id identifier of the entry is needed to allow users to hide entries via settings
52
* @param alignment either LEFT or RIGHT side in the status bar
53
* @param priority items get arranged from highest priority to lowest priority from left to right
54
* in their respective alignment slot
55
*/
56
addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority?: number | IStatusbarEntryPriority): IStatusbarEntryAccessor;
57
addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority?: number | IStatusbarEntryPriority | IStatusbarEntryLocation): IStatusbarEntryAccessor;
58
59
/**
60
* Adds an entry to the statusbar with the given alignment relative to another entry. Use the returned
61
* accessor to update or remove the statusbar entry.
62
*
63
* @param id identifier of the entry is needed to allow users to hide entries via settings
64
* @param alignment either LEFT or RIGHT side in the status bar
65
* @param location a reference to another entry to position relative to
66
*/
67
addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, location?: IStatusbarEntryLocation): IStatusbarEntryAccessor;
68
69
/**
70
* Return if an entry is visible or not.
71
*/
72
isEntryVisible(id: string): boolean;
73
74
/**
75
* Allows to update an entry's visibility with the provided ID.
76
*/
77
updateEntryVisibility(id: string, visible: boolean): void;
78
79
/**
80
* Allows to override the appearance of an entry with the provided ID.
81
*/
82
overrideEntry(id: string, override: Partial<IStatusbarEntry>): IDisposable;
83
84
/**
85
* Focused the status bar. If one of the status bar entries was focused, focuses it directly.
86
*/
87
focus(preserveEntryFocus?: boolean): void;
88
89
/**
90
* Focuses the next status bar entry. If none focused, focuses the first.
91
*/
92
focusNextEntry(): void;
93
94
/**
95
* Focuses the previous status bar entry. If none focused, focuses the last.
96
*/
97
focusPreviousEntry(): void;
98
99
/**
100
* Returns true if a status bar entry is focused.
101
*/
102
isEntryFocused(): boolean;
103
104
/**
105
* Temporarily override statusbar style.
106
*/
107
overrideStyle(style: IStatusbarStyleOverride): IDisposable;
108
}
109
110
interface IPendingStatusbarEntry {
111
readonly id: string;
112
readonly alignment: StatusbarAlignment;
113
readonly priority: IStatusbarEntryPriority;
114
115
entry: IStatusbarEntry;
116
accessor?: IStatusbarEntryAccessor;
117
}
118
119
class StatusbarPart extends Part implements IStatusbarEntryContainer {
120
121
static readonly HEIGHT = 22;
122
123
//#region IView
124
125
readonly minimumWidth: number = 0;
126
readonly maximumWidth: number = Number.POSITIVE_INFINITY;
127
readonly minimumHeight: number = StatusbarPart.HEIGHT;
128
readonly maximumHeight: number = StatusbarPart.HEIGHT;
129
130
//#endregion
131
132
private styleElement: HTMLStyleElement | undefined;
133
134
private pendingEntries: IPendingStatusbarEntry[] = [];
135
136
private readonly viewModel: StatusbarViewModel;
137
138
readonly onDidChangeEntryVisibility: Event<{ id: string; visible: boolean }>;
139
140
private readonly _onWillDispose = this._register(new Emitter<void>());
141
readonly onWillDispose = this._onWillDispose.event;
142
143
private readonly onDidOverrideEntry = this._register(new Emitter<string>());
144
private readonly entryOverrides = new Map<string, Partial<IStatusbarEntry>>();
145
146
private leftItemsContainer: HTMLElement | undefined;
147
private rightItemsContainer: HTMLElement | undefined;
148
149
private readonly hoverDelegate: WorkbenchHoverDelegate;
150
151
private readonly compactEntriesDisposable = this._register(new MutableDisposable<DisposableStore>());
152
private readonly styleOverrides = new Set<IStatusbarStyleOverride>();
153
154
constructor(
155
id: string,
156
@IInstantiationService private readonly instantiationService: IInstantiationService,
157
@IThemeService themeService: IThemeService,
158
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
159
@IStorageService storageService: IStorageService,
160
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
161
@IContextMenuService private readonly contextMenuService: IContextMenuService,
162
@IContextKeyService private readonly contextKeyService: IContextKeyService,
163
) {
164
super(id, { hasTitle: false }, themeService, storageService, layoutService);
165
166
this.viewModel = this._register(new StatusbarViewModel(storageService));
167
this.onDidChangeEntryVisibility = this.viewModel.onDidChangeEntryVisibility;
168
169
this.hoverDelegate = this._register(this.instantiationService.createInstance(WorkbenchHoverDelegate, 'element', {
170
instantHover: true,
171
dynamicDelay(content) {
172
if (
173
typeof content === 'function' ||
174
isHTMLElement(content) ||
175
(isManagedHoverTooltipMarkdownString(content) && typeof content.markdown === 'function') ||
176
isManagedHoverTooltipHTMLElement(content)
177
) {
178
// override the delay for content that is rich (e.g. html or long running)
179
// so that it appears more instantly. these hovers carry more important
180
// information and should not be delayed by preference.
181
return 500;
182
}
183
184
return undefined;
185
}
186
}, (_, focus?: boolean) => (
187
{
188
persistence: {
189
hideOnKeyDown: true,
190
sticky: focus
191
},
192
appearance: {
193
maxHeightRatio: 0.9
194
}
195
}
196
)));
197
198
this.registerListeners();
199
}
200
201
private registerListeners(): void {
202
203
// Entry visibility changes
204
this._register(this.onDidChangeEntryVisibility(() => this.updateCompactEntries()));
205
206
// Workbench state changes
207
this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles()));
208
}
209
210
overrideEntry(id: string, override: Partial<IStatusbarEntry>): IDisposable {
211
this.entryOverrides.set(id, override);
212
this.onDidOverrideEntry.fire(id);
213
214
return toDisposable(() => {
215
const currentOverride = this.entryOverrides.get(id);
216
if (currentOverride === override) {
217
this.entryOverrides.delete(id);
218
this.onDidOverrideEntry.fire(id);
219
}
220
});
221
}
222
223
private withEntryOverride(entry: IStatusbarEntry, id: string): IStatusbarEntry {
224
const override = this.entryOverrides.get(id);
225
if (override) {
226
entry = { ...entry, ...override };
227
}
228
229
return entry;
230
}
231
232
addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priorityOrLocation: number | IStatusbarEntryLocation | IStatusbarEntryPriority = 0): IStatusbarEntryAccessor {
233
let priority: IStatusbarEntryPriority;
234
if (isStatusbarEntryPriority(priorityOrLocation)) {
235
priority = priorityOrLocation;
236
} else {
237
priority = {
238
primary: priorityOrLocation,
239
secondary: hash(id) // derive from identifier to accomplish uniqueness
240
};
241
}
242
243
// As long as we have not been created into a container yet, record all entries
244
// that are pending so that they can get created at a later point
245
if (!this.element) {
246
return this.doAddPendingEntry(entry, id, alignment, priority);
247
}
248
249
// Otherwise add to view
250
return this.doAddEntry(entry, id, alignment, priority);
251
}
252
253
private doAddPendingEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority: IStatusbarEntryPriority): IStatusbarEntryAccessor {
254
const pendingEntry: IPendingStatusbarEntry = { entry, id, alignment, priority };
255
this.pendingEntries.push(pendingEntry);
256
257
const accessor: IStatusbarEntryAccessor = {
258
update: (entry: IStatusbarEntry) => {
259
if (pendingEntry.accessor) {
260
pendingEntry.accessor.update(entry);
261
} else {
262
pendingEntry.entry = entry;
263
}
264
},
265
266
dispose: () => {
267
if (pendingEntry.accessor) {
268
pendingEntry.accessor.dispose();
269
} else {
270
this.pendingEntries = this.pendingEntries.filter(entry => entry !== pendingEntry);
271
}
272
}
273
};
274
275
return accessor;
276
}
277
278
private doAddEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priority: IStatusbarEntryPriority): IStatusbarEntryAccessor {
279
const disposables = new DisposableStore();
280
281
// View model item
282
const itemContainer = this.doCreateStatusItem(id, alignment);
283
const item = disposables.add(this.instantiationService.createInstance(StatusbarEntryItem, itemContainer, this.withEntryOverride(entry, id), this.hoverDelegate));
284
285
// View model entry
286
const viewModelEntry: IStatusbarViewModelEntry = new class implements IStatusbarViewModelEntry {
287
readonly id = id;
288
readonly extensionId = entry.extensionId;
289
readonly alignment = alignment;
290
readonly priority = priority;
291
readonly container = itemContainer;
292
readonly labelContainer = item.labelContainer;
293
294
get name() { return item.name; }
295
get hasCommand() { return item.hasCommand; }
296
};
297
298
// Add to view model
299
const { needsFullRefresh } = this.doAddOrRemoveModelEntry(viewModelEntry, true);
300
if (needsFullRefresh) {
301
this.appendStatusbarEntries();
302
} else {
303
this.appendStatusbarEntry(viewModelEntry);
304
}
305
306
let lastEntry = entry;
307
const accessor: IStatusbarEntryAccessor = {
308
update: entry => {
309
lastEntry = entry;
310
item.update(this.withEntryOverride(entry, id));
311
},
312
dispose: () => {
313
const { needsFullRefresh } = this.doAddOrRemoveModelEntry(viewModelEntry, false);
314
if (needsFullRefresh) {
315
this.appendStatusbarEntries();
316
} else {
317
itemContainer.remove();
318
this.updateCompactEntries();
319
}
320
disposables.dispose();
321
}
322
};
323
324
// React to overrides
325
disposables.add(this.onDidOverrideEntry.event(overrideEntryId => {
326
if (overrideEntryId === id) {
327
accessor.update(lastEntry);
328
}
329
}));
330
331
return accessor;
332
}
333
334
private doCreateStatusItem(id: string, alignment: StatusbarAlignment, ...extraClasses: string[]): HTMLElement {
335
const itemContainer = $('.statusbar-item', { id });
336
337
if (extraClasses) {
338
itemContainer.classList.add(...extraClasses);
339
}
340
341
if (alignment === StatusbarAlignment.RIGHT) {
342
itemContainer.classList.add('right');
343
} else {
344
itemContainer.classList.add('left');
345
}
346
347
return itemContainer;
348
}
349
350
private doAddOrRemoveModelEntry(entry: IStatusbarViewModelEntry, add: boolean) {
351
352
// Update model but remember previous entries
353
const entriesBefore = this.viewModel.entries;
354
if (add) {
355
this.viewModel.add(entry);
356
} else {
357
this.viewModel.remove(entry);
358
}
359
const entriesAfter = this.viewModel.entries;
360
361
// Apply operation onto the entries from before
362
if (add) {
363
entriesBefore.splice(entriesAfter.indexOf(entry), 0, entry);
364
} else {
365
entriesBefore.splice(entriesBefore.indexOf(entry), 1);
366
}
367
368
// Figure out if a full refresh is needed by comparing arrays
369
const needsFullRefresh = !equals(entriesBefore, entriesAfter);
370
371
return { needsFullRefresh };
372
}
373
374
isEntryVisible(id: string): boolean {
375
return !this.viewModel.isHidden(id);
376
}
377
378
updateEntryVisibility(id: string, visible: boolean): void {
379
if (visible) {
380
this.viewModel.show(id);
381
} else {
382
this.viewModel.hide(id);
383
}
384
}
385
386
focusNextEntry(): void {
387
this.viewModel.focusNextEntry();
388
}
389
390
focusPreviousEntry(): void {
391
this.viewModel.focusPreviousEntry();
392
}
393
394
isEntryFocused(): boolean {
395
return this.viewModel.isEntryFocused();
396
}
397
398
focus(preserveEntryFocus = true): void {
399
this.getContainer()?.focus();
400
const lastFocusedEntry = this.viewModel.lastFocusedEntry;
401
if (preserveEntryFocus && lastFocusedEntry) {
402
setTimeout(() => lastFocusedEntry.labelContainer.focus(), 0); // Need a timeout, for some reason without it the inner label container will not get focused
403
}
404
}
405
406
protected override createContentArea(parent: HTMLElement): HTMLElement {
407
this.element = parent;
408
409
// Track focus within container
410
const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element));
411
StatusBarFocused.bindTo(scopedContextKeyService).set(true);
412
413
// Left items container
414
this.leftItemsContainer = $('.left-items.items-container');
415
this.element.appendChild(this.leftItemsContainer);
416
this.element.tabIndex = 0;
417
418
// Right items container
419
this.rightItemsContainer = $('.right-items.items-container');
420
this.element.appendChild(this.rightItemsContainer);
421
422
// Context menu support
423
this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(e)));
424
this._register(Gesture.addTarget(parent));
425
this._register(addDisposableListener(parent, TouchEventType.Contextmenu, e => this.showContextMenu(e)));
426
427
// Initial status bar entries
428
this.createInitialStatusbarEntries();
429
430
return this.element;
431
}
432
433
private createInitialStatusbarEntries(): void {
434
435
// Add items in order according to alignment
436
this.appendStatusbarEntries();
437
438
// Fill in pending entries if any
439
while (this.pendingEntries.length) {
440
const pending = this.pendingEntries.shift();
441
if (pending) {
442
pending.accessor = this.addEntry(pending.entry, pending.id, pending.alignment, pending.priority.primary);
443
}
444
}
445
}
446
447
private appendStatusbarEntries(): void {
448
const leftItemsContainer = assertReturnsDefined(this.leftItemsContainer);
449
const rightItemsContainer = assertReturnsDefined(this.rightItemsContainer);
450
451
// Clear containers
452
clearNode(leftItemsContainer);
453
clearNode(rightItemsContainer);
454
455
// Append all
456
for (const entry of [
457
...this.viewModel.getEntries(StatusbarAlignment.LEFT),
458
...this.viewModel.getEntries(StatusbarAlignment.RIGHT).reverse() // reversing due to flex: row-reverse
459
]) {
460
const target = entry.alignment === StatusbarAlignment.LEFT ? leftItemsContainer : rightItemsContainer;
461
462
target.appendChild(entry.container);
463
}
464
465
// Update compact entries
466
this.updateCompactEntries();
467
}
468
469
private appendStatusbarEntry(entry: IStatusbarViewModelEntry): void {
470
const entries = this.viewModel.getEntries(entry.alignment);
471
472
if (entry.alignment === StatusbarAlignment.RIGHT) {
473
entries.reverse(); // reversing due to flex: row-reverse
474
}
475
476
const target = assertReturnsDefined(entry.alignment === StatusbarAlignment.LEFT ? this.leftItemsContainer : this.rightItemsContainer);
477
478
const index = entries.indexOf(entry);
479
if (index + 1 === entries.length) {
480
target.appendChild(entry.container); // append at the end if last
481
} else {
482
target.insertBefore(entry.container, entries[index + 1].container); // insert before next element otherwise
483
}
484
485
// Update compact entries
486
this.updateCompactEntries();
487
}
488
489
private updateCompactEntries(): void {
490
const entries = this.viewModel.entries;
491
492
// Find visible entries and clear compact related CSS classes if any
493
const mapIdToVisibleEntry = new Map<string, IStatusbarViewModelEntry>();
494
for (const entry of entries) {
495
if (!this.viewModel.isHidden(entry.id)) {
496
mapIdToVisibleEntry.set(entry.id, entry);
497
}
498
499
entry.container.classList.remove('compact-left', 'compact-right');
500
}
501
502
// Figure out groups of entries with `compact` alignment
503
const compactEntryGroups = new Map<string, Map<string, IStatusbarViewModelEntry>>();
504
for (const entry of mapIdToVisibleEntry.values()) {
505
if (
506
isStatusbarEntryLocation(entry.priority.primary) && // entry references another entry as location
507
entry.priority.primary.compact // entry wants to be compact
508
) {
509
const locationId = entry.priority.primary.location.id;
510
const location = mapIdToVisibleEntry.get(locationId);
511
if (!location) {
512
continue; // skip if location does not exist
513
}
514
515
// Build a map of entries that are compact among each other
516
let compactEntryGroup = compactEntryGroups.get(locationId);
517
if (!compactEntryGroup) {
518
519
// It is possible that this entry references another entry
520
// that itself references an entry. In that case, we want
521
// to add it to the entries of the referenced entry.
522
523
for (const group of compactEntryGroups.values()) {
524
if (group.has(locationId)) {
525
compactEntryGroup = group;
526
break;
527
}
528
}
529
530
if (!compactEntryGroup) {
531
compactEntryGroup = new Map<string, IStatusbarViewModelEntry>();
532
compactEntryGroups.set(locationId, compactEntryGroup);
533
}
534
}
535
compactEntryGroup.set(entry.id, entry);
536
compactEntryGroup.set(location.id, location);
537
538
// Adjust CSS classes to move compact items closer together
539
if (entry.priority.primary.alignment === StatusbarAlignment.LEFT) {
540
location.container.classList.add('compact-left');
541
entry.container.classList.add('compact-right');
542
} else {
543
location.container.classList.add('compact-right');
544
entry.container.classList.add('compact-left');
545
}
546
}
547
}
548
549
// Install mouse listeners to update hover feedback for
550
// all compact entries that belong to each other
551
const statusBarItemHoverBackground = this.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND);
552
const statusBarItemCompactHoverBackground = this.getColor(STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND);
553
this.compactEntriesDisposable.value = new DisposableStore();
554
if (statusBarItemHoverBackground && statusBarItemCompactHoverBackground && !isHighContrast(this.theme.type)) {
555
for (const [, compactEntryGroup] of compactEntryGroups) {
556
for (const compactEntry of compactEntryGroup.values()) {
557
if (!compactEntry.hasCommand) {
558
continue; // only show hover feedback when we have a command
559
}
560
561
this.compactEntriesDisposable.value.add(addDisposableListener(compactEntry.labelContainer, EventType.MOUSE_OVER, () => {
562
compactEntryGroup.forEach(compactEntry => compactEntry.labelContainer.style.backgroundColor = statusBarItemHoverBackground);
563
compactEntry.labelContainer.style.backgroundColor = statusBarItemCompactHoverBackground;
564
}));
565
566
this.compactEntriesDisposable.value.add(addDisposableListener(compactEntry.labelContainer, EventType.MOUSE_OUT, () => {
567
compactEntryGroup.forEach(compactEntry => compactEntry.labelContainer.style.backgroundColor = '');
568
}));
569
}
570
}
571
}
572
}
573
574
private showContextMenu(e: MouseEvent | GestureEvent): void {
575
EventHelper.stop(e, true);
576
577
const event = new StandardMouseEvent(getWindow(this.element), e);
578
579
let actions: IAction[] | undefined = undefined;
580
this.contextMenuService.showContextMenu({
581
getAnchor: () => event,
582
getActions: () => {
583
actions = this.getContextMenuActions(event);
584
585
return actions;
586
},
587
onHide: () => {
588
if (actions) {
589
disposeIfDisposable(actions);
590
}
591
}
592
});
593
}
594
595
private getContextMenuActions(event: StandardMouseEvent): IAction[] {
596
const actions: IAction[] = [];
597
598
// Provide an action to hide the status bar at last
599
actions.push(toAction({ id: ToggleStatusbarVisibilityAction.ID, label: localize('hideStatusBar', "Hide Status Bar"), run: () => this.instantiationService.invokeFunction(accessor => new ToggleStatusbarVisibilityAction().run(accessor)) }));
600
actions.push(new Separator());
601
602
// Show an entry per known status entry
603
// Note: even though entries have an identifier, there can be multiple entries
604
// having the same identifier (e.g. from extensions). So we make sure to only
605
// show a single entry per identifier we handled.
606
const handledEntries = new Set<string>();
607
for (const entry of this.viewModel.entries) {
608
if (!handledEntries.has(entry.id)) {
609
actions.push(new ToggleStatusbarEntryVisibilityAction(entry.id, entry.name, this.viewModel));
610
handledEntries.add(entry.id);
611
}
612
}
613
614
// Figure out if mouse is over an entry
615
let statusEntryUnderMouse: IStatusbarViewModelEntry | undefined = undefined;
616
for (let element: HTMLElement | null = event.target; element; element = element.parentElement) {
617
const entry = this.viewModel.findEntry(element);
618
if (entry) {
619
statusEntryUnderMouse = entry;
620
break;
621
}
622
}
623
624
if (statusEntryUnderMouse) {
625
actions.push(new Separator());
626
if (statusEntryUnderMouse.extensionId) {
627
actions.push(this.instantiationService.createInstance(ManageExtensionAction, statusEntryUnderMouse.extensionId));
628
}
629
actions.push(new HideStatusbarEntryAction(statusEntryUnderMouse.id, statusEntryUnderMouse.name, this.viewModel));
630
}
631
632
return actions;
633
}
634
635
override updateStyles(): void {
636
super.updateStyles();
637
638
const container = assertReturnsDefined(this.getContainer());
639
const styleOverride: IStatusbarStyleOverride | undefined = [...this.styleOverrides].sort((a, b) => a.priority - b.priority)[0];
640
641
// Background / foreground colors
642
const backgroundColor = this.getColor(styleOverride?.background ?? (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_BACKGROUND : STATUS_BAR_NO_FOLDER_BACKGROUND)) || '';
643
container.style.backgroundColor = backgroundColor;
644
const foregroundColor = this.getColor(styleOverride?.foreground ?? (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND)) || '';
645
container.style.color = foregroundColor;
646
const itemBorderColor = this.getColor(STATUS_BAR_ITEM_FOCUS_BORDER);
647
648
// Border color
649
const borderColor = this.getColor(styleOverride?.border ?? (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_BORDER : STATUS_BAR_NO_FOLDER_BORDER)) || this.getColor(contrastBorder);
650
if (borderColor) {
651
container.classList.add('status-border-top');
652
container.style.setProperty('--status-border-top-color', borderColor);
653
} else {
654
container.classList.remove('status-border-top');
655
container.style.removeProperty('--status-border-top-color');
656
}
657
658
// Colors and focus outlines via dynamic stylesheet
659
660
const statusBarFocusColor = this.getColor(STATUS_BAR_FOCUS_BORDER);
661
662
if (!this.styleElement) {
663
this.styleElement = createStyleSheet(container);
664
}
665
666
this.styleElement.textContent = `
667
668
/* Status bar focus outline */
669
.monaco-workbench .part.statusbar:focus {
670
outline-color: ${statusBarFocusColor};
671
}
672
673
/* Status bar item focus outline */
674
.monaco-workbench .part.statusbar > .items-container > .statusbar-item a:focus-visible {
675
outline: 1px solid ${this.getColor(activeContrastBorder) ?? itemBorderColor};
676
outline-offset: ${borderColor ? '-2px' : '-1px'};
677
}
678
679
/* Notification Beak */
680
.monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-beak > .status-bar-item-beak-container:before {
681
border-bottom-color: ${borderColor ?? backgroundColor};
682
}
683
`;
684
}
685
686
override layout(width: number, height: number, top: number, left: number): void {
687
super.layout(width, height, top, left);
688
super.layoutContents(width, height);
689
}
690
691
overrideStyle(style: IStatusbarStyleOverride): IDisposable {
692
this.styleOverrides.add(style);
693
this.updateStyles();
694
695
return toDisposable(() => {
696
this.styleOverrides.delete(style);
697
this.updateStyles();
698
});
699
}
700
701
toJSON(): object {
702
return {
703
type: Parts.STATUSBAR_PART
704
};
705
}
706
707
override dispose(): void {
708
this._onWillDispose.fire();
709
710
super.dispose();
711
}
712
}
713
714
export class MainStatusbarPart extends StatusbarPart {
715
716
constructor(
717
@IInstantiationService instantiationService: IInstantiationService,
718
@IThemeService themeService: IThemeService,
719
@IWorkspaceContextService contextService: IWorkspaceContextService,
720
@IStorageService storageService: IStorageService,
721
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
722
@IContextMenuService contextMenuService: IContextMenuService,
723
@IContextKeyService contextKeyService: IContextKeyService,
724
) {
725
super(Parts.STATUSBAR_PART, instantiationService, themeService, contextService, storageService, layoutService, contextMenuService, contextKeyService);
726
}
727
}
728
729
export interface IAuxiliaryStatusbarPart extends IStatusbarEntryContainer, IView {
730
readonly container: HTMLElement;
731
readonly height: number;
732
}
733
734
export class AuxiliaryStatusbarPart extends StatusbarPart implements IAuxiliaryStatusbarPart {
735
736
private static COUNTER = 1;
737
738
readonly height = StatusbarPart.HEIGHT;
739
740
constructor(
741
readonly container: HTMLElement,
742
@IInstantiationService instantiationService: IInstantiationService,
743
@IThemeService themeService: IThemeService,
744
@IWorkspaceContextService contextService: IWorkspaceContextService,
745
@IStorageService storageService: IStorageService,
746
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
747
@IContextMenuService contextMenuService: IContextMenuService,
748
@IContextKeyService contextKeyService: IContextKeyService,
749
) {
750
const id = AuxiliaryStatusbarPart.COUNTER++;
751
super(`workbench.parts.auxiliaryStatus.${id}`, instantiationService, themeService, contextService, storageService, layoutService, contextMenuService, contextKeyService);
752
}
753
}
754
755
export class StatusbarService extends MultiWindowParts<StatusbarPart> implements IStatusbarService {
756
757
declare readonly _serviceBrand: undefined;
758
759
readonly mainPart: MainStatusbarPart;
760
761
private readonly _onDidCreateAuxiliaryStatusbarPart = this._register(new Emitter<AuxiliaryStatusbarPart>());
762
private readonly onDidCreateAuxiliaryStatusbarPart = this._onDidCreateAuxiliaryStatusbarPart.event;
763
764
constructor(
765
@IInstantiationService private readonly instantiationService: IInstantiationService,
766
@IStorageService storageService: IStorageService,
767
@IThemeService themeService: IThemeService
768
) {
769
super('workbench.statusBarService', themeService, storageService);
770
771
this.mainPart = this._register(this.instantiationService.createInstance(MainStatusbarPart));
772
this._register(this.registerPart(this.mainPart));
773
774
this.onDidChangeEntryVisibility = this.mainPart.onDidChangeEntryVisibility;
775
}
776
777
//#region Auxiliary Statusbar Parts
778
779
createAuxiliaryStatusbarPart(container: HTMLElement, instantiationService: IInstantiationService): IAuxiliaryStatusbarPart {
780
781
// Container
782
const statusbarPartContainer = $('footer.part.statusbar', {
783
'role': 'status',
784
'aria-live': 'off',
785
'tabIndex': '0'
786
});
787
statusbarPartContainer.style.position = 'relative';
788
container.appendChild(statusbarPartContainer);
789
790
// Statusbar Part
791
const statusbarPart = instantiationService.createInstance(AuxiliaryStatusbarPart, statusbarPartContainer);
792
const disposable = this.registerPart(statusbarPart);
793
794
statusbarPart.create(statusbarPartContainer);
795
796
Event.once(statusbarPart.onWillDispose)(() => disposable.dispose());
797
798
// Emit internal event
799
this._onDidCreateAuxiliaryStatusbarPart.fire(statusbarPart);
800
801
return statusbarPart;
802
}
803
804
createScoped(statusbarEntryContainer: IStatusbarEntryContainer, disposables: DisposableStore): IStatusbarService {
805
return disposables.add(this.instantiationService.createInstance(ScopedStatusbarService, statusbarEntryContainer));
806
}
807
808
//#endregion
809
810
//#region Service Implementation
811
812
readonly onDidChangeEntryVisibility: Event<{ id: string; visible: boolean }>;
813
814
addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priorityOrLocation: number | IStatusbarEntryLocation | IStatusbarEntryPriority = 0): IStatusbarEntryAccessor {
815
if (entry.showInAllWindows) {
816
return this.doAddEntryToAllWindows(entry, id, alignment, priorityOrLocation);
817
}
818
819
return this.mainPart.addEntry(entry, id, alignment, priorityOrLocation);
820
}
821
822
private doAddEntryToAllWindows(originalEntry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priorityOrLocation: number | IStatusbarEntryLocation | IStatusbarEntryPriority = 0): IStatusbarEntryAccessor {
823
const entryDisposables = new DisposableStore();
824
825
const accessors = new Set<IStatusbarEntryAccessor>();
826
827
let entry = originalEntry;
828
function addEntry(part: StatusbarPart | AuxiliaryStatusbarPart): void {
829
const partDisposables = new DisposableStore();
830
partDisposables.add(part.onWillDispose(() => partDisposables.dispose()));
831
832
const accessor = partDisposables.add(part.addEntry(entry, id, alignment, priorityOrLocation));
833
accessors.add(accessor);
834
partDisposables.add(toDisposable(() => accessors.delete(accessor)));
835
836
entryDisposables.add(partDisposables);
837
partDisposables.add(toDisposable(() => entryDisposables.delete(partDisposables)));
838
}
839
840
for (const part of this.parts) {
841
addEntry(part);
842
}
843
844
entryDisposables.add(this.onDidCreateAuxiliaryStatusbarPart(part => addEntry(part)));
845
846
return {
847
update: (updatedEntry: IStatusbarEntry) => {
848
entry = updatedEntry;
849
850
for (const update of accessors) {
851
update.update(updatedEntry);
852
}
853
},
854
dispose: () => entryDisposables.dispose()
855
};
856
}
857
858
isEntryVisible(id: string): boolean {
859
return this.mainPart.isEntryVisible(id);
860
}
861
862
updateEntryVisibility(id: string, visible: boolean): void {
863
for (const part of this.parts) {
864
part.updateEntryVisibility(id, visible);
865
}
866
}
867
868
overrideEntry(id: string, override: Partial<IStatusbarEntry>): IDisposable {
869
const disposables = new DisposableStore();
870
871
for (const part of this.parts) {
872
disposables.add(part.overrideEntry(id, override));
873
}
874
875
return disposables;
876
}
877
878
focus(preserveEntryFocus?: boolean): void {
879
this.activePart.focus(preserveEntryFocus);
880
}
881
882
focusNextEntry(): void {
883
this.activePart.focusNextEntry();
884
}
885
886
focusPreviousEntry(): void {
887
this.activePart.focusPreviousEntry();
888
}
889
890
isEntryFocused(): boolean {
891
return this.activePart.isEntryFocused();
892
}
893
894
overrideStyle(style: IStatusbarStyleOverride): IDisposable {
895
const disposables = new DisposableStore();
896
897
for (const part of this.parts) {
898
disposables.add(part.overrideStyle(style));
899
}
900
901
return disposables;
902
}
903
904
//#endregion
905
}
906
907
export class ScopedStatusbarService extends Disposable implements IStatusbarService {
908
909
declare readonly _serviceBrand: undefined;
910
911
constructor(
912
private readonly statusbarEntryContainer: IStatusbarEntryContainer,
913
@IStatusbarService private readonly statusbarService: IStatusbarService
914
) {
915
super();
916
917
this.onDidChangeEntryVisibility = this.statusbarEntryContainer.onDidChangeEntryVisibility;
918
}
919
920
createAuxiliaryStatusbarPart(container: HTMLElement, instantiationService: IInstantiationService): IAuxiliaryStatusbarPart {
921
return this.statusbarService.createAuxiliaryStatusbarPart(container, instantiationService);
922
}
923
924
createScoped(statusbarEntryContainer: IStatusbarEntryContainer, disposables: DisposableStore): IStatusbarService {
925
return this.statusbarService.createScoped(statusbarEntryContainer, disposables);
926
}
927
928
getPart(): IStatusbarEntryContainer {
929
return this.statusbarEntryContainer;
930
}
931
932
readonly onDidChangeEntryVisibility: Event<{ id: string; visible: boolean }>;
933
934
addEntry(entry: IStatusbarEntry, id: string, alignment: StatusbarAlignment, priorityOrLocation: number | IStatusbarEntryLocation | IStatusbarEntryPriority = 0): IStatusbarEntryAccessor {
935
return this.statusbarEntryContainer.addEntry(entry, id, alignment, priorityOrLocation);
936
}
937
938
isEntryVisible(id: string): boolean {
939
return this.statusbarEntryContainer.isEntryVisible(id);
940
}
941
942
updateEntryVisibility(id: string, visible: boolean): void {
943
this.statusbarEntryContainer.updateEntryVisibility(id, visible);
944
}
945
946
overrideEntry(id: string, override: Partial<IStatusbarEntry>): IDisposable {
947
return this.statusbarEntryContainer.overrideEntry(id, override);
948
}
949
950
focus(preserveEntryFocus?: boolean): void {
951
this.statusbarEntryContainer.focus(preserveEntryFocus);
952
}
953
954
focusNextEntry(): void {
955
this.statusbarEntryContainer.focusNextEntry();
956
}
957
958
focusPreviousEntry(): void {
959
this.statusbarEntryContainer.focusPreviousEntry();
960
}
961
962
isEntryFocused(): boolean {
963
return this.statusbarEntryContainer.isEntryFocused();
964
}
965
966
overrideStyle(style: IStatusbarStyleOverride): IDisposable {
967
return this.statusbarEntryContainer.overrideStyle(style);
968
}
969
}
970
971
registerSingleton(IStatusbarService, StatusbarService, InstantiationType.Eager);
972
973