From 56064364ff182cf13c4298a02b25400ada427d84 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Sun, 18 Feb 2018 08:34:22 +0100 Subject: [PATCH] notifications - add progress bar and API --- src/vs/base/browser/ui/button/button.ts | 6 +- .../browser/ui/progressbar/progressbar.ts | 12 +- .../standalone/browser/simpleServices.ts | 2 +- .../notification/common/notification.ts | 8 + .../notifications/media/notificationsList.css | 7 + .../parts/notifications/notificationsList.ts | 1 - .../notifications/notificationsViewer.ts | 189 ++++++++++++------ src/vs/workbench/common/notifications.ts | 145 +++++++++++++- .../common/notificationService.ts | 30 ++- 9 files changed, 323 insertions(+), 77 deletions(-) diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index ec8e05d6265..d8883864b4b 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -97,9 +97,9 @@ export class Button { }); // Also set hover background when button is focused for feedback - const tracker = DOM.trackFocus(this.$el.getHTMLElement()); - tracker.onDidFocus(() => this.setHoverBackground()); - tracker.onDidBlur(() => this.applyStyles()); // restore standard styles + this.focusTracker = DOM.trackFocus(this.$el.getHTMLElement()); + this.focusTracker.onDidFocus(() => this.setHoverBackground()); + this.focusTracker.onDidBlur(() => this.applyStyles()); // restore standard styles this.applyStyles(); } diff --git a/src/vs/base/browser/ui/progressbar/progressbar.ts b/src/vs/base/browser/ui/progressbar/progressbar.ts index 19e5657414c..f07bd617f7a 100644 --- a/src/vs/base/browser/ui/progressbar/progressbar.ts +++ b/src/vs/base/browser/ui/progressbar/progressbar.ts @@ -45,7 +45,9 @@ export class ProgressBar { private animationStopToken: ValueCallback; private progressBarBackground: Color; - constructor(builder: Builder, options?: IProgressBarOptions) { + constructor(container: Builder, options?: IProgressBarOptions); + constructor(container: HTMLElement, options?: IProgressBarOptions); + constructor(container: any, options?: IProgressBarOptions) { this.options = options || Object.create(null); mixin(this.options, defaultOpts, false); @@ -54,11 +56,13 @@ export class ProgressBar { this.progressBarBackground = this.options.progressBarBackground; - this.create(builder); + this.create(container); } - private create(parent: Builder): void { - parent.div({ 'class': css_progress_container }, (builder) => { + private create(container: Builder): void; + private create(container: HTMLElement): void; + private create(container: any): void { + $(container).div({ 'class': css_progress_container }, (builder) => { this.element = builder.clone(); builder.div({ 'class': css_progress_bit }).on([DOM.EventType.ANIMATION_START, DOM.EventType.ANIMATION_END, DOM.EventType.ANIMATION_ITERATION], (e: Event) => { diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 2ed59bc5d4d..672d275930f 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -285,7 +285,7 @@ export class SimpleNotificationService implements INotificationService { public _serviceBrand: any; - private static readonly Empty: INotificationHandle = { dispose: () => undefined }; + private static readonly Empty: INotificationHandle = { dispose: () => undefined, progress: undefined }; public info(message: string): INotificationHandle { return this.notify({ severity: Severity.Info, message }); diff --git a/src/vs/platform/notification/common/notification.ts b/src/vs/platform/notification/common/notification.ts index 0038d43f7fe..2599887d985 100644 --- a/src/vs/platform/notification/common/notification.ts +++ b/src/vs/platform/notification/common/notification.ts @@ -25,7 +25,15 @@ export interface INotificationActions { secondary?: IAction[]; } +export interface INotificationProgress { + infinite(): void; + total(value: number): void; + worked(value: number): void; + done(): void; +} + export interface INotificationHandle extends IDisposable { + readonly progress: INotificationProgress; } export interface INotificationService { diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsList.css b/src/vs/workbench/browser/parts/notifications/media/notificationsList.css index d1036af4143..a88ed9c305a 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsList.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsList.css @@ -112,4 +112,11 @@ .monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-actions-container .monaco-button:not(:first-of-type) { margin-left: 10px; +} + +/** Notification: Progress */ + +.monaco-workbench .notifications-list-container .progress-bit { + height: 2px; + bottom: 0; } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/notifications/notificationsList.ts b/src/vs/workbench/browser/parts/notifications/notificationsList.ts index 67bc31342c5..a8676533176 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsList.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsList.ts @@ -66,7 +66,6 @@ export class NotificationsList extends Themable { // Notification Renderer const renderer = this.instantiationService.createInstance(NotificationRenderer, this.instantiationService.createInstance(NotificationActionRunner)); - this.toUnbind.push(renderer); // List this.list = this.instantiationService.createInstance( diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index ad594abc0a9..380a43a940a 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -14,7 +14,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { Severity } from 'vs/platform/message/common/message'; import { localize } from 'vs/nls'; import { Button } from 'vs/base/browser/ui/button/button'; -import { attachButtonStyler } from 'vs/platform/theme/common/styler'; +import { attachButtonStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -27,6 +27,7 @@ import { INotificationViewItem, NotificationViewItem } from 'vs/workbench/common import { ClearNotificationAction, ExpandNotificationAction, CollapseNotificationAction, ConfigureNotificationAction } from 'vs/workbench/browser/parts/notifications/notificationsActions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { MarkedOptions } from 'vs/base/common/marked/marked'; +import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; export class NotificationsListDelegate implements IDelegate { @@ -117,6 +118,9 @@ export interface INotificationTemplateData { detailsRow: HTMLElement; source: HTMLElement; actionsContainer: HTMLElement; + progress: ProgressBar; + + renderer: NotificationTemplateRenderer; } class NotificationMessageMarkdownRenderer { @@ -150,29 +154,12 @@ export class NotificationRenderer implements IRenderer { + NotificationTemplateRenderer.SEVERITIES.forEach(severity => { const domAction = notification.severity === this.toSeverity(severity) ? addClass : removeClass; - domAction(data.icon, `icon-${severity}`); + domAction(this.template.icon, `icon-${severity}`); }); // Message (simple markdown with links support) - clearNode(data.message); - data.message.appendChild(NotificationMessageMarkdownRenderer.render(notification.message, (content: string) => this.openerService.open(URI.parse(content)).then(void 0, onUnexpectedError))); + clearNode(this.template.message); + this.template.message.appendChild(NotificationMessageMarkdownRenderer.render(notification.message, (content: string) => this.openerService.open(URI.parse(content)).then(void 0, onUnexpectedError))); - const messageOverflows = notification.canCollapse && !notification.expanded && data.message.scrollWidth > data.message.clientWidth; + const messageOverflows = notification.canCollapse && !notification.expanded && this.template.message.scrollWidth > this.template.message.clientWidth; if (messageOverflows) { - data.message.title = data.message.textContent; + this.template.message.title = this.template.message.textContent; } else { - data.message.removeAttribute('title'); + this.template.message.removeAttribute('title'); } - const links = data.message.querySelectorAll('a'); + const links = this.template.message.querySelectorAll('a'); for (let i = 0; i < links.length; i++) { links.item(i).tabIndex = -1; // prevent keyboard navigation to links to allow for better keyboard support within a message } @@ -291,7 +319,7 @@ export class NotificationRenderer implements IRenderer 0) { const configureNotificationAction = this.instantiationService.createInstance(ConfigureNotificationAction, ConfigureNotificationAction.ID, ConfigureNotificationAction.LABEL, notification.actions.secondary); actions.push(configureNotificationAction); - data.toDispose.push(configureNotificationAction); + this.inputDisposeables.push(configureNotificationAction); } let showExpandCollapseAction = false; @@ -306,27 +334,74 @@ export class NotificationRenderer implements IRenderer data.toolbar.push(action, { icon: true, label: false, keybinding: this.getKeybindingLabel(action) })); + this.template.toolbar.clear(); + this.template.toolbar.context = notification; + actions.forEach(action => this.template.toolbar.push(action, { icon: true, label: false, keybinding: this.getKeybindingLabel(action) })); // Source if (notification.expanded && notification.source) { - data.source.innerText = localize('notificationSource', "Source: {0}", notification.source); + this.template.source.innerText = localize('notificationSource', "Source: {0}", notification.source); } else { - data.source.innerText = ''; + this.template.source.innerText = ''; } // Actions - clearNode(data.actionsContainer); + clearNode(this.template.actionsContainer); if (notification.expanded) { - notification.actions.primary.forEach(action => this.createButton(notification, action, data)); + notification.actions.primary.forEach(action => this.createButton(notification, action)); + } + + // Progress + this.renderProgress(notification); + this.inputDisposeables.push(notification.progress.onDidChange(() => this.renderProgress(notification))); + } + + private renderProgress(notification: INotificationViewItem): void { + + // Return early if the item has no progress + if (!notification.hasProgress()) { + this.template.progress.done().getContainer().hide(); + + return; + } + + // Infinite + const state = notification.progress.state; + if (state.infinite) { + this.template.progress.infinite().getContainer().show(); + } + + // Total / Worked + else if (state.total || state.worked) { + if (state.total) { + this.template.progress.total(state.total); + } + + if (state.worked) { + this.template.progress.worked(state.worked).getContainer().show(); + } + } + + // Done + else { + this.template.progress.done().getContainer().hide(); + } + } + + private toSeverity(severity: 'info' | 'warning' | 'error'): Severity { + switch (severity) { + case 'info': + return Severity.Info; + case 'warning': + return Severity.Warning; + case 'error': + return Severity.Error; } } @@ -336,29 +411,25 @@ export class NotificationRenderer implements IRenderer { + this.inputDisposeables.push(button.onDidClick(() => { // Run action this.actionRunner.run(action); // Hide notification notification.dispose(); - }); + })); - return button; - } + this.inputDisposeables.push(attachButtonStyler(button, this.themeService)); + this.inputDisposeables.push(button); - public disposeTemplate(templateData: INotificationTemplateData): void { - templateData.toolbar.dispose(); - templateData.toDispose = dispose(templateData.toDispose); + return button; } public dispose(): void { - this.toDispose = dispose(this.toDispose); + this.inputDisposeables = dispose(this.inputDisposeables); } } \ No newline at end of file diff --git a/src/vs/workbench/common/notifications.ts b/src/vs/workbench/common/notifications.ts index 78509de5e76..ebd628c2d26 100644 --- a/src/vs/workbench/common/notifications.ts +++ b/src/vs/workbench/common/notifications.ts @@ -7,7 +7,7 @@ import { Severity } from 'vs/platform/message/common/message'; import { IMarkdownString } from 'vs/base/common/htmlContent'; -import { INotification, INotificationHandle, INotificationActions } from 'vs/platform/notification/common/notification'; +import { INotification, INotificationHandle, INotificationActions, INotificationProgress } from 'vs/platform/notification/common/notification'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import Event, { Emitter, once } from 'vs/base/common/event'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -34,9 +34,21 @@ export interface INotificationChangeEvent { } class NoOpNotification implements INotificationHandle { + public readonly progress = new NoOpProgress(); + public dispose(): void { } } +class NoOpProgress implements INotificationProgress { + public infinite(): void { } + + public done(): void { } + + public total(value: number): void { } + + public worked(value: number): void { } +} + export class NotificationsModel implements INotificationsModel { private static NO_OP_NOTIFICATION = new NoOpNotification(); @@ -64,8 +76,8 @@ export class NotificationsModel implements INotificationsModel { public notify(notification: INotification): INotificationHandle { const item = this.createViewItem(notification); - if (item instanceof NoOpNotification) { - return item; // return early if this is a no-op + if (!item) { + return NotificationsModel.NO_OP_NOTIFICATION; // return early if this is a no-op } // Deduplicate @@ -80,9 +92,15 @@ export class NotificationsModel implements INotificationsModel { // Events this._onDidNotificationChange.fire({ item, index: 0, kind: NotificationChangeType.ADD }); - // Wrap into handles + // Wrap into handle return { - dispose: () => this.disposeItem(item) + dispose: () => this.disposeItem(item), + progress: { + infinite: () => item.progress.infinite(), + total: value => item.progress.total(value), + worked: value => item.progress.worked(value), + done: () => item.progress.done() + } }; } @@ -106,10 +124,10 @@ export class NotificationsModel implements INotificationsModel { return void 0; } - private createViewItem(notification: INotification): INotificationViewItem | NoOpNotification { + private createViewItem(notification: INotification): INotificationViewItem { const item = NotificationViewItem.create(notification); if (!item) { - return NotificationsModel.NO_OP_NOTIFICATION; + return null; } // Item Events @@ -143,6 +161,7 @@ export interface INotificationViewItem { readonly message: IMarkdownString; readonly source: string; readonly actions: INotificationActions; + readonly progress: INotificationViewItemProgress; readonly expanded: boolean; readonly canCollapse: boolean; @@ -154,6 +173,8 @@ export interface INotificationViewItem { collapse(): void; toggle(): void; + hasProgress(): boolean; + dispose(): void; equals(item: INotificationViewItem); @@ -163,6 +184,101 @@ export function isNotificationViewItem(obj: any): obj is INotificationViewItem { return obj instanceof NotificationViewItem; } +export interface INotificationViewItemProgressState { + infinite?: boolean; + total?: number; + worked?: number; + done?: boolean; +} + +export interface INotificationViewItemProgress extends INotificationProgress { + readonly state: INotificationViewItemProgressState; + readonly onDidChange: Event; + + dispose(): void; +} + +export class NotificationViewItemProgress implements INotificationViewItemProgress { + private _state: INotificationViewItemProgressState; + + private _onDidChange: Emitter; + private toDispose: IDisposable[]; + + constructor() { + this.toDispose = []; + this._state = Object.create(null); + + this._onDidChange = new Emitter(); + this.toDispose.push(this._onDidChange); + } + + public get state(): INotificationViewItemProgressState { + return this._state; + } + + public get onDidChange(): Event { + return this._onDidChange.event; + } + + public infinite(): void { + if (this._state.infinite) { + return; + } + + this._state.infinite = true; + + this._state.total = void 0; + this._state.worked = void 0; + this._state.done = void 0; + + this._onDidChange.fire(); + } + + public done(): void { + if (this._state.done) { + return; + } + + this._state.done = true; + + this._state.infinite = void 0; + this._state.total = void 0; + this._state.worked = void 0; + + this._onDidChange.fire(); + } + + public total(value: number): void { + if (this._state.total === value) { + return; + } + + this._state.total = value; + + this._state.infinite = void 0; + this._state.done = void 0; + + this._onDidChange.fire(); + } + + public worked(value: number): void { + if (this._state.worked === value) { + return; + } + + this._state.worked = value; + + this._state.infinite = void 0; + this._state.done = void 0; + + this._onDidChange.fire(); + } + + public dispose(): void { + this.toDispose = dispose(this.toDispose); + } +} + export class NotificationViewItem implements INotificationViewItem { private static MAX_MESSAGE_LENGTH = 1000; @@ -173,6 +289,8 @@ export class NotificationViewItem implements INotificationViewItem { private _onDidChange: Emitter; private _onDidDispose: Emitter; + private _progress: INotificationViewItemProgress; + public static create(notification: INotification): INotificationViewItem { if (!notification || !notification.message || isPromiseCanceledError(notification.message)) { return null; // we need a message to show @@ -247,6 +365,19 @@ export class NotificationViewItem implements INotificationViewItem { return this._severity; } + public hasProgress(): boolean { + return !!this._progress; + } + + public get progress(): INotificationViewItemProgress { + if (!this._progress) { + this._progress = new NotificationViewItemProgress(); + this.toDispose.push(this._progress); + } + + return this._progress; + } + public get message(): IMarkdownString { return this._message; } diff --git a/src/vs/workbench/services/notification/common/notificationService.ts b/src/vs/workbench/services/notification/common/notificationService.ts index 64efbce857c..dba2ba0a4f4 100644 --- a/src/vs/workbench/services/notification/common/notificationService.ts +++ b/src/vs/workbench/services/notification/common/notificationService.ts @@ -42,7 +42,7 @@ export class NotificationService implements INotificationService { message: 'This is a info message with a [link](https://code.visualstudio.com). This is a info message with a [link](https://code.visualstudio.com). This is a info message with a [link](https://code.visualstudio.com). This is a info message with a [link](https://code.visualstudio.com).', source: 'GitLens Extension' }); - this.notify({ + let handle = this.notify({ severity: Severity.Warning, message: 'This is a warning message with a [link](https://code.visualstudio.com).', actions: { @@ -56,10 +56,36 @@ export class NotificationService implements INotificationService { ] } }); - this.notify({ + handle.progress.total(100); + setTimeout(() => { + handle.progress.worked(20); + setTimeout(() => { + handle.progress.worked(40); + setTimeout(() => { + handle.progress.worked(60); + setTimeout(() => { + handle.progress.worked(80); + setTimeout(() => { + handle.progress.worked(100); + setTimeout(() => { + handle.progress.done(); + }, 3000); + }, 3000); + }, 3000); + }, 3000); + }, 3000); + }, 1000); + + let handle2 = this.notify({ severity: Severity.Error, message: 'This is a error message with a [link](https://code.visualstudio.com). This is a error message with a [link](https://code.visualstudio.com). This is a error message with a [link](https://code.visualstudio.com). This is a error message with a [link](https://code.visualstudio.com). This is a error message with a [link](https://code.visualstudio.com). This is a error message with a [link](https://code.visualstudio.com). This is a error message with a [link](https://code.visualstudio.com).This is a error message with a [link](https://code.visualstudio.com). This is a error message with a [link](https://code.visualstudio.com). This is a error message with a [link](https://code.visualstudio.com). This is a error message with a [link](https://code.visualstudio.com).' }); + setTimeout(() => { + handle2.progress.infinite(); + setTimeout(() => { + handle2.progress.done(); + }, 1500); + }, 500); }, 500); } -- GitLab