diff --git a/src/vs/platform/statusbar/common/statusbar.ts b/src/vs/platform/statusbar/common/statusbar.ts index f83ca77e1edac4d8af8a326f91c47f242512e6a2..710cf3fde026475fb0edf74155f0da612ac816c4 100644 --- a/src/vs/platform/statusbar/common/statusbar.ts +++ b/src/vs/platform/statusbar/common/statusbar.ts @@ -11,7 +11,13 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export const IStatusbarService = createDecorator('statusbarService'); export const enum StatusbarAlignment { - LEFT, RIGHT + LEFT, + RIGHT +} + +export interface IStatusbarEntryCategory { + id: string; + label: string; } /** @@ -19,6 +25,11 @@ export const enum StatusbarAlignment { */ export interface IStatusbarEntry { + /** + * The category of the entry is needed to allow users to hide entries via settings. + */ + readonly category: IStatusbarEntryCategory; + /** * The text to show for the entry. You can embed icons in the text by leveraging the syntax: * diff --git a/src/vs/workbench/api/browser/mainThreadStatusBar.ts b/src/vs/workbench/api/browser/mainThreadStatusBar.ts index 78a093bdb8ecced3fc19e090befd423cbca50691..dca3f1406008deaa479d3a76d112e6e1e178f24c 100644 --- a/src/vs/workbench/api/browser/mainThreadStatusBar.ts +++ b/src/vs/workbench/api/browser/mainThreadStatusBar.ts @@ -3,12 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment, IStatusbarEntryAccessor } from 'vs/platform/statusbar/common/statusbar'; +import { IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment, IStatusbarEntryAccessor, IStatusbarEntry } 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 { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { dispose } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; @extHostNamedCustomer(MainContext.MainThreadStatusBar) export class MainThreadStatusBar implements MainThreadStatusBarShape { @@ -25,8 +26,18 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { 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 }; + $setEntry(id: number, extension: IExtensionDescription, text: string, tooltip: string, command: string, color: string | ThemeColor, alignment: MainThreadStatusBarAlignment, priority: number): void { + const entry: IStatusbarEntry = { + category: { + id: extension.identifier.value, + label: localize('extensionLabel', "{0} (Extension)", extension.displayName || extension.name) + }, + text, + tooltip, + command, + color, + extensionId: extension.identifier + }; // Reset existing entry if alignment or priority changed let existingEntry = this.entries.get(id); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 06160349ea4dfaa41a036972862473ceb9312853..0deaddaa6c4c848746b3a9409e688a8e3fab3742 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -495,7 +495,7 @@ export interface MainThreadQuickOpenShape extends IDisposable { } export interface MainThreadStatusBarShape extends IDisposable { - $setEntry(id: number, extensionId: ExtensionIdentifier | undefined, text: string, tooltip: string, command: string, color: string | ThemeColor, alignment: statusbar.StatusbarAlignment, priority: number | undefined): void; + $setEntry(id: number, extension: IExtensionDescription | undefined, text: string, tooltip: string, command: string, color: string | ThemeColor, alignment: statusbar.StatusbarAlignment, priority: number | undefined): void; $dispose(id: number): void; } diff --git a/src/vs/workbench/api/common/extHostStatusBar.ts b/src/vs/workbench/api/common/extHostStatusBar.ts index bc7215c0fe94529658e7dc6c1a8774e75feb7b99..4e841b752f550e11b5aab4bed20ba9cf9786259a 100644 --- a/src/vs/workbench/api/common/extHostStatusBar.ts +++ b/src/vs/workbench/api/common/extHostStatusBar.ts @@ -7,7 +7,7 @@ import { StatusbarAlignment as MainThreadStatusBarAlignment } from 'vs/platform/ import { StatusBarAlignment as ExtHostStatusBarAlignment, Disposable, ThemeColor } from './extHostTypes'; import { StatusBarItem, StatusBarAlignment } from 'vscode'; import { MainContext, MainThreadStatusBarShape, IMainContext } from './extHost.protocol'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; export class ExtHostStatusBarEntry implements StatusBarItem { private static ID_GEN = 0; @@ -26,14 +26,14 @@ export class ExtHostStatusBarEntry implements StatusBarItem { private _timeoutHandle: any; private _proxy: MainThreadStatusBarShape; - private _extensionId?: ExtensionIdentifier; + private _extension?: IExtensionDescription; - constructor(proxy: MainThreadStatusBarShape, extensionId: ExtensionIdentifier | undefined, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number) { + constructor(proxy: MainThreadStatusBarShape, extension: IExtensionDescription | undefined, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number) { this._id = ExtHostStatusBarEntry.ID_GEN++; this._proxy = proxy; this._alignment = alignment; this._priority = priority; - this._extensionId = extensionId; + this._extension = extension; } public get id(): number { @@ -107,7 +107,7 @@ export class ExtHostStatusBarEntry implements StatusBarItem { this._timeoutHandle = undefined; // Set to status bar - this._proxy.$setEntry(this.id, this._extensionId, this.text, this.tooltip, this.command, this.color, + this._proxy.$setEntry(this.id, this._extension, this.text, this.tooltip, this.command, this.color, this._alignment === ExtHostStatusBarAlignment.Left ? MainThreadStatusBarAlignment.LEFT : MainThreadStatusBarAlignment.RIGHT, this._priority); }, 0); @@ -167,8 +167,8 @@ export class ExtHostStatusBar { this._statusMessage = new StatusBarMessage(this); } - createStatusBarEntry(extensionId: ExtensionIdentifier | undefined, alignment?: ExtHostStatusBarAlignment, priority?: number): StatusBarItem { - return new ExtHostStatusBarEntry(this._proxy, extensionId, alignment, priority); + createStatusBarEntry(extension: IExtensionDescription | undefined, alignment?: ExtHostStatusBarAlignment, priority?: number): StatusBarItem { + return new ExtHostStatusBarEntry(this._proxy, extension, alignment, priority); } setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): Disposable { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 18bbd444eb086f170788e247865443f7d54df99b..9d7b6d53f9e336ced056157337d1bac76799fa26 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -466,7 +466,7 @@ export function createApiFactory( return extHostDialogs.showSaveDialog(options); }, createStatusBarItem(position?: vscode.StatusBarAlignment, priority?: number): vscode.StatusBarItem { - return extHostStatusBar.createStatusBarEntry(extension.identifier, position, priority); + return extHostStatusBar.createStatusBarEntry(extension, position, priority); }, setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): vscode.Disposable { return extHostStatusBar.setStatusBarMessage(text, timeoutOrThenable); diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index 29c0e390acbb1e3613030d8c48641e3541d2c333..3a7d965ea56bc70c8991b67f595adc69cfdbf488 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -221,10 +221,20 @@ registerEditorContribution(OpenWorkspaceButtonContribution); // Register Editor Status const statusBar = Registry.as(StatusExtensions.Statusbar); -statusBar.registerStatusbarItem(new StatusbarItemDescriptor(EditorStatus, StatusbarAlignment.RIGHT, 100 /* towards the left of the right hand side */)); +statusBar.registerStatusbarItem(new StatusbarItemDescriptor( + EditorStatus, + { id: 'status.editor', label: nls.localize('status.editor', "Editor Status") }, + StatusbarAlignment.RIGHT, + 100 /* towards the left of the right hand side */ +)); // Register Zoom Status -statusBar.registerStatusbarItem(new StatusbarItemDescriptor(ZoomStatusbarItem, StatusbarAlignment.RIGHT, 101 /* to the left of editor status (100) */)); +statusBar.registerStatusbarItem(new StatusbarItemDescriptor( + ZoomStatusbarItem, + { id: 'status.imageZoom', label: nls.localize('status.imageZoom', "Image Zoom") }, + StatusbarAlignment.RIGHT, + 101 /* to the left of editor status (100) */) +); // Register Status Actions const registry = Registry.as(ActionExtensions.WorkbenchActions); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts index b9d11dbd12b2cb8738c0fbaf954cc36ecb46f058..efda5fd80e3b9a85e84f676756c09f14583b1c41 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsStatus.ts @@ -58,6 +58,7 @@ export class NotificationsStatus extends Disposable { private updateNotificationsCenterStatusItem(): void { const statusProperties: IStatusbarEntry = { + category: { id: 'status.notifications', label: localize('status.notifications', "Notifictions") }, text: this.currentNotifications.size === 0 ? '$(bell)' : `$(bell) ${this.currentNotifications.size}`, command: this.isNotificationsCenterVisible ? HIDE_NOTIFICATIONS_CENTER : SHOW_NOTIFICATIONS_CENTER, tooltip: this.getTooltip(), @@ -137,7 +138,10 @@ export class NotificationsStatus extends Disposable { // Create new let statusMessageEntry: IStatusbarEntryAccessor; let showHandle: any = setTimeout(() => { - statusMessageEntry = this.statusbarService.addEntry({ text: message }, StatusbarAlignment.LEFT, -Number.MAX_VALUE /* far right on left hand side */); + statusMessageEntry = this.statusbarService.addEntry({ + category: { id: 'status.message', label: localize('status.message', "Status Message") }, + text: message + }, StatusbarAlignment.LEFT, -Number.MAX_VALUE /* far right on left hand side */); showHandle = null; }, showAfter); diff --git a/src/vs/workbench/browser/parts/statusbar/statusbar.ts b/src/vs/workbench/browser/parts/statusbar/statusbar.ts index 4c44b2f6479f58928575f97a8b8f5834bdbb513e..d1f753e4cdd12661fb868ada1cae7699b1d0f894 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbar.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbar.ts @@ -5,7 +5,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { StatusbarAlignment } from 'vs/platform/statusbar/common/statusbar'; +import { StatusbarAlignment, IStatusbarEntryCategory } from 'vs/platform/statusbar/common/statusbar'; import { SyncDescriptor0, createSyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IConstructorSignature0 } from 'vs/platform/instantiation/common/instantiation'; @@ -14,11 +14,18 @@ export interface IStatusbarItem { } export class StatusbarItemDescriptor { - syncDescriptor: SyncDescriptor0; - alignment: StatusbarAlignment; - priority: number; - - constructor(ctor: IConstructorSignature0, alignment?: StatusbarAlignment, priority?: number) { + readonly syncDescriptor: SyncDescriptor0; + readonly category: IStatusbarEntryCategory; + readonly alignment: StatusbarAlignment; + readonly priority: number; + + constructor( + ctor: IConstructorSignature0, + category: IStatusbarEntryCategory, + alignment?: StatusbarAlignment, + priority?: number + ) { + this.category = category; this.syncDescriptor = createSyncDescriptor(ctor); this.alignment = alignment || StatusbarAlignment.LEFT; this.priority = priority || 0; @@ -26,21 +33,16 @@ export class StatusbarItemDescriptor { } export interface IStatusbarRegistry { + + readonly items: StatusbarItemDescriptor[]; + registerStatusbarItem(descriptor: StatusbarItemDescriptor): void; - items: StatusbarItemDescriptor[]; } class StatusbarRegistry implements IStatusbarRegistry { - private _items: StatusbarItemDescriptor[]; - - constructor() { - this._items = []; - } - - get items(): StatusbarItemDescriptor[] { - return this._items; - } + private readonly _items: StatusbarItemDescriptor[] = []; + get items(): StatusbarItemDescriptor[] { return this._items; } registerStatusbarItem(descriptor: StatusbarItemDescriptor): void { this._items.push(descriptor); diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index bbda9af04014f17275a0c009a6c986c4ae0f3835..cbc2a350f1999c6c17709cb0536f105714e945c3 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -15,7 +15,7 @@ import { Part } from 'vs/workbench/browser/part'; 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, IStatusbarEntryAccessor } from 'vs/platform/statusbar/common/statusbar'; +import { StatusbarAlignment, IStatusbarService, IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarEntryCategory } from 'vs/platform/statusbar/common/statusbar'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Action, IAction } from 'vs/base/common/actions'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector, ThemeColor } from 'vs/platform/theme/common/themeService'; @@ -24,23 +24,208 @@ 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, addClasses, clearNode, removeClass, EventType } from 'vs/base/browser/dom'; +import { addClass, EventHelper, createStyleSheet, addDisposableListener, addClasses, clearNode, removeClass, EventType, hide, show } from 'vs/base/browser/dom'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } 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'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ToggleStatusbarVisibilityAction } from 'vs/workbench/browser/actions/layoutActions'; +import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Event, Emitter } from 'vs/base/common/event'; +import { values } from 'vs/base/common/map'; + +interface IPendingStatusbarEntry { + entry: IStatusbarEntry; + alignment: StatusbarAlignment; + priority: number; + accessor?: IStatusbarEntryAccessor; +} + +interface IStatusbarViewModelItem { + category: IStatusbarEntryCategory; + alignment: StatusbarAlignment; + priority: number; +} + +class StatusbarViewModel extends Disposable { + + private static readonly HIDDEN_CATEGORIES_KEY = 'workbench.statusbar.hidden'; + + private readonly _items: IStatusbarViewModelItem[] = []; + get items(): IStatusbarViewModelItem[] { return this._items; } + + private readonly _onDidCategoryVisibilityChange: Emitter = this._register(new Emitter()); + get onDidCategoryVisibilityChange(): Event { return this._onDidCategoryVisibilityChange.event; } + + private hiddenCategories: Set; + + constructor(private storageService: IStorageService) { + super(); + + this.restoreState(); + this.registerListeners(); + } + + private restoreState(): void { + const hiddenCategoriesRaw = this.storageService.get(StatusbarViewModel.HIDDEN_CATEGORIES_KEY, StorageScope.GLOBAL); + if (hiddenCategoriesRaw) { + try { + const hiddenCategoriesArray: string[] = JSON.parse(hiddenCategoriesRaw); + this.hiddenCategories = new Set(hiddenCategoriesArray); + } catch (error) { + // ignore parsing errors + } + } + + if (!this.hiddenCategories) { + this.hiddenCategories = new Set(); + } + } + + private registerListeners(): void { + this._register(this.storageService.onDidChangeStorage(e => this.onDidStorageChange(e))); + } + + private onDidStorageChange(event: IWorkspaceStorageChangeEvent): void { + if (event.key === StatusbarViewModel.HIDDEN_CATEGORIES_KEY && event.scope === StorageScope.GLOBAL) { + + // Keep current hidden categories + const currentHiddenCategories = this.hiddenCategories; + + // Load latest state of hidden categories + this.restoreState(); + + const changedCategories = new Set(); + + // Check for each category that is now visible + currentHiddenCategories.forEach(category => { + if (!this.hiddenCategories.has(category)) { + changedCategories.add(category); + } + }); + + // Check for each category that is now hidden + this.hiddenCategories.forEach(category => { + if (!currentHiddenCategories.has(category)) { + changedCategories.add(category); + } + }); + + // Notify listeners that visibility for categories that changed + this._items.forEach(item => { + if (changedCategories.has(item.category.id)) { + this._onDidCategoryVisibilityChange.fire(item.category); + + changedCategories.delete(item.category.id); + } + }); + } + } + + add(item: IStatusbarViewModelItem): void { + this._items.push(item); + this.sort(); + } + + remove(item: IStatusbarViewModelItem): void { + const index = this._items.indexOf(item); + if (index >= 0) { + this._items.splice(index, 1); + } + } + + isHidden(category: IStatusbarEntryCategory): boolean { + return this.hiddenCategories.has(category.id); + } + + hide(category: IStatusbarEntryCategory): void { + if (!this.hiddenCategories.has(category.id)) { + this.hiddenCategories.add(category.id); + + this._onDidCategoryVisibilityChange.fire(category); + + this.saveState(); + } + } + + show(category: IStatusbarEntryCategory): void { + if (this.hiddenCategories.has(category.id)) { + this.hiddenCategories.delete(category.id); + + this._onDidCategoryVisibilityChange.fire(category); + + this.saveState(); + } + } + + private saveState(): void { + if (this.hiddenCategories.size > 0) { + this.storageService.store(StatusbarViewModel.HIDDEN_CATEGORIES_KEY, JSON.stringify(values(this.hiddenCategories)), StorageScope.GLOBAL); + } else { + this.storageService.remove(StatusbarViewModel.HIDDEN_CATEGORIES_KEY, StorageScope.GLOBAL); + } + } + + private sort(): void { + this._items.sort((itemA, itemB) => { + if (itemA.alignment === itemB.alignment) { + return itemB.priority - itemA.priority; + } + + if (itemA.alignment === StatusbarAlignment.LEFT) { + return -1; + } + + if (itemB.alignment === StatusbarAlignment.LEFT) { + return 1; + } + + return 0; + }); + } +} + +class ToggleStatusCategoryVisibilityAction extends Action { + + constructor(private category: IStatusbarEntryCategory, private model: StatusbarViewModel) { + super(category.id, category.label, undefined, true); + + this.checked = !model.isHidden(category); + } + + run(): Promise { + if (this.model.isHidden(this.category)) { + this.model.show(this.category); + } else { + this.model.hide(this.category); + } + + return Promise.resolve(true); + } +} + +class HideStatusCategoryAction extends Action { + + constructor(private category: IStatusbarEntryCategory, private model: StatusbarViewModel) { + super(category.id, nls.localize('hide', "Hide"), undefined, true); + } -interface PendingEntry { entry: IStatusbarEntry; alignment: StatusbarAlignment; priority: number; accessor?: IStatusbarEntryAccessor; } + run(): Promise { + this.model.hide(this.category); + + return Promise.resolve(true); + } +} export class StatusbarPart extends Part implements IStatusbarService { _serviceBrand: ServiceIdentifier; - private static readonly PRIORITY_PROP = 'statusbar-entry-priority'; - private static readonly ALIGNMENT_PROP = 'statusbar-entry-alignment'; + private static readonly PRIORITY_PROP = 'statusbar-item-priority'; + private static readonly ALIGNMENT_PROP = 'statusbar-item-alignment'; + private static readonly CATEGORY_PROP = 'statusbar-item-category'; //#region IView @@ -53,9 +238,9 @@ export class StatusbarPart extends Part implements IStatusbarService { private styleElement: HTMLStyleElement; - private pendingEntries: PendingEntry[] = []; + private pendingEntries: IPendingStatusbarEntry[] = []; - private hideStatusBarAction: ToggleStatusbarVisibilityAction; + private readonly viewModel: StatusbarViewModel; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -67,13 +252,27 @@ export class StatusbarPart extends Part implements IStatusbarService { ) { super(Parts.STATUSBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); - this.hideStatusBarAction = this._register(this.instantiationService.createInstance(ToggleStatusbarVisibilityAction, ToggleStatusbarVisibilityAction.ID, nls.localize('hideStatusBar', "Hide Status Bar"))); + this.viewModel = this._register(new StatusbarViewModel(storageService)); this.registerListeners(); } private registerListeners(): void { this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles())); + this._register(this.viewModel.onDidCategoryVisibilityChange(category => this.onDidCategoryVisibilityChange(category))); + } + + private onDidCategoryVisibilityChange(category: IStatusbarEntryCategory): void { + const isHidden = this.viewModel.isHidden(category); + + const items = this.getEntries(category); + items.forEach(item => { + if (isHidden) { + hide(item); + } else { + show(item); + } + }); } addEntry(entry: IStatusbarEntry, alignment: StatusbarAlignment, priority: number = 0): IStatusbarEntryAccessor { @@ -81,33 +280,47 @@ export class StatusbarPart extends Part implements IStatusbarService { // 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: PendingEntry = { - entry, alignment, priority - }; - this.pendingEntries.push(pendingEntry); - - const accessor: IStatusbarEntryAccessor = { - update: (entry: IStatusbarEntry) => { - if (pendingEntry.accessor) { - pendingEntry.accessor.update(entry); - } else { - pendingEntry.entry = entry; - } - }, - dispose: () => { - if (pendingEntry.accessor) { - pendingEntry.accessor.dispose(); - } else { - this.pendingEntries = this.pendingEntries.filter(entry => entry !== pendingEntry); - } - } - }; - return accessor; + return this.doAddPendingEntry(entry, alignment, priority); } + // Otherwise add to view + return this.doAddEntry(entry, alignment, priority); + } + + private doAddPendingEntry(entry: IStatusbarEntry, alignment: StatusbarAlignment, priority: number): IStatusbarEntryAccessor { + const pendingEntry: IPendingStatusbarEntry = { entry, alignment, priority }; + this.pendingEntries.push(pendingEntry); + + const accessor: IStatusbarEntryAccessor = { + update: (entry: IStatusbarEntry) => { + if (pendingEntry.accessor) { + pendingEntry.accessor.update(entry); + } else { + pendingEntry.entry = entry; + } + }, + + dispose: () => { + if (pendingEntry.accessor) { + pendingEntry.accessor.dispose(); + } else { + this.pendingEntries = this.pendingEntries.filter(entry => entry !== pendingEntry); + } + } + }; + + return accessor; + } + + private doAddEntry(entry: IStatusbarEntry, alignment: StatusbarAlignment, priority: number): IStatusbarEntryAccessor { + + // Add to view model + const viewModelItem: IStatusbarViewModelItem = { category: entry.category, alignment, priority }; + this.viewModel.add(viewModelItem); + // Render entry in status bar - const el = this.doCreateStatusItem(alignment, priority, ...coalesce(['statusbar-entry', entry.showBeak ? 'has-beak' : undefined])); - const item = this.instantiationService.createInstance(StatusBarEntryItem, el, entry); + const itemContainer = this.doCreateStatusItem(entry.category, alignment, priority, ...coalesce(['statusbar-entry', entry.showBeak ? 'has-beak' : undefined])); + const item = this.instantiationService.createInstance(StatusbarEntryItem, itemContainer, entry); // Insert according to priority const container = this.element; @@ -119,14 +332,14 @@ export class StatusbarPart extends Part implements IStatusbarService { alignment === StatusbarAlignment.LEFT && nPriority < priority || alignment === StatusbarAlignment.RIGHT && nPriority > priority ) { - container.insertBefore(el, neighbour); + container.insertBefore(itemContainer, neighbour); inserted = true; break; } } if (!inserted) { - container.appendChild(el); + container.appendChild(itemContainer); } return { @@ -134,30 +347,42 @@ export class StatusbarPart extends Part implements IStatusbarService { // Update beak if (entry.showBeak) { - addClass(el, 'has-beak'); + addClass(itemContainer, 'has-beak'); } else { - removeClass(el, 'has-beak'); + removeClass(itemContainer, 'has-beak'); } // Update entry item.update(entry); }, dispose: () => { - el.remove(); + this.viewModel.remove(viewModelItem); + itemContainer.remove(); dispose(item); } }; } - private getEntries(alignment: StatusbarAlignment): HTMLElement[] { + private getEntries(scope: StatusbarAlignment | IStatusbarEntryCategory): HTMLElement[] { const entries: HTMLElement[] = []; const container = this.element; const children = container.children; for (let i = 0; i < children.length; i++) { const childElement = children.item(i); - if (Number(childElement.getAttribute(StatusbarPart.ALIGNMENT_PROP)) === alignment) { - entries.push(childElement); + + // By alignment + if (typeof scope === 'number') { + if (Number(childElement.getAttribute(StatusbarPart.ALIGNMENT_PROP)) === scope) { + entries.push(childElement); + } + } + + // By category + else { + if (childElement.getAttribute(StatusbarPart.CATEGORY_PROP) === scope.id) { + entries.push(childElement); + } } } @@ -170,31 +395,48 @@ export class StatusbarPart extends Part implements IStatusbarService { // Context menu support this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(e))); - // Fill in initial items that were contributed from the registry + // Initial status bar entries + this.createInitialStatusEntries(); + + return this.element; + } + + private createInitialStatusEntries(): void { const registry = Registry.as(Extensions.Statusbar); - const descriptors = registry.items.slice().sort((a, b) => { - if (a.alignment === b.alignment) { - if (a.alignment === StatusbarAlignment.LEFT) { - return b.priority - a.priority; - } else { - return a.priority - b.priority; + const descriptors = registry.items.slice().sort((itemA, itemB) => { + if (itemA.alignment === itemB.alignment) { + if (itemA.alignment === StatusbarAlignment.LEFT) { + return itemB.priority - itemA.priority; } - } else if (a.alignment === StatusbarAlignment.LEFT) { + + return itemA.priority - itemB.priority; + } + + if (itemA.alignment === StatusbarAlignment.LEFT) { return 1; - } else if (a.alignment === StatusbarAlignment.RIGHT) { + } + + if (itemA.alignment === StatusbarAlignment.RIGHT) { return -1; - } else { - return 0; } + + return 0; }); - for (const descriptor of descriptors) { - const item = this.instantiationService.createInstance(descriptor.syncDescriptor); - const el = this.doCreateStatusItem(descriptor.alignment, descriptor.priority); + // Fill in initial items that were contributed from the registry + for (const { category, alignment, priority, syncDescriptor } of descriptors) { + + // Add to view model + const viewModelItem: IStatusbarViewModelItem = { category, alignment, priority }; + this.viewModel.add(viewModelItem); - this._register(item.render(el)); - this.element.appendChild(el); + // Render + const item = this.instantiationService.createInstance(syncDescriptor); + const itemContainer = this.doCreateStatusItem(category, alignment, priority); + + this._register(item.render(itemContainer)); + this.element.appendChild(itemContainer); } // Fill in pending entries if any @@ -204,39 +446,57 @@ export class StatusbarPart extends Part implements IStatusbarService { entry.accessor = this.addEntry(entry.entry, entry.alignment, entry.priority); } } - - return this.element; } private showContextMenu(e: MouseEvent): void { EventHelper.stop(e, true); const event = new StandardMouseEvent(e); + + let actions: IAction[] | undefined = undefined; this.contextMenuService.showContextMenu({ - getAnchor: () => { return { x: event.posx, y: event.posy }; }, - getActions: () => this.getContextMenuActions() + getAnchor: () => ({ x: event.posx, y: event.posy }), + getActions: () => { + actions = this.getContextMenuActions(event); + + return actions; + }, + onHide: () => { + if (actions) { + dispose(actions); + } + } }); } - private getContextMenuActions(): IAction[] { - const actions: IAction[] = []; + private getContextMenuActions(event: StandardMouseEvent): IAction[] { + const actions: Action[] = []; + + // Figure out if mouse is over an entry + let categoryUnderMouse: IStatusbarEntryCategory | undefined = undefined; + for (let element: HTMLElement | null = event.target; element; element = element.parentElement) { + if (element.hasAttribute(StatusbarPart.CATEGORY_PROP)) { + categoryUnderMouse = { id: element.getAttribute(StatusbarPart.CATEGORY_PROP)!, label: element.title }; + break; + } + } + + if (categoryUnderMouse) { + actions.push(new HideStatusCategoryAction(categoryUnderMouse, this.viewModel)); + actions.push(new Separator()); + } - // TODO@Ben collect more context menu actions - // .map(({ id, name, activityAction }) => ({ - // id, - // label: name || id, - // checked: this.isPinned(id), - // run: () => { - // if (this.isPinned(id)) { - // this.unpin(id); - // } else { - // this.pin(id, true); - // } - // } - // })); - // actions.push(new Separator()); + // Show an entry per known status item category + const handledCategories = new Set(); + this.viewModel.items.forEach(item => { + if (!handledCategories.has(item.category.id)) { + actions.push(new ToggleStatusCategoryVisibilityAction(item.category, this.viewModel)); + } + }); - actions.push(this.hideStatusBarAction); + // Provide an action to hide the status bar at last + actions.push(new Separator()); + actions.push(this.instantiationService.createInstance(ToggleStatusbarVisibilityAction, ToggleStatusbarVisibilityAction.ID, nls.localize('hideStatusBar', "Hide Status Bar"))); return actions; } @@ -265,23 +525,30 @@ 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, ...extraClasses: string[]): HTMLElement { - const el = document.createElement('div'); - addClass(el, 'statusbar-item'); + private doCreateStatusItem(category: IStatusbarEntryCategory, alignment: StatusbarAlignment, priority: number = 0, ...extraClasses: string[]): HTMLElement { + const itemContainer = document.createElement('div'); + itemContainer.title = category.label; + + addClass(itemContainer, 'statusbar-item'); if (extraClasses) { - addClasses(el, ...extraClasses); + addClasses(itemContainer, ...extraClasses); } if (alignment === StatusbarAlignment.RIGHT) { - addClass(el, 'right'); + addClass(itemContainer, 'right'); } else { - addClass(el, 'left'); + addClass(itemContainer, 'left'); } - el.setAttribute(StatusbarPart.PRIORITY_PROP, String(priority)); - el.setAttribute(StatusbarPart.ALIGNMENT_PROP, String(alignment)); + itemContainer.setAttribute(StatusbarPart.PRIORITY_PROP, String(priority)); + itemContainer.setAttribute(StatusbarPart.ALIGNMENT_PROP, String(alignment)); + itemContainer.setAttribute(StatusbarPart.CATEGORY_PROP, category.id); - return el; + if (this.viewModel.isHidden(category)) { + hide(itemContainer); + } + + return itemContainer; } layout(width: number, height: number): void { @@ -295,27 +562,20 @@ export class StatusbarPart extends Part implements IStatusbarService { } } -let manageExtensionAction: ManageExtensionAction; -class StatusBarEntryItem extends Disposable { +class StatusbarEntryItem extends Disposable { private entryDisposables: IDisposable[] = []; constructor( private container: HTMLElement, entry: IStatusbarEntry, @ICommandService private readonly commandService: ICommandService, - @IInstantiationService private readonly instantiationService: IInstantiationService, @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IContextMenuService private readonly contextMenuService: IContextMenuService, @IEditorService private readonly editorService: IEditorService, @IThemeService private readonly themeService: IThemeService ) { super(); - if (!manageExtensionAction) { - manageExtensionAction = this.instantiationService.createInstance(ManageExtensionAction); - } - this.render(entry); } @@ -355,19 +615,6 @@ class StatusBarEntryItem extends Disposable { addClass(this.container, 'has-background-color'); } - // Context Menu - if (entry.extensionId) { - this.entryDisposables.push((addDisposableListener(textContainer, 'contextmenu', e => { - EventHelper.stop(e, true); - - this.contextMenuService.showContextMenu({ - getAnchor: () => this.container, - getActionsContext: () => entry.extensionId!.value, - getActions: () => [manageExtensionAction] - }); - }))); - } - this.container.appendChild(textContainer); } @@ -416,19 +663,6 @@ class StatusBarEntryItem extends Disposable { } } -class ManageExtensionAction extends Action { - - constructor( - @ICommandService private readonly commandService: ICommandService - ) { - super('statusbar.manage.extension', nls.localize('manageExtension', "Manage Extension")); - } - - run(extensionId: string): Promise { - return this.commandService.executeCommand('_extensions.manage', extensionId); - } -} - registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); if (statusBarItemHoverBackground) { diff --git a/src/vs/workbench/contrib/debug/browser/debugStatus.ts b/src/vs/workbench/contrib/debug/browser/debugStatus.ts index 03660fe96560450a5caab2d03cef2574ad68c341..7c2af762d271c6be184e90ffe439f57f29c0f397 100644 --- a/src/vs/workbench/contrib/debug/browser/debugStatus.ts +++ b/src/vs/workbench/contrib/debug/browser/debugStatus.ts @@ -65,6 +65,7 @@ export class DebugStatusContribution implements IWorkbenchContribution { private get entry(): IStatusbarEntry { return { + category: { id: 'status.debug', label: nls.localize('status.debug', "Debug Configuration") }, text: this.getText(), tooltip: nls.localize('selectAndStartDebug', "Select and start debug configuration"), command: 'workbench.action.debug.selectandstart' diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts index 53badb9e0409cd5c2eb27949db8ef27df01441c1..230ba879168769b66670122dc1dc9f003df177de 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts @@ -84,6 +84,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio if (visible) { const indicator: IStatusbarEntry = { + category: { id: 'status.profiler', label: nls.localize('status.profiler', "Extension Profiler") }, text: nls.localize('profilingExtensionHost', "$(sync~spin) Profiling Extension Host"), tooltip: nls.localize('selectAndStartDebug', "Click to stop profiling."), command: 'workbench.action.extensionHostProfilder.stop' diff --git a/src/vs/workbench/contrib/feedback/electron-browser/feedback.contribution.ts b/src/vs/workbench/contrib/feedback/electron-browser/feedback.contribution.ts index 42adc6b0d990afff25cb5b598f58e89a8fc13fb2..229612a51a3c2a98c613eb22be0e5ac562102b92 100644 --- a/src/vs/workbench/contrib/feedback/electron-browser/feedback.contribution.ts +++ b/src/vs/workbench/contrib/feedback/electron-browser/feedback.contribution.ts @@ -13,6 +13,7 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'v // Register Statusbar item Registry.as(Extensions.Statusbar).registerStatusbarItem(new StatusbarItemDescriptor( FeedbackStatusbarItem, + { id: 'status.feedback', label: localize('status.feedback', "Send Feedback") }, StatusbarAlignment.RIGHT, -100 /* towards the end of the right hand side */ )); diff --git a/src/vs/workbench/contrib/feedback/electron-browser/feedbackStatusbarItem.ts b/src/vs/workbench/contrib/feedback/electron-browser/feedbackStatusbarItem.ts index 86cda55e2613212897f37fd5c45b4403768dc94f..d1187297851b0da6da1f6c4ee57fd5fe922be38a 100644 --- a/src/vs/workbench/contrib/feedback/electron-browser/feedbackStatusbarItem.ts +++ b/src/vs/workbench/contrib/feedback/electron-browser/feedbackStatusbarItem.ts @@ -6,7 +6,7 @@ import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; import { IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar'; import { FeedbackDropdown, IFeedback, IFeedbackDelegate, FEEDBACK_VISIBLE_CONFIG } from 'vs/workbench/contrib/feedback/electron-browser/feedback'; -import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import product from 'vs/platform/product/node/product'; import { Themable, STATUS_BAR_ITEM_HOVER_BACKGROUND } from 'vs/workbench/common/theme'; @@ -14,8 +14,6 @@ import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IConfigurationChangeEvent, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { clearNode, EventHelper, addClass, removeClass, addDisposableListener } from 'vs/base/browser/dom'; -import { localize } from 'vs/nls'; -import { Action } from 'vs/base/common/actions'; class TwitterFeedbackService implements IFeedbackDelegate { @@ -54,13 +52,11 @@ export class FeedbackStatusbarItem extends Themable implements IStatusbarItem { private dropdown: FeedbackDropdown | undefined; private enabled: boolean; private container: HTMLElement; - private hideAction: HideAction; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextViewService private readonly contextViewService: IContextViewService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IContextMenuService private readonly contextMenuService: IContextMenuService, @IConfigurationService private readonly configurationService: IConfigurationService, @IThemeService themeService: IThemeService ) { @@ -68,8 +64,6 @@ export class FeedbackStatusbarItem extends Themable implements IStatusbarItem { this.enabled = this.configurationService.getValue(FEEDBACK_VISIBLE_CONFIG); - this.hideAction = this._register(this.instantiationService.createInstance(HideAction)); - this.registerListeners(); } @@ -95,16 +89,6 @@ export class FeedbackStatusbarItem extends Themable implements IStatusbarItem { } }, true)); - // Offer context menu to hide status bar entry - this._register(addDisposableListener(this.container, 'contextmenu', e => { - EventHelper.stop(e, true); - - this.contextMenuService.showContextMenu({ - getAnchor: () => this.container, - getActions: () => [this.hideAction] - }); - })); - return this.update(); } @@ -143,19 +127,6 @@ export class FeedbackStatusbarItem extends Themable implements IStatusbarItem { } } -class HideAction extends Action { - - constructor( - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super('feedback.hide', localize('hide', "Hide")); - } - - run(extensionId: string): Promise { - return this.configurationService.updateValue(FEEDBACK_VISIBLE_CONFIG, false); - } -} - registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); if (statusBarItemHoverBackground) { diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index 9e5568ee46b3eafb2568e376f9b75b2997719d66..dd1e7c4feee110a5a25277c2e5e705b358535c72 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -286,6 +286,7 @@ class MarkersStatusBarContributions extends Disposable implements IWorkbenchCont private getMarkersItem(): IStatusbarEntry { const markersStatistics = this.markerService.getStatistics(); return { + category: { id: 'status.problems', label: localize('status.problems', "Problems") }, text: this.getMarkersText(markersStatistics), tooltip: this.getMarkersTooltip(markersStatistics), command: 'workbench.actions.view.toggleProblems' diff --git a/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts b/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts index 2ddf2907a6e3b917a04be82c3a20f6fac0b16887..a5d961a588a7aa3cf4ba6be149dfd93bf1a1aebe 100644 --- a/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts @@ -138,6 +138,7 @@ export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenc private renderWindowIndicator(text: string, tooltip?: string, command?: string): void { const properties: IStatusbarEntry = { + category: { id: 'status.host', label: nls.localize('status.host', "Remote Host") }, backgroundColor: themeColorFromId(STATUS_BAR_HOST_NAME_BACKGROUND), color: themeColorFromId(STATUS_BAR_HOST_NAME_FOREGROUND), text, tooltip, command }; if (this.windowIndicatorEntry) { diff --git a/src/vs/workbench/contrib/scm/browser/scmActivity.ts b/src/vs/workbench/contrib/scm/browser/scmActivity.ts index 557d4f5e90a17e88f22271db251fdb92e6008229..1bbaca9b7b341d9bec0c05836c78a7d23c85d708 100644 --- a/src/vs/workbench/contrib/scm/browser/scmActivity.ts +++ b/src/vs/workbench/contrib/scm/browser/scmActivity.ts @@ -190,6 +190,7 @@ export class StatusBarController implements IWorkbenchContribution { const disposables = new DisposableStore(); for (const c of commands) { disposables.add(this.statusbarService.addEntry({ + category: { id: 'status.scm', label: localize('status.scm', "Source Control") }, text: c.title, tooltip: `${label} - ${c.tooltip}`, command: c.id, diff --git a/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts index 9f053c9515eca87094552e37ae01e62536a329c5..1da64d3756a3c6462b3d1d653e347b498914ee06 100644 --- a/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/electron-browser/task.contribution.ts @@ -182,6 +182,7 @@ export class TaskStatusBarContributions extends Disposable implements IWorkbench } } else { const itemProps: IStatusbarEntry = { + category: { id: 'status.runningTasks', label: nls.localize('status.runningTasks', "Running Tasks") }, text: `$(tools) ${tasks.length}`, tooltip: nls.localize('runningTasks', "Show Running Tasks"), command: 'workbench.action.tasks.showTasks', diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 13bb9b423067c3e0041c34e8e40cebe9f1a44ab1..5385e10d7de6cac3c40cac13676e366f36565667 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -129,6 +129,7 @@ export class ProgressService implements IProgressService { } this._globalStatusEntry = this._statusbarService.addEntry({ + category: { id: 'status.progress', label: localize('status.progress', "Progress Message") }, text: `$(sync~spin) ${text}`, tooltip: title }, StatusbarAlignment.LEFT);