Files
azuredatastudio/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts
2019-06-11 12:35:58 -07:00

455 lines
16 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/statusbarpart';
import * as nls from 'vs/nls';
import { toErrorMessage } from 'vs/base/common/errorMessage';
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 } 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 { 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';
import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_ITEM_ACTIVE_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_FOREGROUND, STATUS_BAR_PROMINENT_ITEM_BACKGROUND, STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_BORDER } from 'vs/workbench/common/theme';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/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 } 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';
interface PendingEntry { entry: IStatusbarEntry; alignment: StatusbarAlignment; priority: number; accessor?: IStatusbarEntryAccessor; }
export class StatusbarPart extends Part implements IStatusbarService {
_serviceBrand: ServiceIdentifier<any>;
private static readonly PRIORITY_PROP = 'statusbar-entry-priority';
private static readonly ALIGNMENT_PROP = 'statusbar-entry-alignment';
//#region IView
readonly minimumWidth: number = 0;
readonly maximumWidth: number = Number.POSITIVE_INFINITY;
readonly minimumHeight: number = 22;
readonly maximumHeight: number = 22;
//#endregion
private statusMessageDispose: IDisposable;
private styleElement: HTMLStyleElement;
private pendingEntries: PendingEntry[] = [];
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IStorageService storageService: IStorageService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService
) {
super(Parts.STATUSBAR_PART, { hasTitle: false }, themeService, storageService, layoutService);
this.registerListeners();
}
private registerListeners(): void {
this._register(this.contextService.onDidChangeWorkbenchState(() => this.updateStyles()));
}
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: 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;
}
// 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);
// Insert according to priority
const container = this.element;
const neighbours = this.getEntries(alignment);
let inserted = false;
for (const neighbour of neighbours) {
const nPriority = Number(neighbour.getAttribute(StatusbarPart.PRIORITY_PROP));
if (
alignment === StatusbarAlignment.LEFT && nPriority < priority ||
alignment === StatusbarAlignment.RIGHT && nPriority > priority
) {
container.insertBefore(el, neighbour);
inserted = true;
break;
}
}
if (!inserted) {
container.appendChild(el);
}
return {
update: entry => {
// 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[] {
const entries: HTMLElement[] = [];
const container = this.element;
const children = container.children;
for (let i = 0; i < children.length; i++) {
const childElement = <HTMLElement>children.item(i);
if (Number(childElement.getAttribute(StatusbarPart.ALIGNMENT_PROP)) === alignment) {
entries.push(childElement);
}
}
return entries;
}
createContentArea(parent: HTMLElement): HTMLElement {
this.element = parent;
// Fill in initial items that were contributed from the registry
const registry = Registry.as<IStatusbarRegistry>(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;
}
} else if (a.alignment === StatusbarAlignment.LEFT) {
return 1;
} else if (a.alignment === StatusbarAlignment.RIGHT) {
return -1;
} else {
return 0;
}
});
for (const descriptor of descriptors) {
const item = this.instantiationService.createInstance(descriptor.syncDescriptor);
const el = this.doCreateStatusItem(descriptor.alignment, descriptor.priority);
this._register(item.render(el));
this.element.appendChild(el);
}
// Fill in pending entries if any
while (this.pendingEntries.length) {
const entry = this.pendingEntries.shift();
if (entry) {
entry.accessor = this.addEntry(entry.entry, entry.alignment, entry.priority);
}
}
return this.element;
}
updateStyles(): void {
super.updateStyles();
const container = this.getContainer();
// Background colors
const backgroundColor = this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_BACKGROUND : STATUS_BAR_NO_FOLDER_BACKGROUND);
container.style.backgroundColor = backgroundColor;
container.style.color = this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_FOREGROUND : STATUS_BAR_NO_FOLDER_FOREGROUND);
// Border color
const borderColor = this.getColor(this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY ? STATUS_BAR_BORDER : STATUS_BAR_NO_FOLDER_BORDER) || this.getColor(contrastBorder);
container.style.borderTopWidth = borderColor ? '1px' : null;
container.style.borderTopStyle = borderColor ? 'solid' : null;
container.style.borderTopColor = borderColor;
// Notification Beak
if (!this.styleElement) {
this.styleElement = createStyleSheet(container);
}
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');
if (extraClasses) {
addClasses(el, ...extraClasses);
}
if (alignment === StatusbarAlignment.RIGHT) {
addClass(el, 'right');
} else {
addClass(el, 'left');
}
el.setAttribute(StatusbarPart.PRIORITY_PROP, String(priority));
el.setAttribute(StatusbarPart.ALIGNMENT_PROP, String(alignment));
return el;
}
setStatusMessage(message: string, autoDisposeAfter: number = -1, delayBy: number = 0): IDisposable {
// Dismiss any previous
dispose(this.statusMessageDispose);
// Create new
let statusMessageEntry: IStatusbarEntryAccessor;
let showHandle: any = setTimeout(() => {
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 statusMessageDispose = {
dispose: () => {
if (showHandle) {
clearTimeout(showHandle);
}
if (hideHandle) {
clearTimeout(hideHandle);
}
if (statusMessageEntry) {
statusMessageEntry.dispose();
}
}
};
this.statusMessageDispose = statusMessageDispose;
if (typeof autoDisposeAfter === 'number' && autoDisposeAfter > 0) {
hideHandle = setTimeout(() => statusMessageDispose.dispose(), autoDisposeAfter);
}
return statusMessageDispose;
}
layout(width: number, height: number): void {
super.layoutContents(width, height);
}
toJSON(): object {
return {
type: Parts.STATUSBAR_PART
};
}
}
let manageExtensionAction: ManageExtensionAction;
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);
}
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 (entry.command) {
textContainer = document.createElement('a');
this.entryDisposables.push((addDisposableListener(textContainer, 'click', () => this.executeCommand(entry.command!, entry.arguments))));
} else {
textContainer = document.createElement('span');
}
// Label
new OcticonLabel(textContainer).text = entry.text;
// Tooltip
if (entry.tooltip) {
textContainer.title = entry.tooltip;
}
// Color (only applies to text container)
this.applyColor(textContainer, entry.color);
// Background Color (applies to parent element to fully fill container)
if (entry.backgroundColor) {
this.applyColor(this.container, entry.backgroundColor, true);
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);
}
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();
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;
}
}
private async executeCommand(id: string, args?: unknown[]): Promise<void> {
args = args || [];
// Maintain old behaviour of always focusing the editor here
const activeTextEditorWidget = this.editorService.activeTextEditorWidget;
if (activeTextEditorWidget) {
activeTextEditorWidget.focus();
}
/* __GDPR__
"workbenchActionExecuted" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('workbenchActionExecuted', { id, from: 'status bar' });
try {
await this.commandService.executeCommand(id, ...args);
} catch (error) {
this.notificationService.error(toErrorMessage(error));
}
}
dispose(): void {
super.dispose();
this.entryDisposables = dispose(this.entryDisposables);
}
}
class ManageExtensionAction extends Action {
constructor(
@ICommandService private readonly commandService: ICommandService
) {
super('statusbar.manage.extension', nls.localize('manageExtension', "Manage Extension"));
}
run(extensionId: string): Promise<any> {
return this.commandService.executeCommand('_extensions.manage', extensionId);
}
}
registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {
const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND);
if (statusBarItemHoverBackground) {
collector.addRule(`.monaco-workbench .part.statusbar > .statusbar-item a:hover { background-color: ${statusBarItemHoverBackground}; }`);
}
const statusBarItemActiveBackground = theme.getColor(STATUS_BAR_ITEM_ACTIVE_BACKGROUND);
if (statusBarItemActiveBackground) {
collector.addRule(`.monaco-workbench .part.statusbar > .statusbar-item a:active { background-color: ${statusBarItemActiveBackground}; }`);
}
const statusBarProminentItemForeground = theme.getColor(STATUS_BAR_PROMINENT_ITEM_FOREGROUND);
if (statusBarProminentItemForeground) {
collector.addRule(`.monaco-workbench .part.statusbar > .statusbar-item .status-bar-info { color: ${statusBarProminentItemForeground}; }`);
}
const statusBarProminentItemBackground = theme.getColor(STATUS_BAR_PROMINENT_ITEM_BACKGROUND);
if (statusBarProminentItemBackground) {
collector.addRule(`.monaco-workbench .part.statusbar > .statusbar-item .status-bar-info { background-color: ${statusBarProminentItemBackground}; }`);
}
const statusBarProminentItemHoverBackground = theme.getColor(STATUS_BAR_PROMINENT_ITEM_HOVER_BACKGROUND);
if (statusBarProminentItemHoverBackground) {
collector.addRule(`.monaco-workbench .part.statusbar > .statusbar-item a.status-bar-info:hover { background-color: ${statusBarProminentItemHoverBackground}; }`);
}
});
registerSingleton(IStatusbarService, StatusbarPart);