Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/parts/notifications/notificationsCenter.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/notificationsCenter.css';
7
import './media/notificationsActions.css';
8
import { NOTIFICATIONS_CENTER_HEADER_FOREGROUND, NOTIFICATIONS_CENTER_HEADER_BACKGROUND, NOTIFICATIONS_CENTER_BORDER } from '../../../common/theme.js';
9
import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js';
10
import { INotificationsModel, INotificationChangeEvent, NotificationChangeType, NotificationViewItemContentChangeKind } from '../../../common/notifications.js';
11
import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js';
12
import { Emitter } from '../../../../base/common/event.js';
13
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
14
import { INotificationsCenterController, NotificationActionRunner } from './notificationsCommands.js';
15
import { NotificationsList } from './notificationsList.js';
16
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
17
import { $, Dimension, isAncestorOfActiveElement } from '../../../../base/browser/dom.js';
18
import { widgetShadow } from '../../../../platform/theme/common/colorRegistry.js';
19
import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
20
import { localize } from '../../../../nls.js';
21
import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
22
import { ClearAllNotificationsAction, ConfigureDoNotDisturbAction, ToggleDoNotDisturbBySourceAction, HideNotificationsCenterAction, ToggleDoNotDisturbAction } from './notificationsActions.js';
23
import { IAction, Separator, toAction } from '../../../../base/common/actions.js';
24
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
25
import { assertReturnsAllDefined, assertReturnsDefined } from '../../../../base/common/types.js';
26
import { NotificationsCenterVisibleContext } from '../../../common/contextkeys.js';
27
import { INotificationService, NotificationsFilter } from '../../../../platform/notification/common/notification.js';
28
import { mainWindow } from '../../../../base/browser/window.js';
29
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
30
import { DropdownMenuActionViewItem } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js';
31
import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
32
33
export class NotificationsCenter extends Themable implements INotificationsCenterController {
34
35
private static readonly MAX_DIMENSIONS = new Dimension(450, 400);
36
37
private static readonly MAX_NOTIFICATION_SOURCES = 10; // maximum number of notification sources to show in configure dropdown
38
39
private readonly _onDidChangeVisibility = this._register(new Emitter<void>());
40
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
41
42
private notificationsCenterContainer: HTMLElement | undefined;
43
private notificationsCenterHeader: HTMLElement | undefined;
44
private notificationsCenterTitle: HTMLSpanElement | undefined;
45
private notificationsList: NotificationsList | undefined;
46
private _isVisible: boolean | undefined;
47
private workbenchDimensions: Dimension | undefined;
48
private readonly notificationsCenterVisibleContextKey;
49
private clearAllAction: ClearAllNotificationsAction | undefined;
50
private configureDoNotDisturbAction: ConfigureDoNotDisturbAction | undefined;
51
52
constructor(
53
private readonly container: HTMLElement,
54
private readonly model: INotificationsModel,
55
@IThemeService themeService: IThemeService,
56
@IInstantiationService private readonly instantiationService: IInstantiationService,
57
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
58
@IContextKeyService contextKeyService: IContextKeyService,
59
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
60
@IKeybindingService private readonly keybindingService: IKeybindingService,
61
@INotificationService private readonly notificationService: INotificationService,
62
@IAccessibilitySignalService private readonly accessibilitySignalService: IAccessibilitySignalService,
63
@IContextMenuService private readonly contextMenuService: IContextMenuService
64
) {
65
super(themeService);
66
67
this.notificationsCenterVisibleContextKey = NotificationsCenterVisibleContext.bindTo(contextKeyService);
68
69
this.registerListeners();
70
}
71
72
private registerListeners(): void {
73
this._register(this.model.onDidChangeNotification(e => this.onDidChangeNotification(e)));
74
this._register(this.layoutService.onDidLayoutMainContainer(dimension => this.layout(Dimension.lift(dimension))));
75
this._register(this.notificationService.onDidChangeFilter(() => this.onDidChangeFilter()));
76
}
77
78
private onDidChangeFilter(): void {
79
if (this.notificationService.getFilter() === NotificationsFilter.ERROR) {
80
this.hide(); // hide the notification center when we have a error filter enabled
81
}
82
}
83
84
get isVisible(): boolean {
85
return !!this._isVisible;
86
}
87
88
show(): void {
89
if (this._isVisible) {
90
const notificationsList = assertReturnsDefined(this.notificationsList);
91
92
// Make visible
93
notificationsList.show();
94
95
// Focus first
96
notificationsList.focusFirst();
97
98
return; // already visible
99
}
100
101
// Lazily create if showing for the first time
102
if (!this.notificationsCenterContainer) {
103
this.create();
104
}
105
106
// Title
107
this.updateTitle();
108
109
// Make visible
110
const [notificationsList, notificationsCenterContainer] = assertReturnsAllDefined(this.notificationsList, this.notificationsCenterContainer);
111
this._isVisible = true;
112
notificationsCenterContainer.classList.add('visible');
113
notificationsList.show();
114
115
// Layout
116
this.layout(this.workbenchDimensions);
117
118
// Show all notifications that are present now
119
notificationsList.updateNotificationsList(0, 0, this.model.notifications);
120
121
// Focus first
122
notificationsList.focusFirst();
123
124
// Theming
125
this.updateStyles();
126
127
// Mark as visible
128
this.model.notifications.forEach(notification => notification.updateVisibility(true));
129
130
// Context Key
131
this.notificationsCenterVisibleContextKey.set(true);
132
133
// Event
134
this._onDidChangeVisibility.fire();
135
}
136
137
private updateTitle(): void {
138
const [notificationsCenterTitle, clearAllAction] = assertReturnsAllDefined(this.notificationsCenterTitle, this.clearAllAction);
139
140
if (this.model.notifications.length === 0) {
141
notificationsCenterTitle.textContent = localize('notificationsEmpty', "No new notifications");
142
clearAllAction.enabled = false;
143
} else {
144
notificationsCenterTitle.textContent = localize('notifications', "Notifications");
145
clearAllAction.enabled = this.model.notifications.some(notification => !notification.hasProgress);
146
}
147
}
148
149
private create(): void {
150
151
// Container
152
this.notificationsCenterContainer = $('.notifications-center');
153
154
// Header
155
this.notificationsCenterHeader = $('.notifications-center-header');
156
this.notificationsCenterContainer.appendChild(this.notificationsCenterHeader);
157
158
// Header Title
159
this.notificationsCenterTitle = $('span.notifications-center-header-title');
160
this.notificationsCenterHeader.appendChild(this.notificationsCenterTitle);
161
162
// Header Toolbar
163
const toolbarContainer = $('.notifications-center-header-toolbar');
164
this.notificationsCenterHeader.appendChild(toolbarContainer);
165
166
const actionRunner = this._register(this.instantiationService.createInstance(NotificationActionRunner));
167
168
const that = this;
169
const notificationsToolBar = this._register(new ActionBar(toolbarContainer, {
170
ariaLabel: localize('notificationsToolbar', "Notification Center Actions"),
171
actionRunner,
172
actionViewItemProvider: (action, options) => {
173
if (action.id === ConfigureDoNotDisturbAction.ID) {
174
return this._register(this.instantiationService.createInstance(DropdownMenuActionViewItem, action, {
175
getActions() {
176
const actions = [toAction({
177
id: ToggleDoNotDisturbAction.ID,
178
label: that.notificationService.getFilter() === NotificationsFilter.OFF ? localize('turnOnNotifications', "Enable Do Not Disturb Mode") : localize('turnOffNotifications', "Disable Do Not Disturb Mode"),
179
run: () => that.notificationService.setFilter(that.notificationService.getFilter() === NotificationsFilter.OFF ? NotificationsFilter.ERROR : NotificationsFilter.OFF)
180
})];
181
182
const sortedFilters = that.notificationService.getFilters().sort((a, b) => a.label.localeCompare(b.label));
183
for (const source of sortedFilters.slice(0, NotificationsCenter.MAX_NOTIFICATION_SOURCES)) {
184
if (actions.length === 1) {
185
actions.push(new Separator());
186
}
187
188
actions.push(toAction({
189
id: `${ToggleDoNotDisturbAction.ID}.${source.id}`,
190
label: source.label,
191
checked: source.filter !== NotificationsFilter.ERROR,
192
run: () => that.notificationService.setFilter({
193
...source,
194
filter: source.filter === NotificationsFilter.ERROR ? NotificationsFilter.OFF : NotificationsFilter.ERROR
195
})
196
}));
197
}
198
199
if (sortedFilters.length > NotificationsCenter.MAX_NOTIFICATION_SOURCES) {
200
actions.push(new Separator());
201
actions.push(that._register(that.instantiationService.createInstance(ToggleDoNotDisturbBySourceAction, ToggleDoNotDisturbBySourceAction.ID, localize('moreSources', "More…"))));
202
}
203
204
return actions;
205
},
206
}, this.contextMenuService, {
207
...options,
208
actionRunner,
209
classNames: action.class,
210
keybindingProvider: action => this.keybindingService.lookupKeybinding(action.id)
211
}));
212
}
213
214
return undefined;
215
}
216
}));
217
218
this.clearAllAction = this._register(this.instantiationService.createInstance(ClearAllNotificationsAction, ClearAllNotificationsAction.ID, ClearAllNotificationsAction.LABEL));
219
notificationsToolBar.push(this.clearAllAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.clearAllAction) });
220
221
this.configureDoNotDisturbAction = this._register(this.instantiationService.createInstance(ConfigureDoNotDisturbAction, ConfigureDoNotDisturbAction.ID, ConfigureDoNotDisturbAction.LABEL));
222
notificationsToolBar.push(this.configureDoNotDisturbAction, { icon: true, label: false });
223
224
const hideAllAction = this._register(this.instantiationService.createInstance(HideNotificationsCenterAction, HideNotificationsCenterAction.ID, HideNotificationsCenterAction.LABEL));
225
notificationsToolBar.push(hideAllAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(hideAllAction) });
226
227
// Notifications List
228
this.notificationsList = this.instantiationService.createInstance(NotificationsList, this.notificationsCenterContainer, {
229
widgetAriaLabel: localize('notificationsCenterWidgetAriaLabel', "Notifications Center")
230
});
231
this.container.appendChild(this.notificationsCenterContainer);
232
}
233
234
private getKeybindingLabel(action: IAction): string | null {
235
const keybinding = this.keybindingService.lookupKeybinding(action.id);
236
237
return keybinding ? keybinding.getLabel() : null;
238
}
239
240
private onDidChangeNotification(e: INotificationChangeEvent): void {
241
if (!this._isVisible) {
242
return; // only if visible
243
}
244
245
let focusEditor = false;
246
247
// Update notifications list based on event kind
248
const [notificationsList, notificationsCenterContainer] = assertReturnsAllDefined(this.notificationsList, this.notificationsCenterContainer);
249
switch (e.kind) {
250
case NotificationChangeType.ADD:
251
notificationsList.updateNotificationsList(e.index, 0, [e.item]);
252
e.item.updateVisibility(true);
253
break;
254
case NotificationChangeType.CHANGE:
255
// Handle content changes
256
// - actions: re-draw to properly show them
257
// - message: update notification height unless collapsed
258
switch (e.detail) {
259
case NotificationViewItemContentChangeKind.ACTIONS:
260
notificationsList.updateNotificationsList(e.index, 1, [e.item]);
261
break;
262
case NotificationViewItemContentChangeKind.MESSAGE:
263
if (e.item.expanded) {
264
notificationsList.updateNotificationHeight(e.item);
265
}
266
break;
267
}
268
break;
269
case NotificationChangeType.EXPAND_COLLAPSE:
270
// Re-draw entire item when expansion changes to reveal or hide details
271
notificationsList.updateNotificationsList(e.index, 1, [e.item]);
272
break;
273
case NotificationChangeType.REMOVE:
274
focusEditor = isAncestorOfActiveElement(notificationsCenterContainer);
275
notificationsList.updateNotificationsList(e.index, 1);
276
e.item.updateVisibility(false);
277
break;
278
}
279
280
// Update title
281
this.updateTitle();
282
283
// Hide if no more notifications to show
284
if (this.model.notifications.length === 0) {
285
this.hide();
286
287
// Restore focus to editor group if we had focus
288
if (focusEditor) {
289
this.editorGroupService.activeGroup.focus();
290
}
291
}
292
}
293
294
hide(): void {
295
if (!this._isVisible || !this.notificationsCenterContainer || !this.notificationsList) {
296
return; // already hidden
297
}
298
299
const focusEditor = isAncestorOfActiveElement(this.notificationsCenterContainer);
300
301
// Hide
302
this._isVisible = false;
303
this.notificationsCenterContainer.classList.remove('visible');
304
this.notificationsList.hide();
305
306
// Mark as hidden
307
this.model.notifications.forEach(notification => notification.updateVisibility(false));
308
309
// Context Key
310
this.notificationsCenterVisibleContextKey.set(false);
311
312
// Event
313
this._onDidChangeVisibility.fire();
314
315
// Restore focus to editor group if we had focus
316
if (focusEditor) {
317
this.editorGroupService.activeGroup.focus();
318
}
319
}
320
321
override updateStyles(): void {
322
if (this.notificationsCenterContainer && this.notificationsCenterHeader) {
323
const widgetShadowColor = this.getColor(widgetShadow);
324
this.notificationsCenterContainer.style.boxShadow = widgetShadowColor ? `0 0 8px 2px ${widgetShadowColor}` : '';
325
326
const borderColor = this.getColor(NOTIFICATIONS_CENTER_BORDER);
327
this.notificationsCenterContainer.style.border = borderColor ? `1px solid ${borderColor}` : '';
328
329
const headerForeground = this.getColor(NOTIFICATIONS_CENTER_HEADER_FOREGROUND);
330
this.notificationsCenterHeader.style.color = headerForeground ?? '';
331
332
const headerBackground = this.getColor(NOTIFICATIONS_CENTER_HEADER_BACKGROUND);
333
this.notificationsCenterHeader.style.background = headerBackground ?? '';
334
335
}
336
}
337
338
layout(dimension: Dimension | undefined): void {
339
this.workbenchDimensions = dimension;
340
341
if (this._isVisible && this.notificationsCenterContainer) {
342
const maxWidth = NotificationsCenter.MAX_DIMENSIONS.width;
343
const maxHeight = NotificationsCenter.MAX_DIMENSIONS.height;
344
345
let availableWidth = maxWidth;
346
let availableHeight = maxHeight;
347
348
if (this.workbenchDimensions) {
349
350
// Make sure notifications are not exceding available width
351
availableWidth = this.workbenchDimensions.width;
352
availableWidth -= (2 * 8); // adjust for paddings left and right
353
354
// Make sure notifications are not exceeding available height
355
availableHeight = this.workbenchDimensions.height - 35 /* header */;
356
if (this.layoutService.isVisible(Parts.STATUSBAR_PART, mainWindow)) {
357
availableHeight -= 22; // adjust for status bar
358
}
359
360
if (this.layoutService.isVisible(Parts.TITLEBAR_PART, mainWindow)) {
361
availableHeight -= 22; // adjust for title bar
362
}
363
364
availableHeight -= (2 * 12); // adjust for paddings top and bottom
365
}
366
367
// Apply to list
368
const notificationsList = assertReturnsDefined(this.notificationsList);
369
notificationsList.layout(Math.min(maxWidth, availableWidth), Math.min(maxHeight, availableHeight));
370
}
371
}
372
373
clearAll(): void {
374
375
// Hide notifications center first
376
this.hide();
377
378
// Close all
379
for (const notification of [...this.model.notifications] /* copy array since we modify it from closing */) {
380
if (!notification.hasProgress) {
381
notification.close();
382
}
383
this.accessibilitySignalService.playSignal(AccessibilitySignal.clear);
384
}
385
}
386
}
387
388
389