Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/parts/banner/bannerPart.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/bannerpart.css';
7
import { localize, localize2 } from '../../../../nls.js';
8
import { $, addDisposableListener, append, clearNode, EventType, isHTMLElement } from '../../../../base/browser/dom.js';
9
import { asCSSUrl } from '../../../../base/browser/cssValue.js';
10
import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
11
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
12
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
13
import { IStorageService } from '../../../../platform/storage/common/storage.js';
14
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
15
import { ThemeIcon } from '../../../../base/common/themables.js';
16
import { Part } from '../../part.js';
17
import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js';
18
import { Action } from '../../../../base/common/actions.js';
19
import { Link } from '../../../../platform/opener/browser/link.js';
20
import { MarkdownString } from '../../../../base/common/htmlContent.js';
21
import { Emitter } from '../../../../base/common/event.js';
22
import { IBannerItem, IBannerService } from '../../../services/banner/browser/bannerService.js';
23
import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';
24
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
25
import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';
26
import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
27
import { KeyCode } from '../../../../base/common/keyCodes.js';
28
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
29
import { URI } from '../../../../base/common/uri.js';
30
import { widgetClose } from '../../../../platform/theme/common/iconRegistry.js';
31
import { BannerFocused } from '../../../common/contextkeys.js';
32
33
// Banner Part
34
35
export class BannerPart extends Part implements IBannerService {
36
37
declare readonly _serviceBrand: undefined;
38
39
// #region IView
40
41
readonly height: number = 26;
42
readonly minimumWidth: number = 0;
43
readonly maximumWidth: number = Number.POSITIVE_INFINITY;
44
45
get minimumHeight(): number {
46
return this.visible ? this.height : 0;
47
}
48
49
get maximumHeight(): number {
50
return this.visible ? this.height : 0;
51
}
52
53
private _onDidChangeSize = this._register(new Emitter<{ width: number; height: number } | undefined>());
54
override get onDidChange() { return this._onDidChangeSize.event; }
55
56
//#endregion
57
58
private item: IBannerItem | undefined;
59
private readonly markdownRenderer: MarkdownRenderer;
60
private visible = false;
61
62
private actionBar: ActionBar | undefined;
63
private messageActionsContainer: HTMLElement | undefined;
64
private focusedActionIndex: number = -1;
65
66
constructor(
67
@IThemeService themeService: IThemeService,
68
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
69
@IStorageService storageService: IStorageService,
70
@IContextKeyService private readonly contextKeyService: IContextKeyService,
71
@IInstantiationService private readonly instantiationService: IInstantiationService,
72
) {
73
super(Parts.BANNER_PART, { hasTitle: false }, themeService, storageService, layoutService);
74
75
this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {});
76
}
77
78
protected override createContentArea(parent: HTMLElement): HTMLElement {
79
this.element = parent;
80
this.element.tabIndex = 0;
81
82
// Restore focused action if needed
83
this._register(addDisposableListener(this.element, EventType.FOCUS, () => {
84
if (this.focusedActionIndex !== -1) {
85
this.focusActionLink();
86
}
87
}));
88
89
// Track focus
90
const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element));
91
BannerFocused.bindTo(scopedContextKeyService).set(true);
92
93
return this.element;
94
}
95
96
private close(item: IBannerItem): void {
97
// Hide banner
98
this.setVisibility(false);
99
100
// Remove from document
101
clearNode(this.element);
102
103
// Remember choice
104
if (typeof item.onClose === 'function') {
105
item.onClose();
106
}
107
108
this.item = undefined;
109
}
110
111
private focusActionLink(): void {
112
const length = this.item?.actions?.length ?? 0;
113
114
if (this.focusedActionIndex < length) {
115
const actionLink = this.messageActionsContainer?.children[this.focusedActionIndex];
116
if (isHTMLElement(actionLink)) {
117
this.actionBar?.setFocusable(false);
118
actionLink.focus();
119
}
120
} else {
121
this.actionBar?.focus(0);
122
}
123
}
124
125
private getAriaLabel(item: IBannerItem): string | undefined {
126
if (item.ariaLabel) {
127
return item.ariaLabel;
128
}
129
if (typeof item.message === 'string') {
130
return item.message;
131
}
132
133
return undefined;
134
}
135
136
private getBannerMessage(message: MarkdownString | string): HTMLElement {
137
if (typeof message === 'string') {
138
const element = $('span');
139
element.textContent = message;
140
return element;
141
}
142
143
return this.markdownRenderer.render(message).element;
144
}
145
146
private setVisibility(visible: boolean): void {
147
if (visible !== this.visible) {
148
this.visible = visible;
149
this.focusedActionIndex = -1;
150
151
this.layoutService.setPartHidden(!visible, Parts.BANNER_PART);
152
this._onDidChangeSize.fire(undefined);
153
}
154
}
155
156
focus(): void {
157
this.focusedActionIndex = -1;
158
this.element.focus();
159
}
160
161
focusNextAction(): void {
162
const length = this.item?.actions?.length ?? 0;
163
this.focusedActionIndex = this.focusedActionIndex < length ? this.focusedActionIndex + 1 : 0;
164
165
this.focusActionLink();
166
}
167
168
focusPreviousAction(): void {
169
const length = this.item?.actions?.length ?? 0;
170
this.focusedActionIndex = this.focusedActionIndex > 0 ? this.focusedActionIndex - 1 : length;
171
172
this.focusActionLink();
173
}
174
175
hide(id: string): void {
176
if (this.item?.id !== id) {
177
return;
178
}
179
180
this.setVisibility(false);
181
}
182
183
show(item: IBannerItem): void {
184
if (item.id === this.item?.id) {
185
this.setVisibility(true);
186
return;
187
}
188
189
// Clear previous item
190
clearNode(this.element);
191
192
// Banner aria label
193
const ariaLabel = this.getAriaLabel(item);
194
if (ariaLabel) {
195
this.element.setAttribute('aria-label', ariaLabel);
196
}
197
198
// Icon
199
const iconContainer = append(this.element, $('div.icon-container'));
200
iconContainer.setAttribute('aria-hidden', 'true');
201
202
if (ThemeIcon.isThemeIcon(item.icon)) {
203
iconContainer.appendChild($(`div${ThemeIcon.asCSSSelector(item.icon)}`));
204
} else {
205
iconContainer.classList.add('custom-icon');
206
207
if (URI.isUri(item.icon)) {
208
iconContainer.style.backgroundImage = asCSSUrl(item.icon);
209
}
210
}
211
212
// Message
213
const messageContainer = append(this.element, $('div.message-container'));
214
messageContainer.setAttribute('aria-hidden', 'true');
215
messageContainer.appendChild(this.getBannerMessage(item.message));
216
217
// Message Actions
218
this.messageActionsContainer = append(this.element, $('div.message-actions-container'));
219
if (item.actions) {
220
for (const action of item.actions) {
221
this._register(this.instantiationService.createInstance(Link, this.messageActionsContainer, { ...action, tabIndex: -1 }, {}));
222
}
223
}
224
225
// Action
226
const actionBarContainer = append(this.element, $('div.action-container'));
227
this.actionBar = this._register(new ActionBar(actionBarContainer));
228
const label = item.closeLabel ?? localize('closeBanner', "Close Banner");
229
const closeAction = this._register(new Action('banner.close', label, ThemeIcon.asClassName(widgetClose), true, () => this.close(item)));
230
this.actionBar.push(closeAction, { icon: true, label: false });
231
this.actionBar.setFocusable(false);
232
233
this.setVisibility(true);
234
this.item = item;
235
}
236
237
toJSON(): object {
238
return {
239
type: Parts.BANNER_PART
240
};
241
}
242
}
243
244
registerSingleton(IBannerService, BannerPart, InstantiationType.Eager);
245
246
247
// Keybindings
248
249
KeybindingsRegistry.registerCommandAndKeybindingRule({
250
id: 'workbench.banner.focusBanner',
251
weight: KeybindingWeight.WorkbenchContrib,
252
primary: KeyCode.Escape,
253
when: BannerFocused,
254
handler: (accessor: ServicesAccessor) => {
255
const bannerService = accessor.get(IBannerService);
256
bannerService.focus();
257
}
258
});
259
260
KeybindingsRegistry.registerCommandAndKeybindingRule({
261
id: 'workbench.banner.focusNextAction',
262
weight: KeybindingWeight.WorkbenchContrib,
263
primary: KeyCode.RightArrow,
264
secondary: [KeyCode.DownArrow],
265
when: BannerFocused,
266
handler: (accessor: ServicesAccessor) => {
267
const bannerService = accessor.get(IBannerService);
268
bannerService.focusNextAction();
269
}
270
});
271
272
KeybindingsRegistry.registerCommandAndKeybindingRule({
273
id: 'workbench.banner.focusPreviousAction',
274
weight: KeybindingWeight.WorkbenchContrib,
275
primary: KeyCode.LeftArrow,
276
secondary: [KeyCode.UpArrow],
277
when: BannerFocused,
278
handler: (accessor: ServicesAccessor) => {
279
const bannerService = accessor.get(IBannerService);
280
bannerService.focusPreviousAction();
281
}
282
});
283
284
285
// Actions
286
287
class FocusBannerAction extends Action2 {
288
289
static readonly ID = 'workbench.action.focusBanner';
290
static readonly LABEL = localize2('focusBanner', "Focus Banner");
291
292
constructor() {
293
super({
294
id: FocusBannerAction.ID,
295
title: FocusBannerAction.LABEL,
296
category: Categories.View,
297
f1: true
298
});
299
}
300
301
async run(accessor: ServicesAccessor): Promise<void> {
302
const layoutService = accessor.get(IWorkbenchLayoutService);
303
layoutService.focusPart(Parts.BANNER_PART);
304
}
305
}
306
307
registerAction2(FocusBannerAction);
308
309