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
5334 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 { IMarkdownRendererService } from '../../../../platform/markdown/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 visible = false;
60
61
private actionBar: ActionBar | undefined;
62
private messageActionsContainer: HTMLElement | undefined;
63
private focusedActionIndex: number = -1;
64
65
constructor(
66
@IThemeService themeService: IThemeService,
67
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
68
@IStorageService storageService: IStorageService,
69
@IContextKeyService private readonly contextKeyService: IContextKeyService,
70
@IInstantiationService private readonly instantiationService: IInstantiationService,
71
@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,
72
) {
73
super(Parts.BANNER_PART, { hasTitle: false }, themeService, storageService, layoutService);
74
}
75
76
protected override createContentArea(parent: HTMLElement): HTMLElement {
77
this.element = parent;
78
this.element.tabIndex = 0;
79
80
// Restore focused action if needed
81
this._register(addDisposableListener(this.element, EventType.FOCUS, () => {
82
if (this.focusedActionIndex !== -1) {
83
this.focusActionLink();
84
}
85
}));
86
87
// Track focus
88
const scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.element));
89
BannerFocused.bindTo(scopedContextKeyService).set(true);
90
91
return this.element;
92
}
93
94
private close(item: IBannerItem): void {
95
// Hide banner
96
this.setVisibility(false);
97
98
// Remove from document
99
clearNode(this.element);
100
101
// Remember choice
102
if (typeof item.onClose === 'function') {
103
item.onClose();
104
}
105
106
this.item = undefined;
107
}
108
109
private focusActionLink(): void {
110
const length = this.item?.actions?.length ?? 0;
111
112
if (this.focusedActionIndex < length) {
113
const actionLink = this.messageActionsContainer?.children[this.focusedActionIndex];
114
if (isHTMLElement(actionLink)) {
115
this.actionBar?.setFocusable(false);
116
actionLink.focus();
117
}
118
} else {
119
this.actionBar?.focus(0);
120
}
121
}
122
123
private getAriaLabel(item: IBannerItem): string | undefined {
124
if (item.ariaLabel) {
125
return item.ariaLabel;
126
}
127
if (typeof item.message === 'string') {
128
return item.message;
129
}
130
131
return undefined;
132
}
133
134
private getBannerMessage(message: MarkdownString | string): HTMLElement {
135
if (typeof message === 'string') {
136
const element = $('span');
137
element.textContent = message;
138
return element;
139
}
140
141
return this.markdownRendererService.render(message).element;
142
}
143
144
private setVisibility(visible: boolean): void {
145
if (visible !== this.visible) {
146
this.visible = visible;
147
this.focusedActionIndex = -1;
148
149
this.layoutService.setPartHidden(!visible, Parts.BANNER_PART);
150
this._onDidChangeSize.fire(undefined);
151
}
152
}
153
154
focus(): void {
155
this.focusedActionIndex = -1;
156
this.element.focus();
157
}
158
159
focusNextAction(): void {
160
const length = this.item?.actions?.length ?? 0;
161
this.focusedActionIndex = this.focusedActionIndex < length ? this.focusedActionIndex + 1 : 0;
162
163
this.focusActionLink();
164
}
165
166
focusPreviousAction(): void {
167
const length = this.item?.actions?.length ?? 0;
168
this.focusedActionIndex = this.focusedActionIndex > 0 ? this.focusedActionIndex - 1 : length;
169
170
this.focusActionLink();
171
}
172
173
hide(id: string): void {
174
if (this.item?.id !== id) {
175
return;
176
}
177
178
this.setVisibility(false);
179
}
180
181
show(item: IBannerItem): void {
182
if (item.id === this.item?.id) {
183
this.setVisibility(true);
184
return;
185
}
186
187
// Clear previous item
188
clearNode(this.element);
189
190
// Banner aria label
191
const ariaLabel = this.getAriaLabel(item);
192
if (ariaLabel) {
193
this.element.setAttribute('aria-label', ariaLabel);
194
}
195
196
// Icon
197
const iconContainer = append(this.element, $('div.icon-container'));
198
iconContainer.setAttribute('aria-hidden', 'true');
199
200
if (ThemeIcon.isThemeIcon(item.icon)) {
201
iconContainer.appendChild($(`div${ThemeIcon.asCSSSelector(item.icon)}`));
202
} else {
203
iconContainer.classList.add('custom-icon');
204
205
if (URI.isUri(item.icon)) {
206
iconContainer.style.backgroundImage = asCSSUrl(item.icon);
207
}
208
}
209
210
// Message
211
const messageContainer = append(this.element, $('div.message-container'));
212
messageContainer.setAttribute('aria-hidden', 'true');
213
messageContainer.appendChild(this.getBannerMessage(item.message));
214
215
// Message Actions
216
this.messageActionsContainer = append(this.element, $('div.message-actions-container'));
217
if (item.actions) {
218
for (const action of item.actions) {
219
this._register(this.instantiationService.createInstance(Link, this.messageActionsContainer, { ...action, tabIndex: -1 }, {}));
220
}
221
}
222
223
// Action
224
const actionBarContainer = append(this.element, $('div.action-container'));
225
this.actionBar = this._register(new ActionBar(actionBarContainer));
226
const label = item.closeLabel ?? localize('closeBanner', "Close Banner");
227
const closeAction = this._register(new Action('banner.close', label, ThemeIcon.asClassName(widgetClose), true, () => this.close(item)));
228
this.actionBar.push(closeAction, { icon: true, label: false });
229
this.actionBar.setFocusable(false);
230
231
this.setVisibility(true);
232
this.item = item;
233
}
234
235
toJSON(): object {
236
return {
237
type: Parts.BANNER_PART
238
};
239
}
240
}
241
242
registerSingleton(IBannerService, BannerPart, InstantiationType.Eager);
243
244
245
// Keybindings
246
247
KeybindingsRegistry.registerCommandAndKeybindingRule({
248
id: 'workbench.banner.focusBanner',
249
weight: KeybindingWeight.WorkbenchContrib,
250
primary: KeyCode.Escape,
251
when: BannerFocused,
252
handler: (accessor: ServicesAccessor) => {
253
const bannerService = accessor.get(IBannerService);
254
bannerService.focus();
255
}
256
});
257
258
KeybindingsRegistry.registerCommandAndKeybindingRule({
259
id: 'workbench.banner.focusNextAction',
260
weight: KeybindingWeight.WorkbenchContrib,
261
primary: KeyCode.RightArrow,
262
secondary: [KeyCode.DownArrow],
263
when: BannerFocused,
264
handler: (accessor: ServicesAccessor) => {
265
const bannerService = accessor.get(IBannerService);
266
bannerService.focusNextAction();
267
}
268
});
269
270
KeybindingsRegistry.registerCommandAndKeybindingRule({
271
id: 'workbench.banner.focusPreviousAction',
272
weight: KeybindingWeight.WorkbenchContrib,
273
primary: KeyCode.LeftArrow,
274
secondary: [KeyCode.UpArrow],
275
when: BannerFocused,
276
handler: (accessor: ServicesAccessor) => {
277
const bannerService = accessor.get(IBannerService);
278
bannerService.focusPreviousAction();
279
}
280
});
281
282
283
// Actions
284
285
class FocusBannerAction extends Action2 {
286
287
static readonly ID = 'workbench.action.focusBanner';
288
static readonly LABEL = localize2('focusBanner', "Focus Banner");
289
290
constructor() {
291
super({
292
id: FocusBannerAction.ID,
293
title: FocusBannerAction.LABEL,
294
category: Categories.View,
295
f1: true
296
});
297
}
298
299
async run(accessor: ServicesAccessor): Promise<void> {
300
const layoutService = accessor.get(IWorkbenchLayoutService);
301
layoutService.focusPart(Parts.BANNER_PART);
302
}
303
}
304
305
registerAction2(FocusBannerAction);
306
307