Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/parts/statusbar/statusbarItem.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 { toErrorMessage } from '../../../../base/common/errorMessage.js';
7
import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
8
import { SimpleIconLabel } from '../../../../base/browser/ui/iconLabel/simpleIconLabel.js';
9
import { ICommandService } from '../../../../platform/commands/common/commands.js';
10
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
11
import { IStatusbarEntry, isTooltipWithCommands, ShowTooltipCommand, StatusbarEntryKinds, TooltipContent } from '../../../services/statusbar/browser/statusbar.js';
12
import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from '../../../../base/common/actions.js';
13
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
14
import { ThemeColor } from '../../../../base/common/themables.js';
15
import { isThemeColor } from '../../../../editor/common/editorCommon.js';
16
import { addDisposableListener, EventType, hide, show, append, EventHelper, $ } from '../../../../base/browser/dom.js';
17
import { INotificationService } from '../../../../platform/notification/common/notification.js';
18
import { assertReturnsDefined } from '../../../../base/common/types.js';
19
import { Command } from '../../../../editor/common/languages.js';
20
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
21
import { KeyCode } from '../../../../base/common/keyCodes.js';
22
import { renderIcon, renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
23
import { spinningLoading, syncing } from '../../../../platform/theme/common/iconRegistry.js';
24
import { isMarkdownString, markdownStringEqual } from '../../../../base/common/htmlContent.js';
25
import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js';
26
import { Gesture, EventType as TouchEventType } from '../../../../base/browser/touch.js';
27
import { IManagedHover, IManagedHoverOptions } from '../../../../base/browser/ui/hover/hover.js';
28
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
29
30
export class StatusbarEntryItem extends Disposable {
31
32
private readonly label: StatusBarCodiconLabel;
33
34
private entry: IStatusbarEntry | undefined = undefined;
35
36
private readonly foregroundListener = this._register(new MutableDisposable());
37
private readonly backgroundListener = this._register(new MutableDisposable());
38
39
private readonly commandMouseListener = this._register(new MutableDisposable());
40
private readonly commandTouchListener = this._register(new MutableDisposable());
41
private readonly commandKeyboardListener = this._register(new MutableDisposable());
42
43
private hover: IManagedHover | undefined = undefined;
44
45
readonly labelContainer: HTMLElement;
46
readonly beakContainer: HTMLElement;
47
48
get name(): string {
49
return assertReturnsDefined(this.entry).name;
50
}
51
52
get hasCommand(): boolean {
53
return typeof this.entry?.command !== 'undefined';
54
}
55
56
constructor(
57
private container: HTMLElement,
58
entry: IStatusbarEntry,
59
private readonly hoverDelegate: IHoverDelegate,
60
@ICommandService private readonly commandService: ICommandService,
61
@IHoverService private readonly hoverService: IHoverService,
62
@INotificationService private readonly notificationService: INotificationService,
63
@ITelemetryService private readonly telemetryService: ITelemetryService,
64
@IThemeService private readonly themeService: IThemeService
65
) {
66
super();
67
68
// Label Container
69
this.labelContainer = $('a.statusbar-item-label', {
70
role: 'button',
71
tabIndex: -1 // allows screen readers to read title, but still prevents tab focus.
72
});
73
this._register(Gesture.addTarget(this.labelContainer)); // enable touch
74
75
// Label (with support for progress)
76
this.label = this._register(new StatusBarCodiconLabel(this.labelContainer));
77
this.container.appendChild(this.labelContainer);
78
79
// Beak Container
80
this.beakContainer = $('.status-bar-item-beak-container');
81
this.container.appendChild(this.beakContainer);
82
83
if (entry.content) {
84
this.container.appendChild(entry.content);
85
}
86
87
this.update(entry);
88
}
89
90
update(entry: IStatusbarEntry): void {
91
92
// Update: Progress
93
this.label.showProgress = entry.showProgress ?? false;
94
95
// Update: Text
96
if (!this.entry || entry.text !== this.entry.text) {
97
this.label.text = entry.text;
98
99
if (entry.text) {
100
show(this.labelContainer);
101
} else {
102
hide(this.labelContainer);
103
}
104
}
105
106
// Update: ARIA label
107
//
108
// Set the aria label on both elements so screen readers would read
109
// the correct thing without duplication #96210
110
111
if (!this.entry || entry.ariaLabel !== this.entry.ariaLabel) {
112
this.container.setAttribute('aria-label', entry.ariaLabel);
113
this.labelContainer.setAttribute('aria-label', entry.ariaLabel);
114
}
115
116
if (!this.entry || entry.role !== this.entry.role) {
117
this.labelContainer.setAttribute('role', entry.role || 'button');
118
}
119
120
// Update: Hover
121
if (!this.entry || !this.isEqualTooltip(this.entry, entry)) {
122
let hoverOptions: IManagedHoverOptions | undefined;
123
let hoverTooltip: TooltipContent | undefined;
124
if (isTooltipWithCommands(entry.tooltip)) {
125
hoverTooltip = entry.tooltip.content;
126
hoverOptions = {
127
actions: entry.tooltip.commands.map(command => ({
128
commandId: command.id,
129
label: command.title,
130
run: () => this.executeCommand(command)
131
}))
132
};
133
} else {
134
hoverTooltip = entry.tooltip;
135
}
136
137
const hoverContents = isMarkdownString(hoverTooltip) ? { markdown: hoverTooltip, markdownNotSupportedFallback: undefined } : hoverTooltip;
138
if (this.hover) {
139
this.hover.update(hoverContents, hoverOptions);
140
} else {
141
this.hover = this._register(this.hoverService.setupManagedHover(this.hoverDelegate, this.container, hoverContents, hoverOptions));
142
}
143
}
144
145
// Update: Command
146
if (!this.entry || entry.command !== this.entry.command) {
147
this.commandMouseListener.clear();
148
this.commandTouchListener.clear();
149
this.commandKeyboardListener.clear();
150
151
const command = entry.command;
152
if (command && (command !== ShowTooltipCommand || this.hover) /* "Show Hover" is only valid when we have a hover */) {
153
this.commandMouseListener.value = addDisposableListener(this.labelContainer, EventType.CLICK, () => this.executeCommand(command));
154
this.commandTouchListener.value = addDisposableListener(this.labelContainer, TouchEventType.Tap, () => this.executeCommand(command));
155
this.commandKeyboardListener.value = addDisposableListener(this.labelContainer, EventType.KEY_DOWN, e => {
156
const event = new StandardKeyboardEvent(e);
157
if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) {
158
EventHelper.stop(e);
159
160
this.executeCommand(command);
161
} else if (event.equals(KeyCode.Escape) || event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.RightArrow)) {
162
EventHelper.stop(e);
163
164
this.hover?.hide();
165
}
166
});
167
168
this.labelContainer.classList.remove('disabled');
169
} else {
170
this.labelContainer.classList.add('disabled');
171
}
172
}
173
174
// Update: Beak
175
if (!this.entry || entry.showBeak !== this.entry.showBeak) {
176
if (entry.showBeak) {
177
this.container.classList.add('has-beak');
178
} else {
179
this.container.classList.remove('has-beak');
180
}
181
}
182
183
const hasBackgroundColor = !!entry.backgroundColor || (entry.kind && entry.kind !== 'standard');
184
185
// Update: Kind
186
if (!this.entry || entry.kind !== this.entry.kind) {
187
for (const kind of StatusbarEntryKinds) {
188
this.container.classList.remove(`${kind}-kind`);
189
}
190
191
if (entry.kind && entry.kind !== 'standard') {
192
this.container.classList.add(`${entry.kind}-kind`);
193
}
194
195
this.container.classList.toggle('has-background-color', hasBackgroundColor);
196
}
197
198
// Update: Foreground
199
if (!this.entry || entry.color !== this.entry.color) {
200
this.applyColor(this.labelContainer, entry.color);
201
}
202
203
// Update: Background
204
if (!this.entry || entry.backgroundColor !== this.entry.backgroundColor) {
205
this.container.classList.toggle('has-background-color', hasBackgroundColor);
206
this.applyColor(this.container, entry.backgroundColor, true);
207
}
208
209
// Remember for next round
210
this.entry = entry;
211
}
212
213
private isEqualTooltip({ tooltip }: IStatusbarEntry, { tooltip: otherTooltip }: IStatusbarEntry) {
214
if (tooltip === undefined) {
215
return otherTooltip === undefined;
216
}
217
218
if (isMarkdownString(tooltip)) {
219
return isMarkdownString(otherTooltip) && markdownStringEqual(tooltip, otherTooltip);
220
}
221
222
return tooltip === otherTooltip;
223
}
224
225
private async executeCommand(command: string | Command): Promise<void> {
226
227
// Custom command from us: Show tooltip
228
if (command === ShowTooltipCommand) {
229
this.hover?.show(true /* focus */);
230
}
231
232
// Any other command is going through command service
233
else {
234
const id = typeof command === 'string' ? command : command.id;
235
const args = typeof command === 'string' ? [] : command.arguments ?? [];
236
237
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id, from: 'status bar' });
238
try {
239
await this.commandService.executeCommand(id, ...args);
240
} catch (error) {
241
this.notificationService.error(toErrorMessage(error));
242
}
243
}
244
}
245
246
private applyColor(container: HTMLElement, color: string | ThemeColor | undefined, isBackground?: boolean): void {
247
let colorResult: string | undefined = undefined;
248
249
if (isBackground) {
250
this.backgroundListener.clear();
251
} else {
252
this.foregroundListener.clear();
253
}
254
255
if (color) {
256
if (isThemeColor(color)) {
257
colorResult = this.themeService.getColorTheme().getColor(color.id)?.toString();
258
259
const listener = this.themeService.onDidColorThemeChange(theme => {
260
const colorValue = theme.getColor(color.id)?.toString();
261
262
if (isBackground) {
263
container.style.backgroundColor = colorValue ?? '';
264
} else {
265
container.style.color = colorValue ?? '';
266
}
267
});
268
269
if (isBackground) {
270
this.backgroundListener.value = listener;
271
} else {
272
this.foregroundListener.value = listener;
273
}
274
} else {
275
colorResult = color;
276
}
277
}
278
279
if (isBackground) {
280
container.style.backgroundColor = colorResult ?? '';
281
} else {
282
container.style.color = colorResult ?? '';
283
}
284
}
285
}
286
287
class StatusBarCodiconLabel extends SimpleIconLabel {
288
289
private progressCodicon = renderIcon(syncing);
290
291
private currentText = '';
292
private currentShowProgress: boolean | 'loading' | 'syncing' = false;
293
294
constructor(
295
private readonly container: HTMLElement
296
) {
297
super(container);
298
}
299
300
set showProgress(showProgress: boolean | 'loading' | 'syncing') {
301
if (this.currentShowProgress !== showProgress) {
302
this.currentShowProgress = showProgress;
303
this.progressCodicon = renderIcon(showProgress === 'syncing' ? syncing : spinningLoading);
304
this.text = this.currentText;
305
}
306
}
307
308
override set text(text: string) {
309
310
// Progress: insert progress codicon as first element as needed
311
// but keep it stable so that the animation does not reset
312
if (this.currentShowProgress) {
313
314
// Append as needed
315
if (this.container.firstChild !== this.progressCodicon) {
316
this.container.appendChild(this.progressCodicon);
317
}
318
319
// Remove others
320
for (const node of Array.from(this.container.childNodes)) {
321
if (node !== this.progressCodicon) {
322
node.remove();
323
}
324
}
325
326
// If we have text to show, add a space to separate from progress
327
let textContent = text ?? '';
328
if (textContent) {
329
textContent = `\u00A0${textContent}`; // prepend non-breaking space
330
}
331
332
// Append new elements
333
append(this.container, ...renderLabelWithIcons(textContent));
334
}
335
336
// No Progress: no special handling
337
else {
338
super.text = text;
339
}
340
}
341
}
342
343