diff --git a/src/vs/platform/statusbar/common/statusbar.ts b/src/vs/platform/statusbar/common/statusbar.ts index caa6286d40e..92a2c0c99f5 100644 --- a/src/vs/platform/statusbar/common/statusbar.ts +++ b/src/vs/platform/statusbar/common/statusbar.ts @@ -67,13 +67,21 @@ export interface IStatusbarService { _serviceBrand: any; /** - * Adds an entry to the statusbar with the given alignment and priority. Use the returned IDisposable - * to remove the statusbar entry. + * Adds an entry to the statusbar with the given alignment and priority. Use the returned accessor + * to update or remove the statusbar entry. */ - addEntry(entry: IStatusbarEntry, alignment: StatusbarAlignment, priority?: number): IDisposable; + addEntry(entry: IStatusbarEntry, alignment: StatusbarAlignment, priority?: number): IStatusbarEntryAccessor; /** * Prints something to the status bar area with optional auto dispose and delay. */ setStatusMessage(message: string, autoDisposeAfter?: number, delayBy?: number): IDisposable; +} + +export interface IStatusbarEntryAccessor extends IDisposable { + + /** + * Allows to update an existing status bar entry. + */ + update(properties: IStatusbarEntry): void; } \ No newline at end of file diff --git a/src/vs/workbench/api/browser/mainThreadStatusBar.ts b/src/vs/workbench/api/browser/mainThreadStatusBar.ts index cc4d8b7bec0..78a093bdb8e 100644 --- a/src/vs/workbench/api/browser/mainThreadStatusBar.ts +++ b/src/vs/workbench/api/browser/mainThreadStatusBar.ts @@ -3,47 +3,55 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment } from 'vs/platform/statusbar/common/statusbar'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment, IStatusbarEntryAccessor } from 'vs/platform/statusbar/common/statusbar'; import { MainThreadStatusBarShape, MainContext, IExtHostContext } from '../common/extHost.protocol'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { dispose } from 'vs/base/common/lifecycle'; @extHostNamedCustomer(MainContext.MainThreadStatusBar) export class MainThreadStatusBar implements MainThreadStatusBarShape { - private readonly _entries: { [id: number]: IDisposable }; + private readonly entries: Map = new Map(); constructor( - extHostContext: IExtHostContext, - @IStatusbarService private readonly _statusbarService: IStatusbarService - ) { - this._entries = Object.create(null); - } + _extHostContext: IExtHostContext, + @IStatusbarService private readonly statusbarService: IStatusbarService + ) { } dispose(): void { - for (const key in this._entries) { - this._entries[key].dispose(); - } + this.entries.forEach(entry => entry.accessor.dispose()); + this.entries.clear(); } $setEntry(id: number, extensionId: ExtensionIdentifier, text: string, tooltip: string, command: string, color: string | ThemeColor, alignment: MainThreadStatusBarAlignment, priority: number): void { + const entry = { text, tooltip, command, color, extensionId }; - // Dispose any old - this.$dispose(id); + // Reset existing entry if alignment or priority changed + let existingEntry = this.entries.get(id); + if (existingEntry && (existingEntry.alignment !== alignment || existingEntry.priority !== priority)) { + dispose(existingEntry.accessor); + this.entries.delete(id); + existingEntry = undefined; + } - // Add new - const entry = this._statusbarService.addEntry({ text, tooltip, command, color, extensionId }, alignment, priority); - this._entries[id] = entry; + // Create new entry if not existing + if (!existingEntry) { + this.entries.set(id, { accessor: this.statusbarService.addEntry(entry, alignment, priority), alignment, priority }); + } + + // Otherwise update + else { + existingEntry.accessor.update(entry); + } } $dispose(id: number) { - const disposeable = this._entries[id]; - if (disposeable) { - disposeable.dispose(); + const entry = this.entries.get(id); + if (entry) { + dispose(entry.accessor); + this.entries.delete(id); } - - delete this._entries[id]; } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts index c3abfe4b909..b121c3ca2f0 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { INotificationsModel, INotificationChangeEvent, NotificationChangeType, INotificationViewItem } from 'vs/workbench/common/notifications'; -import { IStatusbarService, StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar'; -import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IStatusbarService, StatusbarAlignment, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/platform/statusbar/common/statusbar'; +import { Disposable } from 'vs/base/common/lifecycle'; import { HIDE_NOTIFICATIONS_CENTER, SHOW_NOTIFICATIONS_CENTER } from 'vs/workbench/browser/parts/notifications/notificationsCommands'; import { localize } from 'vs/nls'; export class NotificationsStatus extends Disposable { - private statusItem: IDisposable; + private statusItem: IStatusbarEntryAccessor; private isNotificationsCenterVisible: boolean; private _counter: Set; @@ -64,19 +64,18 @@ export class NotificationsStatus extends Disposable { } private updateNotificationsStatusItem(): void { - - // Dispose old first - if (this.statusItem) { - this.statusItem.dispose(); - } - - // Create new - this.statusItem = this.statusbarService.addEntry({ + const statusProperties: IStatusbarEntry = { text: this.count === 0 ? '$(bell)' : `$(bell) ${this.count}`, command: this.isNotificationsCenterVisible ? HIDE_NOTIFICATIONS_CENTER : SHOW_NOTIFICATIONS_CENTER, tooltip: this.getTooltip(), showBeak: this.isNotificationsCenterVisible - }, StatusbarAlignment.RIGHT, -1000 /* towards the far end of the right hand side */); + }; + + if (!this.statusItem) { + this.statusItem = this.statusbarService.addEntry(statusProperties, StatusbarAlignment.RIGHT, -1000 /* towards the far end of the right hand side */); + } else { + this.statusItem.update(statusProperties); + } } private getTooltip(): string { @@ -98,12 +97,4 @@ export class NotificationsStatus extends Disposable { return localize('notifications', "{0} New Notifications", this.count); } - - dispose() { - super.dispose(); - - if (this.statusItem) { - this.statusItem.dispose(); - } - } } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 283366c9d94..40683090984 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -6,16 +6,16 @@ import 'vs/css!./media/statusbarpart'; import * as nls from 'vs/nls'; import { toErrorMessage } from 'vs/base/common/errorMessage'; -import { dispose, IDisposable, toDisposable, combinedDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { OcticonLabel } from 'vs/base/browser/ui/octiconLabel/octiconLabel'; import { Registry } from 'vs/platform/registry/common/platform'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Part } from 'vs/workbench/browser/part'; -import { IStatusbarRegistry, Extensions, IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; +import { IStatusbarRegistry, Extensions } from 'vs/workbench/browser/parts/statusbar/statusbar'; import { IInstantiationService, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { StatusbarAlignment, IStatusbarService, IStatusbarEntry } from 'vs/platform/statusbar/common/statusbar'; +import { StatusbarAlignment, IStatusbarService, IStatusbarEntry, IStatusbarEntryAccessor } from 'vs/platform/statusbar/common/statusbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Action } from 'vs/base/common/actions'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector, ThemeColor } from 'vs/platform/theme/common/themeService'; @@ -24,11 +24,12 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { isThemeColor } from 'vs/editor/common/editorCommon'; import { Color } from 'vs/base/common/color'; -import { addClass, EventHelper, createStyleSheet, addDisposableListener } from 'vs/base/browser/dom'; +import { addClass, EventHelper, createStyleSheet, addDisposableListener, addClasses, clearNode, removeClass } from 'vs/base/browser/dom'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { coalesce } from 'vs/base/common/arrays'; export class StatusbarPart extends Part implements IStatusbarService { @@ -46,10 +47,10 @@ export class StatusbarPart extends Part implements IStatusbarService { //#endregion - private statusMsgDispose: IDisposable; + private statusMessageDispose: IDisposable; private styleElement: HTMLStyleElement; - private pendingEntries: { entry: IStatusbarEntry, alignment: StatusbarAlignment, priority: number, disposable: IDisposable }[] = []; + private pendingEntries: { entry: IStatusbarEntry, alignment: StatusbarAlignment, priority: number, accessor: IStatusbarEntryAccessor }[] = []; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -67,24 +68,30 @@ export class StatusbarPart extends Part implements IStatusbarService { this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles())); } - addEntry(entry: IStatusbarEntry, alignment: StatusbarAlignment, priority: number = 0): IDisposable { + addEntry(entry: IStatusbarEntry, alignment: StatusbarAlignment, priority: number = 0): IStatusbarEntryAccessor { // As long as we have not been created into a container yet, record all entries // that are pending so that they can get created at a later point if (!this.element) { - const pendingEntry = { entry, alignment, priority, disposable: Disposable.None }; + const pendingEntry = { + entry, alignment, priority, accessor: { + update: (entry: IStatusbarEntry) => { + pendingEntry.entry = entry; + }, + dispose: () => { + this.pendingEntries = this.pendingEntries.filter(entry => entry !== pendingEntry); + dispose(pendingEntry.accessor); + } + } + }; this.pendingEntries.push(pendingEntry); - return toDisposable(() => { - this.pendingEntries = this.pendingEntries.filter(e => e !== pendingEntry); - pendingEntry.disposable.dispose(); - }); + return pendingEntry.accessor; } // Render entry in status bar - const el = this.doCreateStatusItem(alignment, priority, entry.showBeak ? 'has-beak' : undefined); - const item = this.instantiationService.createInstance(StatusBarEntryItem, entry); - const toDispose = item.render(el); + const el = this.doCreateStatusItem(alignment, priority, ...coalesce(['statusbar-entry', entry.showBeak ? 'has-beak' : undefined])); + const item = this.instantiationService.createInstance(StatusBarEntryItem, el, entry); // Insert according to priority const container = this.element; @@ -106,13 +113,24 @@ export class StatusbarPart extends Part implements IStatusbarService { container.appendChild(el); } - return toDisposable(() => { - el.remove(); + return { + update: entry => { - if (toDispose) { - toDispose.dispose(); + // Update beak + if (entry.showBeak) { + addClass(el, 'has-beak'); + } else { + removeClass(el, 'has-beak'); + } + + // Update entry + item.update(entry); + }, + dispose: () => { + el.remove(); + dispose(item); } - }); + }; } private getEntries(alignment: StatusbarAlignment): HTMLElement[] { @@ -164,7 +182,7 @@ export class StatusbarPart extends Part implements IStatusbarService { while (this.pendingEntries.length) { const entry = this.pendingEntries.shift(); if (entry) { - entry.disposable = this.addEntry(entry.entry, entry.alignment, entry.priority); + entry.accessor = this.addEntry(entry.entry, entry.alignment, entry.priority); } } @@ -195,11 +213,11 @@ export class StatusbarPart extends Part implements IStatusbarService { this.styleElement.innerHTML = `.monaco-workbench .part.statusbar > .statusbar-item.has-beak:before { border-bottom-color: ${backgroundColor}; }`; } - private doCreateStatusItem(alignment: StatusbarAlignment, priority: number = 0, extraClass?: string): HTMLElement { + private doCreateStatusItem(alignment: StatusbarAlignment, priority: number = 0, ...extraClasses: string[]): HTMLElement { const el = document.createElement('div'); addClass(el, 'statusbar-item'); - if (extraClass) { - addClass(el, extraClass); + if (extraClasses) { + addClasses(el, ...extraClasses); } if (alignment === StatusbarAlignment.RIGHT) { @@ -215,20 +233,20 @@ export class StatusbarPart extends Part implements IStatusbarService { } setStatusMessage(message: string, autoDisposeAfter: number = -1, delayBy: number = 0): IDisposable { - if (this.statusMsgDispose) { - this.statusMsgDispose.dispose(); // dismiss any previous - } + + // Dismiss any previous + dispose(this.statusMessageDispose); // Create new - let statusDispose: IDisposable; + let statusMessageEntry: IStatusbarEntryAccessor; let showHandle: any = setTimeout(() => { - statusDispose = this.addEntry({ text: message }, StatusbarAlignment.LEFT, -Number.MAX_VALUE /* far right on left hand side */); + statusMessageEntry = this.addEntry({ text: message }, StatusbarAlignment.LEFT, -Number.MAX_VALUE /* far right on left hand side */); showHandle = null; }, delayBy); let hideHandle: any; // Dispose function takes care of timeouts and actual entry - const dispose = { + const statusMessageDispose = { dispose: () => { if (showHandle) { clearTimeout(showHandle); @@ -238,18 +256,18 @@ export class StatusbarPart extends Part implements IStatusbarService { clearTimeout(hideHandle); } - if (statusDispose) { - statusDispose.dispose(); + if (statusMessageEntry) { + statusMessageEntry.dispose(); } } }; - this.statusMsgDispose = dispose; + this.statusMessageDispose = statusMessageDispose; if (typeof autoDisposeAfter === 'number' && autoDisposeAfter > 0) { - hideHandle = setTimeout(() => dispose.dispose(), autoDisposeAfter); + hideHandle = setTimeout(() => statusMessageDispose.dispose(), autoDisposeAfter); } - return dispose; + return statusMessageDispose; } layout(width: number, height: number): void { @@ -264,10 +282,12 @@ export class StatusbarPart extends Part implements IStatusbarService { } let manageExtensionAction: ManageExtensionAction; -class StatusBarEntryItem implements IStatusbarItem { +class StatusBarEntryItem extends Disposable { + private entryDisposables: IDisposable[] = []; constructor( - private entry: IStatusbarEntry, + private container: HTMLElement, + entry: IStatusbarEntry, @ICommandService private readonly commandService: ICommandService, @IInstantiationService private readonly instantiationService: IInstantiationService, @INotificationService private readonly notificationService: INotificationService, @@ -276,78 +296,80 @@ class StatusBarEntryItem implements IStatusbarItem { @IEditorService private readonly editorService: IEditorService, @IThemeService private readonly themeService: IThemeService ) { - this.entry = entry; + super(); if (!manageExtensionAction) { manageExtensionAction = this.instantiationService.createInstance(ManageExtensionAction); } + + this.render(entry); } - render(el: HTMLElement): IDisposable { - let toDispose: IDisposable[] = []; - addClass(el, 'statusbar-entry'); + update(entry: IStatusbarEntry): void { + clearNode(this.container); + this.entryDisposables = dispose(this.entryDisposables); + + this.render(entry); + } + + private render(entry: IStatusbarEntry): void { // Text Container let textContainer: HTMLElement; - if (this.entry.command) { + if (entry.command) { textContainer = document.createElement('a'); - toDispose.push(addDisposableListener(textContainer, 'click', () => this.executeCommand(this.entry.command!, this.entry.arguments))); + this.entryDisposables.push((addDisposableListener(textContainer, 'click', () => this.executeCommand(entry.command!, entry.arguments)))); } else { textContainer = document.createElement('span'); } // Label - new OcticonLabel(textContainer).text = this.entry.text; + new OcticonLabel(textContainer).text = entry.text; // Tooltip - if (this.entry.tooltip) { - textContainer.title = this.entry.tooltip; + if (entry.tooltip) { + textContainer.title = entry.tooltip; } // Color (only applies to text container) - toDispose.push(this.applyColor(textContainer, this.entry.color)); + this.applyColor(textContainer, entry.color); // Background Color (applies to parent element to fully fill container) - if (this.entry.backgroundColor) { - toDispose.push(this.applyColor(el, this.entry.backgroundColor, true)); - addClass(el, 'has-background-color'); + if (entry.backgroundColor) { + this.applyColor(this.container, entry.backgroundColor, true); + addClass(this.container, 'has-background-color'); } // Context Menu - if (this.entry.extensionId) { - toDispose.push(addDisposableListener(textContainer, 'contextmenu', e => { + if (entry.extensionId) { + this.entryDisposables.push((addDisposableListener(textContainer, 'contextmenu', e => { EventHelper.stop(e, true); this.contextMenuService.showContextMenu({ - getAnchor: () => el, - getActionsContext: () => this.entry.extensionId!.value, + getAnchor: () => this.container, + getActionsContext: () => entry.extensionId!.value, getActions: () => [manageExtensionAction] }); - })); + }))); } - el.appendChild(textContainer); - - return toDisposable(() => toDispose = dispose(toDispose)); + this.container.appendChild(textContainer); } - private applyColor(container: HTMLElement, color: string | ThemeColor | undefined, isBackground?: boolean): IDisposable { - const disposable: IDisposable[] = []; - + private applyColor(container: HTMLElement, color: string | ThemeColor | undefined, isBackground?: boolean): void { if (color) { if (isThemeColor(color)) { const colorId = color.id; color = (this.themeService.getTheme().getColor(colorId) || Color.transparent).toString(); - disposable.push(this.themeService.onThemeChange(theme => { + this.entryDisposables.push(((this.themeService.onThemeChange(theme => { const colorValue = (theme.getColor(colorId) || Color.transparent).toString(); isBackground ? container.style.backgroundColor = colorValue : container.style.color = colorValue; - })); + })))); } + isBackground ? container.style.backgroundColor = color : container.style.color = color; } - - return combinedDisposable(disposable); } private executeCommand(id: string, args?: unknown[]) { @@ -368,6 +390,12 @@ class StatusBarEntryItem implements IStatusbarItem { this.telemetryService.publicLog('workbenchActionExecuted', { id, from: 'status bar' }); this.commandService.executeCommand(id, ...args).then(undefined, err => this.notificationService.error(toErrorMessage(err))); } + + dispose(): void { + super.dispose(); + + this.entryDisposables = dispose(this.entryDisposables); + } } class ManageExtensionAction extends Action { diff --git a/src/vs/workbench/contrib/tasks/common/media/task.contribution.css b/src/vs/workbench/contrib/tasks/common/media/task.contribution.css index 05c596a162b..ce012591ac4 100644 --- a/src/vs/workbench/contrib/tasks/common/media/task.contribution.css +++ b/src/vs/workbench/contrib/tasks/common/media/task.contribution.css @@ -43,6 +43,7 @@ .task-statusbar-item-label { display: inline-block; cursor: pointer; + padding: 0 5px 0 5px; } .task-statusbar-item-label > .task-statusbar-item-label-counter {