Files
azuredatastudio/src/vs/workbench/browser/parts/compositePart.ts

525 lines
17 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/compositepart';
import * as nls from 'vs/nls';
import { defaultGenerator } from 'vs/base/common/idGenerator';
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import * as strings from 'vs/base/common/strings';
import { Emitter } from 'vs/base/common/event';
import * as errors from 'vs/base/common/errors';
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
import { prepareActions } from 'vs/workbench/browser/actions';
import { IAction } from 'vs/base/common/actions';
import { Part, IPartOptions } from 'vs/workbench/browser/part';
import { Composite, CompositeRegistry } from 'vs/workbench/browser/composite';
import { IComposite } from 'vs/workbench/common/composite';
import { ScopedProgressService } from 'vs/workbench/services/progress/browser/progressService';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachProgressBarStyler } from 'vs/platform/theme/common/styler';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { Dimension, append, $, addClass, hide, show, addClasses } from 'vs/base/browser/dom';
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
import { withNullAsUndefined } from 'vs/base/common/types';
export interface ICompositeTitleLabel {
/**
* Asks to update the title for the composite with the given ID.
*/
updateTitle(id: string, title: string, keybinding?: string): void;
/**
* Called when theming information changes.
*/
updateStyles(): void;
}
interface CompositeItem {
composite: Composite;
disposable: IDisposable;
progressService: IProgressService;
}
export abstract class CompositePart<T extends Composite> extends Part {
protected readonly onDidCompositeOpen = this._register(new Emitter<{ composite: IComposite, focus: boolean }>());
protected readonly onDidCompositeClose = this._register(new Emitter<IComposite>());
protected toolBar: ToolBar;
private mapCompositeToCompositeContainer: { [compositeId: string]: HTMLElement; };
private mapActionsBindingToComposite: { [compositeId: string]: () => void; };
private activeComposite: Composite | null;
private lastActiveCompositeId: string;
private instantiatedCompositeItems: Map<string, CompositeItem>;
private titleLabel: ICompositeTitleLabel;
private progressBar: ProgressBar;
private contentAreaSize: Dimension;
private telemetryActionsListener: IDisposable | null;
private currentCompositeOpenToken: string;
constructor(
private notificationService: INotificationService,
protected storageService: IStorageService,
private telemetryService: ITelemetryService,
protected contextMenuService: IContextMenuService,
protected layoutService: IWorkbenchLayoutService,
protected keybindingService: IKeybindingService,
protected instantiationService: IInstantiationService,
themeService: IThemeService,
protected readonly registry: CompositeRegistry<T>,
private activeCompositeSettingsKey: string,
private defaultCompositeId: string,
private nameForTelemetry: string,
private compositeCSSClass: string,
private titleForegroundColor: string | undefined,
id: string,
options: IPartOptions
) {
super(id, options, themeService, storageService, layoutService);
this.mapCompositeToCompositeContainer = {};
this.mapActionsBindingToComposite = {};
this.activeComposite = null;
this.instantiatedCompositeItems = new Map<string, CompositeItem>();
this.lastActiveCompositeId = storageService.get(activeCompositeSettingsKey, StorageScope.WORKSPACE, this.defaultCompositeId);
}
protected openComposite(id: string, focus?: boolean): Composite | undefined {
// Check if composite already visible and just focus in that case
if (this.activeComposite && this.activeComposite.getId() === id) {
if (focus) {
this.activeComposite.focus();
}
// Fullfill promise with composite that is being opened
return this.activeComposite;
}
// Open
return this.doOpenComposite(id, focus);
}
private doOpenComposite(id: string, focus: boolean = false): Composite | undefined {
// Use a generated token to avoid race conditions from long running promises
const currentCompositeOpenToken = defaultGenerator.nextId();
this.currentCompositeOpenToken = currentCompositeOpenToken;
// Hide current
if (this.activeComposite) {
this.hideActiveComposite();
}
// Update Title
this.updateTitle(id);
// Create composite
const composite = this.createComposite(id, true);
// Check if another composite opened meanwhile and return in that case
if ((this.currentCompositeOpenToken !== currentCompositeOpenToken) || (this.activeComposite && this.activeComposite.getId() !== composite.getId())) {
return undefined;
}
// Check if composite already visible and just focus in that case
if (this.activeComposite && this.activeComposite.getId() === composite.getId()) {
if (focus) {
composite.focus();
}
this.onDidCompositeOpen.fire({ composite, focus });
return composite;
}
// Show Composite and Focus
this.showComposite(composite);
if (focus) {
composite.focus();
}
// Return with the composite that is being opened
if (composite) {
this.onDidCompositeOpen.fire({ composite, focus });
}
return composite;
}
protected createComposite(id: string, isActive?: boolean): Composite {
// Check if composite is already created
const compositeItem = this.instantiatedCompositeItems.get(id);
if (compositeItem) {
return compositeItem.composite;
}
// Instantiate composite from registry otherwise
const compositeDescriptor = this.registry.getComposite(id);
if (compositeDescriptor) {
const progressService = this.instantiationService.createInstance(ScopedProgressService, this.progressBar, compositeDescriptor.id, isActive);
const compositeInstantiationService = this.instantiationService.createChild(new ServiceCollection([IProgressService, progressService]));
const composite = compositeDescriptor.instantiate(compositeInstantiationService);
const disposables: IDisposable[] = [];
// Remember as Instantiated
this.instantiatedCompositeItems.set(id, { composite, disposable: toDisposable(() => dispose(disposables)), progressService });
// Register to title area update events from the composite
composite.onTitleAreaUpdate(() => this.onTitleAreaUpdate(composite.getId()), this, disposables);
return composite;
}
throw new Error(`Unable to find composite with id ${id}`);
}
protected showComposite(composite: Composite): void {
// Remember Composite
this.activeComposite = composite;
// Store in preferences
const id = this.activeComposite.getId();
if (id !== this.defaultCompositeId) {
this.storageService.store(this.activeCompositeSettingsKey, id, StorageScope.WORKSPACE);
} else {
this.storageService.remove(this.activeCompositeSettingsKey, StorageScope.WORKSPACE);
}
// Remember
this.lastActiveCompositeId = this.activeComposite.getId();
// Composites created for the first time
let compositeContainer = this.mapCompositeToCompositeContainer[composite.getId()];
if (!compositeContainer) {
// Build Container off-DOM
compositeContainer = $('.composite');
addClasses(compositeContainer, this.compositeCSSClass);
compositeContainer.id = composite.getId();
composite.create(compositeContainer);
composite.updateStyles();
// Remember composite container
this.mapCompositeToCompositeContainer[composite.getId()] = compositeContainer;
}
// Report progress for slow loading composites (but only if we did not create the composites before already)
const compositeItem = this.instantiatedCompositeItems.get(composite.getId());
if (compositeItem && !compositeContainer) {
compositeItem.progressService.showWhile(Promise.resolve(), this.layoutService.isRestored() ? 800 : 3200 /* less ugly initial startup */);
}
// Fill Content and Actions
// Make sure that the user meanwhile did not open another composite or closed the part containing the composite
if (!this.activeComposite || composite.getId() !== this.activeComposite.getId()) {
return undefined;
}
// Take Composite on-DOM and show
const contentArea = this.getContentArea();
if (contentArea) {
contentArea.appendChild(compositeContainer);
}
show(compositeContainer);
// Setup action runner
this.toolBar.actionRunner = composite.getActionRunner();
// Update title with composite title if it differs from descriptor
const descriptor = this.registry.getComposite(composite.getId());
if (descriptor && descriptor.name !== composite.getTitle()) {
this.updateTitle(composite.getId(), withNullAsUndefined(composite.getTitle()));
}
// Handle Composite Actions
let actionsBinding = this.mapActionsBindingToComposite[composite.getId()];
if (!actionsBinding) {
actionsBinding = this.collectCompositeActions(composite);
this.mapActionsBindingToComposite[composite.getId()] = actionsBinding;
}
actionsBinding();
if (this.telemetryActionsListener) {
this.telemetryActionsListener.dispose();
this.telemetryActionsListener = null;
}
// Action Run Handling
this.telemetryActionsListener = this.toolBar.actionRunner.onDidRun(e => {
// Check for Error
if (e.error && !errors.isPromiseCanceledError(e.error)) {
this.notificationService.error(e.error);
}
// Log in telemetry
if (this.telemetryService) {
/* __GDPR__
"workbenchActionExecuted" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: this.nameForTelemetry });
}
});
// Indicate to composite that it is now visible
composite.setVisible(true);
// Make sure that the user meanwhile did not open another composite or closed the part containing the composite
if (!this.activeComposite || composite.getId() !== this.activeComposite.getId()) {
return;
}
// Make sure the composite is layed out
if (this.contentAreaSize) {
composite.layout(this.contentAreaSize);
}
}
protected onTitleAreaUpdate(compositeId: string): void {
// Active Composite
if (this.activeComposite && this.activeComposite.getId() === compositeId) {
// Title
this.updateTitle(this.activeComposite.getId(), this.activeComposite.getTitle() || undefined);
// Actions
const actionsBinding = this.collectCompositeActions(this.activeComposite);
this.mapActionsBindingToComposite[this.activeComposite.getId()] = actionsBinding;
actionsBinding();
}
// Otherwise invalidate actions binding for next time when the composite becomes visible
else {
delete this.mapActionsBindingToComposite[compositeId];
}
}
private updateTitle(compositeId: string, compositeTitle?: string): void {
const compositeDescriptor = this.registry.getComposite(compositeId);
if (!compositeDescriptor || !this.titleLabel) {
return;
}
if (!compositeTitle) {
compositeTitle = compositeDescriptor.name;
}
const keybinding = this.keybindingService.lookupKeybinding(compositeId);
this.titleLabel.updateTitle(compositeId, compositeTitle, (keybinding && keybinding.getLabel()) || undefined);
this.toolBar.setAriaLabel(nls.localize('ariaCompositeToolbarLabel', "{0} actions", compositeTitle));
}
private collectCompositeActions(composite: Composite): () => void {
// From Composite
const primaryActions: IAction[] = composite.getActions().slice(0);
const secondaryActions: IAction[] = composite.getSecondaryActions().slice(0);
// From Part
primaryActions.push(...this.getActions());
secondaryActions.push(...this.getSecondaryActions());
// Update context
this.toolBar.context = this.actionsContextProvider();
// Return fn to set into toolbar
return this.toolBar.setActions(prepareActions(primaryActions), prepareActions(secondaryActions));
}
protected getActiveComposite(): IComposite | null {
return this.activeComposite;
}
protected getLastActiveCompositetId(): string {
return this.lastActiveCompositeId;
}
protected hideActiveComposite(): Composite | undefined {
if (!this.activeComposite) {
return undefined; // Nothing to do
}
const composite = this.activeComposite;
this.activeComposite = null;
const compositeContainer = this.mapCompositeToCompositeContainer[composite.getId()];
// Indicate to Composite
composite.setVisible(false);
// Take Container Off-DOM and hide
compositeContainer.remove();
hide(compositeContainer);
// Clear any running Progress
this.progressBar.stop().hide();
// Empty Actions
this.toolBar.setActions([])();
this.onDidCompositeClose.fire(composite);
return composite;
}
createTitleArea(parent: HTMLElement): HTMLElement {
// Title Area Container
const titleArea = append(parent, $('.composite'));
addClass(titleArea, 'title');
// Left Title Label
this.titleLabel = this.createTitleLabel(titleArea);
// Right Actions Container
const titleActionsContainer = append(titleArea, $('.title-actions'));
// Toolbar
this.toolBar = this._register(new ToolBar(titleActionsContainer, this.contextMenuService, {
actionItemProvider: action => this.actionItemProvider(action),
orientation: ActionsOrientation.HORIZONTAL,
getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id),
anchorAlignmentProvider: () => this.getTitleAreaDropDownAnchorAlignment()
}));
return titleArea;
}
protected createTitleLabel(parent: HTMLElement): ICompositeTitleLabel {
const titleContainer = append(parent, $('.title-label'));
const titleLabel = append(titleContainer, $('h2'));
const $this = this;
return {
updateTitle: (id, title, keybinding) => {
titleLabel.innerHTML = strings.escape(title);
titleLabel.title = keybinding ? nls.localize('titleTooltip', "{0} ({1})", title, keybinding) : title;
},
updateStyles: () => {
titleLabel.style.color = $this.titleForegroundColor ? $this.getColor($this.titleForegroundColor) : null;
}
};
}
updateStyles(): void {
super.updateStyles();
// Forward to title label
this.titleLabel.updateStyles();
}
protected actionItemProvider(action: IAction): IActionItem | undefined {
// Check Active Composite
if (this.activeComposite) {
return this.activeComposite.getActionItem(action);
}
return undefined;
}
protected actionsContextProvider(): unknown {
// Check Active Composite
if (this.activeComposite) {
return this.activeComposite.getActionsContext();
}
return null;
}
createContentArea(parent: HTMLElement): HTMLElement {
const contentContainer = append(parent, $('.content'));
this.progressBar = this._register(new ProgressBar(contentContainer));
this._register(attachProgressBarStyler(this.progressBar, this.themeService));
this.progressBar.hide();
return contentContainer;
}
getProgressIndicator(id: string): IProgressService | null {
const compositeItem = this.instantiatedCompositeItems.get(id);
return compositeItem ? compositeItem.progressService : null;
}
protected getActions(): IAction[] {
return [];
}
protected getSecondaryActions(): IAction[] {
return [];
}
protected getTitleAreaDropDownAnchorAlignment(): AnchorAlignment {
return AnchorAlignment.RIGHT;
}
layout(width: number, height: number): void {
// Layout contents
this.contentAreaSize = super.layoutContents(width, height).contentSize;
// Layout composite
if (this.activeComposite) {
this.activeComposite.layout(this.contentAreaSize);
}
}
protected removeComposite(compositeId: string): boolean {
if (this.activeComposite && this.activeComposite.getId() === compositeId) {
return false; // do not remove active composite
}
delete this.mapCompositeToCompositeContainer[compositeId];
delete this.mapActionsBindingToComposite[compositeId];
const compositeItem = this.instantiatedCompositeItems.get(compositeId);
if (compositeItem) {
compositeItem.composite.dispose();
dispose(compositeItem.disposable);
this.instantiatedCompositeItems.delete(compositeId);
}
return true;
}
dispose(): void {
this.mapCompositeToCompositeContainer = null!; // StrictNullOverride: nulling out ok in dispose
this.mapActionsBindingToComposite = null!; // StrictNullOverride: nulling out ok in dispose
this.instantiatedCompositeItems.forEach(compositeItem => {
compositeItem.composite.dispose();
dispose(compositeItem.disposable);
});
this.instantiatedCompositeItems.clear();
super.dispose();
}
}