notificationsCenter.ts 10.1 KB
Newer Older
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

8
import 'vs/css!./media/notificationsCenter';
9
import { addClass, removeClass } from 'vs/base/browser/dom';
10 11 12 13
import { WorkbenchList } from 'vs/platform/list/browser/listService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IListOptions } from 'vs/base/browser/ui/list/listWidget';
import { localize } from 'vs/nls';
14
import { Themable, NOTIFICATIONS_BORDER, NOTIFICATIONS_LINKS, NOTIFICATIONS_BACKGROUND, NOTIFICATIONS_FOREGROUND } from 'vs/workbench/common/theme';
15
import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
16
import { contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
17
import { INotificationViewItem, INotificationsModel, INotificationChangeEvent, NotificationChangeType } from 'vs/workbench/common/notifications';
B
Benjamin Pasero 已提交
18
import { NotificationsListDelegate, NotificationRenderer } from 'vs/workbench/browser/parts/notifications/notificationsViewer';
19
import { NotificationActionRunner } from 'vs/workbench/browser/parts/notifications/notificationsActions';
20 21
import { Dimension } from 'vs/base/browser/builder';
import { IPartService, Parts } from 'vs/workbench/services/part/common/partService';
22
import Event, { Emitter } from 'vs/base/common/event';
23 24
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { NotificationsCenterFocusedContext, NotificationsCenterVisibleContext } from 'vs/workbench/browser/parts/notifications/notificationCommands';
25

26
export class NotificationsCenter extends Themable {
27 28 29

	private static MAX_DIMENSIONS = new Dimension(600, 600);

30 31
	private listContainer: HTMLElement;
	private list: WorkbenchList<INotificationViewItem>;
32
	private viewModel: INotificationViewItem[];
33
	private _isVisible: boolean;
34
	private workbenchDimensions: Dimension;
35
	private _onDidChangeVisibility: Emitter<void>;
36
	private notificationsCenterVisibleContextKey: IContextKey<boolean>;
37 38

	constructor(
39
		private container: HTMLElement,
40
		private model: INotificationsModel,
41
		@IInstantiationService private instantiationService: IInstantiationService,
42
		@IThemeService themeService: IThemeService,
43 44
		@IPartService private partService: IPartService,
		@IContextKeyService contextKeyService: IContextKeyService
45
	) {
46 47
		super(themeService);

48 49 50
		this._onDidChangeVisibility = new Emitter<void>();
		this.toUnbind.push(this._onDidChangeVisibility);

51 52
		this.notificationsCenterVisibleContextKey = NotificationsCenterVisibleContext.bindTo(contextKeyService);

53
		this.viewModel = [];
54 55 56
		this.registerListeners();
	}

57 58 59 60
	public get onDidChangeVisibility(): Event<void> {
		return this._onDidChangeVisibility.event;
	}

61 62 63 64
	public get isVisible(): boolean {
		return this._isVisible;
	}

65 66 67 68 69 70 71 72 73 74
	public get selected(): INotificationViewItem {
		if (!this._isVisible || !this.list) {
			return null;
		}

		const focusedIndex = this.list.getFocus()[0];

		return this.viewModel[focusedIndex];
	}

75
	private registerListeners(): void {
76
		this.toUnbind.push(this.model.onDidNotificationChange(e => this.onDidNotificationChange(e)));
77 78
	}

79 80
	public show(): void {
		if (this._isVisible) {
81 82
			this.focusNotificationsList();

83
			return; // already visible
84
		}
85

86 87 88
		// Lazily create if showing for the first time
		if (!this.list) {
			this.createNotificationsList();
89
		}
90 91 92 93 94 95 96

		// Make visible
		this._isVisible = true;
		addClass(this.listContainer, 'visible');

		// Show all notifications that are present now
		this.onNotificationsAdded(0, this.model.notifications);
97

98 99 100
		// Focus
		this.focusNotificationsList();

101 102 103
		// Context Key
		this.notificationsCenterVisibleContextKey.set(true);

104 105
		// Event
		this._onDidChangeVisibility.fire();
106 107
	}

108 109 110 111 112 113 114 115
	private focusNotificationsList(): void {
		if (!this._isVisible) {
			return;
		}

		this.list.domFocus();
	}

116
	private createNotificationsList(): void {
117 118 119 120 121

		// List Container
		this.listContainer = document.createElement('div');
		addClass(this.listContainer, 'notifications-list-container');

122
		// Notification Renderer
123
		const renderer = this.instantiationService.createInstance(NotificationRenderer, this.instantiationService.createInstance(NotificationActionRunner));
124 125
		this.toUnbind.push(renderer);

126 127 128 129
		// List
		this.list = this.instantiationService.createInstance(
			WorkbenchList,
			this.listContainer,
130
			new NotificationsListDelegate(this.listContainer),
131
			[renderer],
132
			{
133
				ariaLabel: localize('notificationsList', "Notifications List")
134
			} as IListOptions<INotificationViewItem>
135
		);
136
		this.toUnbind.push(this.list);
137

138 139 140
		// Context key
		NotificationsCenterFocusedContext.bindTo(this.list.contextKeyService);

141 142 143 144 145 146 147 148 149
		// Only allow for focus in notifications, as the
		// selection is too strong over the contents of
		// the notification
		this.toUnbind.push(this.list.onSelectionChange(e => {
			if (e.indexes.length > 0) {
				this.list.setSelection([]);
			}
		}));

150
		this.container.appendChild(this.listContainer);
151 152

		this.updateStyles();
153
		this.layoutList();
154 155
	}

156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
	private onDidNotificationChange(e: INotificationChangeEvent): void {
		if (!this._isVisible) {
			return; // only if visible
		}

		switch (e.kind) {
			case NotificationChangeType.ADD:
				return this.onNotificationsAdded(e.index, [e.item]);
			case NotificationChangeType.CHANGE:
				return this.onNotificationChanged(e.index, e.item);
			case NotificationChangeType.REMOVE:
				return this.onNotificationRemoved(e.index, e.item);
		}
	}

171 172
	private onNotificationsAdded(index: number, items: INotificationViewItem[]): void {
		this.updateNotificationsList(index, 0, items);
173 174
	}

175 176 177
	private onNotificationChanged(index: number, item: INotificationViewItem): void {
		this.updateNotificationsList(index, 1, [item]);
	}
178

179 180 181
	private onNotificationRemoved(index: number, item: INotificationViewItem): void {
		this.updateNotificationsList(index, 1);
	}
182

183
	private updateNotificationsList(start: number, deleteCount: number, items: INotificationViewItem[] = []) {
184

185
		// Remember focus
186 187
		const focusedIndex = this.list.getFocus()[0];
		const focusedItem = this.viewModel[focusedIndex];
188 189 190 191

		// Update view model
		this.viewModel.splice(start, deleteCount, ...items);

192 193
		// Update list
		this.list.splice(start, deleteCount, items);
194
		this.list.layout();
195

B
Benjamin Pasero 已提交
196 197 198 199 200
		// Hide if no more notifications to show
		if (this.viewModel.length === 0) {
			this.hide();
		}

201 202
		// Otherwise restore focus if we had
		else if (typeof focusedIndex === 'number') {
203 204 205 206 207 208 209 210 211 212 213
			let indexToFocus = 0;
			if (focusedItem) {
				let indexToFocusCandidate = this.viewModel.indexOf(focusedItem);
				if (indexToFocusCandidate === -1) {
					indexToFocusCandidate = focusedIndex - 1; // item could have been removed
				}

				if (indexToFocusCandidate < this.viewModel.length && indexToFocusCandidate >= 0) {
					indexToFocus = indexToFocusCandidate;
				}
			}
214

215 216
			this.list.setFocus([indexToFocus]);
		}
217 218
	}

219
	public hide(): void {
220 221 222
		if (!this._isVisible || !this.list) {
			return; // already hidden
		}
223

224 225
		// Hide
		this._isVisible = false;
226
		removeClass(this.listContainer, 'visible');
227 228 229 230 231 232

		// Clear list
		this.list.splice(0, this.viewModel.length);

		// Clear view model
		this.viewModel = [];
233

234 235 236
		// Context Key
		this.notificationsCenterVisibleContextKey.set(false);

237 238
		// Event
		this._onDidChangeVisibility.fire();
239 240 241 242
	}

	protected updateStyles(): void {
		if (this.listContainer) {
243 244 245 246 247 248
			const foreground = this.getColor(NOTIFICATIONS_FOREGROUND);
			this.listContainer.style.color = foreground ? foreground.toString() : null;

			const background = this.getColor(NOTIFICATIONS_BACKGROUND);
			this.listContainer.style.background = background ? background.toString() : null;

249 250 251 252 253 254
			const outlineColor = this.getColor(contrastBorder);
			this.listContainer.style.outlineColor = outlineColor ? outlineColor.toString() : null;

			const widgetShadowColor = this.getColor(widgetShadow);
			this.listContainer.style.boxShadow = widgetShadowColor ? `0 5px 8px ${widgetShadowColor}` : null;
		}
255
	}
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284

	public layout(dimension: Dimension): void {
		this.workbenchDimensions = dimension;

		if (this._isVisible && this.listContainer) {
			this.layoutList();
		}
	}

	private layoutList(): void {
		let width = NotificationsCenter.MAX_DIMENSIONS.width;
		let maxHeight = NotificationsCenter.MAX_DIMENSIONS.height;

		if (this.workbenchDimensions) {

			// Make sure notifications are not exceding available width
			let availableWidth = this.workbenchDimensions.width;
			availableWidth -= (2 * 12); // adjust for paddings left and right

			if (width > availableWidth) {
				width = availableWidth;
			}

			// Make sure notifications are not exceeding available height
			let availableHeight = this.workbenchDimensions.height;
			if (this.partService.isVisible(Parts.STATUSBAR_PART)) {
				availableHeight -= 22; // adjust for status bar
			}

285 286 287 288
			if (this.partService.isVisible(Parts.TITLEBAR_PART)) {
				availableHeight -= 22; // adjust for title bar
			}

289 290 291 292 293 294 295 296 297 298 299
			availableHeight -= (2 * 12); // adjust for paddings top and bottom

			if (maxHeight > availableHeight) {
				maxHeight = availableHeight;
			}
		}

		this.listContainer.style.width = `${width}px`;
		this.list.getHTMLElement().style.maxHeight = `${maxHeight}px`;
		this.list.layout();
	}
300 301 302 303 304 305 306 307 308 309 310

	public clearAll(): void {

		// Hide notifications center first
		this.hide();

		// Dispose all
		while (this.model.notifications.length) {
			this.model.notifications[0].dispose();
		}
	}
311 312 313
}

registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
314
	const linkColor = theme.getColor(NOTIFICATIONS_LINKS);
315
	if (linkColor) {
316
		collector.addRule(`.monaco-workbench > .notifications-list-container .notification-list-item .notification-list-item-message a { color: ${linkColor}; }`);
317
	}
318 319 320 321 322

	const notificationBorderColor = theme.getColor(NOTIFICATIONS_BORDER);
	if (notificationBorderColor) {
		collector.addRule(`.monaco-workbench > .notifications-list-container .notification-list-item { border-bottom: 1px solid ${notificationBorderColor}; }`);
	}
323
});