提交 56064364 编写于 作者: B Benjamin Pasero

notifications - add progress bar and API

上级 5bd22ef4
......@@ -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();
}
......
......@@ -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) => {
......
......@@ -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 });
......
......@@ -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 {
......
......@@ -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
......@@ -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(
......
......@@ -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<INotificationViewItem> {
......@@ -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<INotificationViewItem, IN
public static readonly TEMPLATE_ID = 'notification';
private static readonly SEVERITIES: ('info' | 'warning' | 'error')[] = ['info', 'warning', 'error'];
private toDispose: IDisposable[];
private closeNotificationAction: ClearNotificationAction;
private expandNotificationAction: ExpandNotificationAction;
private collapseNotificationAction: CollapseNotificationAction;
constructor(
private actionRunner: IActionRunner,
@IOpenerService private openerService: IOpenerService,
@IThemeService private themeService: IThemeService,
@IInstantiationService private instantiationService: IInstantiationService,
@IContextMenuService private contextMenuService: IContextMenuService,
@IKeybindingService private keybindingService: IKeybindingService
@IInstantiationService private instantiationService: IInstantiationService
) {
this.toDispose = [];
this.closeNotificationAction = instantiationService.createInstance(ClearNotificationAction, ClearNotificationAction.ID, ClearNotificationAction.LABEL);
this.expandNotificationAction = instantiationService.createInstance(ExpandNotificationAction, ExpandNotificationAction.ID, ExpandNotificationAction.LABEL);
this.collapseNotificationAction = instantiationService.createInstance(CollapseNotificationAction, CollapseNotificationAction.ID, CollapseNotificationAction.LABEL);
this.toDispose.push(this.closeNotificationAction, this.expandNotificationAction, this.collapseNotificationAction);
}
public get templateId() {
......@@ -218,6 +205,7 @@ export class NotificationRenderer implements IRenderer<INotificationViewItem, IN
}
}
);
data.toDispose.push(data.toolbar);
// Details Row
data.detailsRow = document.createElement('div');
......@@ -244,43 +232,83 @@ export class NotificationRenderer implements IRenderer<INotificationViewItem, IN
data.mainRow.appendChild(data.message);
data.mainRow.appendChild(toolbarContainer);
// Progress: below the rows to span the entire width of the item
data.progress = new ProgressBar(container);
data.toDispose.push(attachProgressBarStyler(data.progress, this.themeService));
data.toDispose.push(data.progress);
// Renderer
data.renderer = this.instantiationService.createInstance(NotificationTemplateRenderer, data, this.actionRunner);
data.toDispose.push(data.renderer);
return data;
}
private toSeverity(severity: 'info' | 'warning' | 'error'): Severity {
switch (severity) {
case 'info':
return Severity.Info;
case 'warning':
return Severity.Warning;
case 'error':
return Severity.Error;
public renderElement(notification: INotificationViewItem, index: number, data: INotificationTemplateData): void {
data.renderer.setInput(notification);
}
public disposeTemplate(templateData: INotificationTemplateData): void {
templateData.toDispose = dispose(templateData.toDispose);
}
}
export class NotificationTemplateRenderer {
private static closeNotificationAction: ClearNotificationAction;
private static expandNotificationAction: ExpandNotificationAction;
private static collapseNotificationAction: CollapseNotificationAction;
private static readonly SEVERITIES: ('info' | 'warning' | 'error')[] = ['info', 'warning', 'error'];
private inputDisposeables: IDisposable[];
constructor(
private template: INotificationTemplateData,
private actionRunner: IActionRunner,
@IOpenerService private openerService: IOpenerService,
@IInstantiationService private instantiationService: IInstantiationService,
@IThemeService private themeService: IThemeService,
@IKeybindingService private keybindingService: IKeybindingService
) {
this.inputDisposeables = [];
if (!NotificationTemplateRenderer.closeNotificationAction) {
NotificationTemplateRenderer.closeNotificationAction = instantiationService.createInstance(ClearNotificationAction, ClearNotificationAction.ID, ClearNotificationAction.LABEL);
NotificationTemplateRenderer.expandNotificationAction = instantiationService.createInstance(ExpandNotificationAction, ExpandNotificationAction.ID, ExpandNotificationAction.LABEL);
NotificationTemplateRenderer.collapseNotificationAction = instantiationService.createInstance(CollapseNotificationAction, CollapseNotificationAction.ID, CollapseNotificationAction.LABEL);
}
}
public renderElement(notification: INotificationViewItem, index: number, data: INotificationTemplateData): void {
public setInput(notification: INotificationViewItem): void {
this.inputDisposeables = dispose(this.inputDisposeables);
this.render(notification);
}
private render(notification: INotificationViewItem): void {
// Container
toggleClass(data.container, 'expanded', notification.expanded);
toggleClass(this.template.container, 'expanded', notification.expanded);
// Icon
NotificationRenderer.SEVERITIES.forEach(severity => {
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<INotificationViewItem, IN
if (notification.actions.secondary.length > 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<INotificationViewItem, IN
}
if (showExpandCollapseAction) {
actions.push(notification.expanded ? this.collapseNotificationAction : this.expandNotificationAction);
actions.push(notification.expanded ? NotificationTemplateRenderer.collapseNotificationAction : NotificationTemplateRenderer.expandNotificationAction);
}
actions.push(this.closeNotificationAction);
actions.push(NotificationTemplateRenderer.closeNotificationAction);
// Toolbar
data.toolbar.clear();
data.toolbar.context = notification;
actions.forEach(action => 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<INotificationViewItem, IN
return keybinding ? keybinding.getLabel() : void 0;
}
private createButton(notification: INotificationViewItem, action: IAction, data: INotificationTemplateData): Button {
const button = new Button(data.actionsContainer);
data.toDispose.push(attachButtonStyler(button, this.themeService));
private createButton(notification: INotificationViewItem, action: IAction): Button {
const button = new Button(this.template.actionsContainer);
button.label = action.label;
button.onDidClick(() => {
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
......@@ -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<void>;
dispose(): void;
}
export class NotificationViewItemProgress implements INotificationViewItemProgress {
private _state: INotificationViewItemProgressState;
private _onDidChange: Emitter<void>;
private toDispose: IDisposable[];
constructor() {
this.toDispose = [];
this._state = Object.create(null);
this._onDidChange = new Emitter<void>();
this.toDispose.push(this._onDidChange);
}
public get state(): INotificationViewItemProgressState {
return this._state;
}
public get onDidChange(): Event<void> {
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<void>;
private _onDidDispose: Emitter<void>;
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;
}
......
......@@ -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);
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册