Path: blob/main/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './media/notificationsCenter.css';6import './media/notificationsActions.css';7import { NOTIFICATIONS_CENTER_HEADER_FOREGROUND, NOTIFICATIONS_CENTER_HEADER_BACKGROUND, NOTIFICATIONS_CENTER_BORDER } from '../../../common/theme.js';8import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js';9import { INotificationsModel, INotificationChangeEvent, NotificationChangeType, NotificationViewItemContentChangeKind } from '../../../common/notifications.js';10import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js';11import { Emitter } from '../../../../base/common/event.js';12import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';13import { INotificationsCenterController, NotificationActionRunner } from './notificationsCommands.js';14import { NotificationsList } from './notificationsList.js';15import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';16import { $, Dimension, isAncestorOfActiveElement } from '../../../../base/browser/dom.js';17import { widgetShadow } from '../../../../platform/theme/common/colorRegistry.js';18import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';19import { localize } from '../../../../nls.js';20import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';21import { ClearAllNotificationsAction, ConfigureDoNotDisturbAction, ToggleDoNotDisturbBySourceAction, HideNotificationsCenterAction, ToggleDoNotDisturbAction } from './notificationsActions.js';22import { IAction, Separator, toAction } from '../../../../base/common/actions.js';23import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';24import { assertReturnsAllDefined, assertReturnsDefined } from '../../../../base/common/types.js';25import { NotificationsCenterVisibleContext } from '../../../common/contextkeys.js';26import { INotificationService, NotificationsFilter } from '../../../../platform/notification/common/notification.js';27import { mainWindow } from '../../../../base/browser/window.js';28import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';29import { DropdownMenuActionViewItem } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js';30import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';3132export class NotificationsCenter extends Themable implements INotificationsCenterController {3334private static readonly MAX_DIMENSIONS = new Dimension(450, 400);3536private static readonly MAX_NOTIFICATION_SOURCES = 10; // maximum number of notification sources to show in configure dropdown3738private readonly _onDidChangeVisibility = this._register(new Emitter<void>());39readonly onDidChangeVisibility = this._onDidChangeVisibility.event;4041private notificationsCenterContainer: HTMLElement | undefined;42private notificationsCenterHeader: HTMLElement | undefined;43private notificationsCenterTitle: HTMLSpanElement | undefined;44private notificationsList: NotificationsList | undefined;45private _isVisible: boolean | undefined;46private workbenchDimensions: Dimension | undefined;47private readonly notificationsCenterVisibleContextKey;48private clearAllAction: ClearAllNotificationsAction | undefined;49private configureDoNotDisturbAction: ConfigureDoNotDisturbAction | undefined;5051constructor(52private readonly container: HTMLElement,53private readonly model: INotificationsModel,54@IThemeService themeService: IThemeService,55@IInstantiationService private readonly instantiationService: IInstantiationService,56@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,57@IContextKeyService contextKeyService: IContextKeyService,58@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,59@IKeybindingService private readonly keybindingService: IKeybindingService,60@INotificationService private readonly notificationService: INotificationService,61@IAccessibilitySignalService private readonly accessibilitySignalService: IAccessibilitySignalService,62@IContextMenuService private readonly contextMenuService: IContextMenuService63) {64super(themeService);6566this.notificationsCenterVisibleContextKey = NotificationsCenterVisibleContext.bindTo(contextKeyService);6768this.registerListeners();69}7071private registerListeners(): void {72this._register(this.model.onDidChangeNotification(e => this.onDidChangeNotification(e)));73this._register(this.layoutService.onDidLayoutMainContainer(dimension => this.layout(Dimension.lift(dimension))));74this._register(this.notificationService.onDidChangeFilter(() => this.onDidChangeFilter()));75}7677private onDidChangeFilter(): void {78if (this.notificationService.getFilter() === NotificationsFilter.ERROR) {79this.hide(); // hide the notification center when we have a error filter enabled80}81}8283get isVisible(): boolean {84return !!this._isVisible;85}8687show(): void {88if (this._isVisible) {89const notificationsList = assertReturnsDefined(this.notificationsList);9091// Make visible92notificationsList.show();9394// Focus first95notificationsList.focusFirst();9697return; // already visible98}99100// Lazily create if showing for the first time101if (!this.notificationsCenterContainer) {102this.create();103}104105// Title106this.updateTitle();107108// Make visible109const [notificationsList, notificationsCenterContainer] = assertReturnsAllDefined(this.notificationsList, this.notificationsCenterContainer);110this._isVisible = true;111notificationsCenterContainer.classList.add('visible');112notificationsList.show();113114// Layout115this.layout(this.workbenchDimensions);116117// Show all notifications that are present now118notificationsList.updateNotificationsList(0, 0, this.model.notifications);119120// Focus first121notificationsList.focusFirst();122123// Theming124this.updateStyles();125126// Mark as visible127this.model.notifications.forEach(notification => notification.updateVisibility(true));128129// Context Key130this.notificationsCenterVisibleContextKey.set(true);131132// Event133this._onDidChangeVisibility.fire();134}135136private updateTitle(): void {137const [notificationsCenterTitle, clearAllAction] = assertReturnsAllDefined(this.notificationsCenterTitle, this.clearAllAction);138139if (this.model.notifications.length === 0) {140notificationsCenterTitle.textContent = localize('notificationsEmpty', "No new notifications");141clearAllAction.enabled = false;142} else {143notificationsCenterTitle.textContent = localize('notifications', "Notifications");144clearAllAction.enabled = this.model.notifications.some(notification => !notification.hasProgress);145}146}147148private create(): void {149150// Container151this.notificationsCenterContainer = $('.notifications-center');152153// Header154this.notificationsCenterHeader = $('.notifications-center-header');155this.notificationsCenterContainer.appendChild(this.notificationsCenterHeader);156157// Header Title158this.notificationsCenterTitle = $('span.notifications-center-header-title');159this.notificationsCenterHeader.appendChild(this.notificationsCenterTitle);160161// Header Toolbar162const toolbarContainer = $('.notifications-center-header-toolbar');163this.notificationsCenterHeader.appendChild(toolbarContainer);164165const actionRunner = this._register(this.instantiationService.createInstance(NotificationActionRunner));166167const that = this;168const notificationsToolBar = this._register(new ActionBar(toolbarContainer, {169ariaLabel: localize('notificationsToolbar', "Notification Center Actions"),170actionRunner,171actionViewItemProvider: (action, options) => {172if (action.id === ConfigureDoNotDisturbAction.ID) {173return this._register(this.instantiationService.createInstance(DropdownMenuActionViewItem, action, {174getActions() {175const actions = [toAction({176id: ToggleDoNotDisturbAction.ID,177label: that.notificationService.getFilter() === NotificationsFilter.OFF ? localize('turnOnNotifications', "Enable Do Not Disturb Mode") : localize('turnOffNotifications', "Disable Do Not Disturb Mode"),178run: () => that.notificationService.setFilter(that.notificationService.getFilter() === NotificationsFilter.OFF ? NotificationsFilter.ERROR : NotificationsFilter.OFF)179})];180181const sortedFilters = that.notificationService.getFilters().sort((a, b) => a.label.localeCompare(b.label));182for (const source of sortedFilters.slice(0, NotificationsCenter.MAX_NOTIFICATION_SOURCES)) {183if (actions.length === 1) {184actions.push(new Separator());185}186187actions.push(toAction({188id: `${ToggleDoNotDisturbAction.ID}.${source.id}`,189label: source.label,190checked: source.filter !== NotificationsFilter.ERROR,191run: () => that.notificationService.setFilter({192...source,193filter: source.filter === NotificationsFilter.ERROR ? NotificationsFilter.OFF : NotificationsFilter.ERROR194})195}));196}197198if (sortedFilters.length > NotificationsCenter.MAX_NOTIFICATION_SOURCES) {199actions.push(new Separator());200actions.push(that._register(that.instantiationService.createInstance(ToggleDoNotDisturbBySourceAction, ToggleDoNotDisturbBySourceAction.ID, localize('moreSources', "Moreā¦"))));201}202203return actions;204},205}, this.contextMenuService, {206...options,207actionRunner,208classNames: action.class,209keybindingProvider: action => this.keybindingService.lookupKeybinding(action.id)210}));211}212213return undefined;214}215}));216217this.clearAllAction = this._register(this.instantiationService.createInstance(ClearAllNotificationsAction, ClearAllNotificationsAction.ID, ClearAllNotificationsAction.LABEL));218notificationsToolBar.push(this.clearAllAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.clearAllAction) });219220this.configureDoNotDisturbAction = this._register(this.instantiationService.createInstance(ConfigureDoNotDisturbAction, ConfigureDoNotDisturbAction.ID, ConfigureDoNotDisturbAction.LABEL));221notificationsToolBar.push(this.configureDoNotDisturbAction, { icon: true, label: false });222223const hideAllAction = this._register(this.instantiationService.createInstance(HideNotificationsCenterAction, HideNotificationsCenterAction.ID, HideNotificationsCenterAction.LABEL));224notificationsToolBar.push(hideAllAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(hideAllAction) });225226// Notifications List227this.notificationsList = this.instantiationService.createInstance(NotificationsList, this.notificationsCenterContainer, {228widgetAriaLabel: localize('notificationsCenterWidgetAriaLabel', "Notifications Center")229});230this.container.appendChild(this.notificationsCenterContainer);231}232233private getKeybindingLabel(action: IAction): string | null {234const keybinding = this.keybindingService.lookupKeybinding(action.id);235236return keybinding ? keybinding.getLabel() : null;237}238239private onDidChangeNotification(e: INotificationChangeEvent): void {240if (!this._isVisible) {241return; // only if visible242}243244let focusEditor = false;245246// Update notifications list based on event kind247const [notificationsList, notificationsCenterContainer] = assertReturnsAllDefined(this.notificationsList, this.notificationsCenterContainer);248switch (e.kind) {249case NotificationChangeType.ADD:250notificationsList.updateNotificationsList(e.index, 0, [e.item]);251e.item.updateVisibility(true);252break;253case NotificationChangeType.CHANGE:254// Handle content changes255// - actions: re-draw to properly show them256// - message: update notification height unless collapsed257switch (e.detail) {258case NotificationViewItemContentChangeKind.ACTIONS:259notificationsList.updateNotificationsList(e.index, 1, [e.item]);260break;261case NotificationViewItemContentChangeKind.MESSAGE:262if (e.item.expanded) {263notificationsList.updateNotificationHeight(e.item);264}265break;266}267break;268case NotificationChangeType.EXPAND_COLLAPSE:269// Re-draw entire item when expansion changes to reveal or hide details270notificationsList.updateNotificationsList(e.index, 1, [e.item]);271break;272case NotificationChangeType.REMOVE:273focusEditor = isAncestorOfActiveElement(notificationsCenterContainer);274notificationsList.updateNotificationsList(e.index, 1);275e.item.updateVisibility(false);276break;277}278279// Update title280this.updateTitle();281282// Hide if no more notifications to show283if (this.model.notifications.length === 0) {284this.hide();285286// Restore focus to editor group if we had focus287if (focusEditor) {288this.editorGroupService.activeGroup.focus();289}290}291}292293hide(): void {294if (!this._isVisible || !this.notificationsCenterContainer || !this.notificationsList) {295return; // already hidden296}297298const focusEditor = isAncestorOfActiveElement(this.notificationsCenterContainer);299300// Hide301this._isVisible = false;302this.notificationsCenterContainer.classList.remove('visible');303this.notificationsList.hide();304305// Mark as hidden306this.model.notifications.forEach(notification => notification.updateVisibility(false));307308// Context Key309this.notificationsCenterVisibleContextKey.set(false);310311// Event312this._onDidChangeVisibility.fire();313314// Restore focus to editor group if we had focus315if (focusEditor) {316this.editorGroupService.activeGroup.focus();317}318}319320override updateStyles(): void {321if (this.notificationsCenterContainer && this.notificationsCenterHeader) {322const widgetShadowColor = this.getColor(widgetShadow);323this.notificationsCenterContainer.style.boxShadow = widgetShadowColor ? `0 0 8px 2px ${widgetShadowColor}` : '';324325const borderColor = this.getColor(NOTIFICATIONS_CENTER_BORDER);326this.notificationsCenterContainer.style.border = borderColor ? `1px solid ${borderColor}` : '';327328const headerForeground = this.getColor(NOTIFICATIONS_CENTER_HEADER_FOREGROUND);329this.notificationsCenterHeader.style.color = headerForeground ?? '';330331const headerBackground = this.getColor(NOTIFICATIONS_CENTER_HEADER_BACKGROUND);332this.notificationsCenterHeader.style.background = headerBackground ?? '';333334}335}336337layout(dimension: Dimension | undefined): void {338this.workbenchDimensions = dimension;339340if (this._isVisible && this.notificationsCenterContainer) {341const maxWidth = NotificationsCenter.MAX_DIMENSIONS.width;342const maxHeight = NotificationsCenter.MAX_DIMENSIONS.height;343344let availableWidth = maxWidth;345let availableHeight = maxHeight;346347if (this.workbenchDimensions) {348349// Make sure notifications are not exceding available width350availableWidth = this.workbenchDimensions.width;351availableWidth -= (2 * 8); // adjust for paddings left and right352353// Make sure notifications are not exceeding available height354availableHeight = this.workbenchDimensions.height - 35 /* header */;355if (this.layoutService.isVisible(Parts.STATUSBAR_PART, mainWindow)) {356availableHeight -= 22; // adjust for status bar357}358359if (this.layoutService.isVisible(Parts.TITLEBAR_PART, mainWindow)) {360availableHeight -= 22; // adjust for title bar361}362363availableHeight -= (2 * 12); // adjust for paddings top and bottom364}365366// Apply to list367const notificationsList = assertReturnsDefined(this.notificationsList);368notificationsList.layout(Math.min(maxWidth, availableWidth), Math.min(maxHeight, availableHeight));369}370}371372clearAll(): void {373374// Hide notifications center first375this.hide();376377// Close all378for (const notification of [...this.model.notifications] /* copy array since we modify it from closing */) {379if (!notification.hasProgress) {380notification.close();381}382this.accessibilitySignalService.playSignal(AccessibilitySignal.clear);383}384}385}386387388389