/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import 'vs/css!./media/sidebarpart'; import {TPromise} from 'vs/base/common/winjs.base'; import nls = require('vs/nls'); import {Registry} from 'vs/platform/platform'; import {IDisposable} from 'vs/base/common/lifecycle'; import {Dimension, Builder, $} from 'vs/base/browser/builder'; import uuid = require('vs/base/common/uuid'); import events = require('vs/base/common/events'); import timer = require('vs/base/common/timer'); import strings = require('vs/base/common/strings'); import types = require('vs/base/common/types'); import errors = require('vs/base/common/errors'); import {CONTEXT as ToolBarContext, 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 {Scope, IActionBarRegistry, Extensions, prepareActions} from 'vs/workbench/browser/actionBarRegistry'; import {Action, IAction} from 'vs/base/common/actions'; import {Part} from 'vs/workbench/browser/part'; import {EventType as WorkbenchEventType, ViewletEvent} from 'vs/workbench/browser/events'; import {Viewlet, EventType as ViewletEventType, IViewletRegistry, Extensions as ViewletExtensions} from 'vs/workbench/browser/viewlet'; import {IWorkbenchActionRegistry, Extensions as ActionExtensions} from 'vs/workbench/browser/actionRegistry'; import {SyncActionDescriptor} from 'vs/platform/actions/common/actions'; import {WorkbenchProgressService} from 'vs/workbench/services/progress/browser/progressService'; import {IViewletService} from 'vs/workbench/services/viewlet/common/viewletService'; import {IPartService} from 'vs/workbench/services/part/common/partService'; import {IViewlet} from 'vs/workbench/common/viewlet'; import {IStorageService, StorageScope} from 'vs/platform/storage/common/storage'; import {IContextMenuService} from 'vs/platform/contextview/browser/contextView'; import {IEventService} from 'vs/platform/event/common/event'; import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; import {IMessageService, Severity} from 'vs/platform/message/common/message'; import {IProgressService} from 'vs/platform/progress/common/progress'; import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry'; import {IKeybindingService} from 'vs/platform/keybinding/common/keybindingService'; import {KeybindingsUtils} from 'vs/platform/keybinding/common/keybindingsUtils'; import {KeyMod, KeyCode} from 'vs/base/common/keyCodes'; export class SidebarPart extends Part implements IViewletService { public serviceId = IViewletService; private instantiationService: IInstantiationService; private activeViewletListeners: { (): void; }[]; private instantiatedViewletListeners: { (): void; }[]; private mapViewletToViewletContainer: { [viewletId: string]: Builder; }; private mapActionsBindingToViewlet: { [viewletId: string]: () => void; }; private mapProgressServiceToViewlet: { [viewletId: string]: IProgressService; }; private activeViewlet: Viewlet; private lastActiveViewletId: string; private instantiatedViewlets: Viewlet[]; private titleLabel: Builder; private toolBar: ToolBar; private viewletLoaderPromises: { [viewletId: string]: TPromise; }; private progressBar: ProgressBar; private contentAreaSize: Dimension; private blockOpeningViewlet: boolean; private registry: IViewletRegistry; private telemetryActionsListener: IDisposable; private currentViewletOpenToken: string; constructor( private messageService: IMessageService, private storageService: IStorageService, private eventService: IEventService, private telemetryService: ITelemetryService, private contextMenuService: IContextMenuService, private partService: IPartService, private keybindingService: IKeybindingService, id: string ) { super(id); this.activeViewletListeners = []; this.instantiatedViewletListeners = []; this.mapViewletToViewletContainer = {}; this.mapActionsBindingToViewlet = {}; this.mapProgressServiceToViewlet = {}; this.activeViewlet = null; this.instantiatedViewlets = []; this.viewletLoaderPromises = {}; this.registry = (Registry.as(ViewletExtensions.Viewlets)); } public setInstantiationService(service: IInstantiationService): void { this.instantiationService = service; } public openViewlet(id: string, focus?: boolean): TPromise { if (this.blockOpeningViewlet) { return TPromise.as(null); // Workaround against a potential race condition } // First check if sidebar is hidden and show if so if (this.partService.isSideBarHidden()) { try { this.blockOpeningViewlet = true; this.partService.setSideBarHidden(false); } finally { this.blockOpeningViewlet = false; } } // Check if viewlet already visible and just focus in that case if (this.activeViewlet && this.activeViewlet.getId() === id) { if (focus) { this.activeViewlet.focus(); } // Fullfill promise with viewlet that is being opened return TPromise.as(this.activeViewlet); } // Open return this.doOpenViewlet(id, focus); } private doOpenViewlet(id: string, focus?: boolean): TPromise { let timerEvent = timer.start(timer.Topic.WORKBENCH, strings.format('Open Viewlet {0}', id.substr(id.lastIndexOf('.') + 1))); // Use a generated token to avoid race conditions from long running promises let currentViewletOpenToken = uuid.generateUuid(); this.currentViewletOpenToken = currentViewletOpenToken; // Emit Viewlet Opening Event this.emit(WorkbenchEventType.VIEWLET_OPENING, new ViewletEvent(id)); // Hide current let hidePromise: TPromise; if (this.activeViewlet) { hidePromise = this.hideActiveViewlet(); } else { hidePromise = TPromise.as(null); } return hidePromise.then(() => { // Update Title this.updateTitle(id); // Create viewlet return this.createViewlet(id, true).then((viewlet: Viewlet) => { // Check if another viewlet opened meanwhile and return in that case if ((this.currentViewletOpenToken !== currentViewletOpenToken) || (this.activeViewlet && this.activeViewlet.getId() !== viewlet.getId())) { timerEvent.stop(); return TPromise.as(null); } // Check if viewlet already visible and just focus in that case if (this.activeViewlet && this.activeViewlet.getId() === viewlet.getId()) { if (focus) { viewlet.focus(); } timerEvent.stop(); // Fullfill promise with viewlet that is being opened return TPromise.as(viewlet); } // Show Viewlet and Focus return this.showViewlet(viewlet).then(() => { if (focus) { viewlet.focus(); } timerEvent.stop(); // Fullfill promise with viewlet that is being opened return viewlet; }); }); }); } private createViewlet(id: string, isActive?: boolean): TPromise { // Check if viewlet is already created for (let i = 0; i < this.instantiatedViewlets.length; i++) { if (this.instantiatedViewlets[i].getId() === id) { return TPromise.as(this.instantiatedViewlets[i]); } } // Instantiate viewlet from registry otherwise let viewletDescriptor = this.registry.getViewlet(id); if (viewletDescriptor) { let loaderPromise = this.viewletLoaderPromises[id]; if (!loaderPromise) { let progressService = new WorkbenchProgressService(this.eventService, this.progressBar, viewletDescriptor.id, isActive); let services = { progressService: progressService }; let viewletInstantiationService = this.instantiationService.createChild(services); loaderPromise = viewletInstantiationService.createInstance(viewletDescriptor).then((viewlet: Viewlet) => { this.mapProgressServiceToViewlet[viewlet.getId()] = progressService; // Remember as Instantiated this.instantiatedViewlets.push(viewlet); // Register to title area update events from the viewlet this.instantiatedViewletListeners.push(viewlet.addListener(ViewletEventType.INTERNAL_VIEWLET_TITLE_AREA_UPDATE, (e) => { this.onTitleAreaUpdate(e); })); // Remove from Promises Cache since Loaded delete this.viewletLoaderPromises[id]; return viewlet; }); // Report progress for slow loading viewlets progressService.showWhile(loaderPromise, this.partService.isCreated() ? 800 : 3200 /* less ugly initial startup */); // Add to Promise Cache until Loaded this.viewletLoaderPromises[id] = loaderPromise; } return loaderPromise; } throw new Error(strings.format('Unable to find viewlet with id {0}', id)); } private showViewlet(viewlet: Viewlet): TPromise { // Remember Viewlet this.activeViewlet = viewlet; // Remember this.lastActiveViewletId = this.activeViewlet.getId(); // Register as Emitter to Workbench Bus this.activeViewletListeners.push(this.eventService.addEmitter(this.activeViewlet, this.activeViewlet.getId())); let createViewletPromise: TPromise; // Viewlet created for the first time let viewletContainer = this.mapViewletToViewletContainer[viewlet.getId()]; if (!viewletContainer) { // Build Container off-DOM viewletContainer = $().div({ 'class': 'viewlet', id: viewlet.getId() }, (div: Builder) => { createViewletPromise = viewlet.create(div); }); // Remember viewlet container this.mapViewletToViewletContainer[viewlet.getId()] = viewletContainer; } // Viewlet already exists but is hidden else { createViewletPromise = TPromise.as(null); } // Report progress for slow loading viewlets (but only if we did not create the viewlet before already) let progressService = this.mapProgressServiceToViewlet[viewlet.getId()]; if (progressService && !viewletContainer) { this.mapProgressServiceToViewlet[viewlet.getId()].showWhile(createViewletPromise, this.partService.isCreated() ? 800 : 3200 /* less ugly initial startup */); } // Fill Content and Actions return createViewletPromise.then(() => { // Make sure that the user meanwhile did not open another viewlet or closed the sidebar if (!this.activeViewlet || viewlet.getId() !== this.activeViewlet.getId()) { return; } // Take Viewlet on-DOM and show viewletContainer.build(this.getContentArea()); viewletContainer.show(); // Setup action runner this.toolBar.actionRunner = viewlet.getActionRunner(); // Update title with viewlet title if it differs from descriptor let descriptor = this.registry.getViewlet(viewlet.getId()); if (descriptor && descriptor.name !== viewlet.getTitle()) { this.updateTitle(viewlet.getId(), viewlet.getTitle()); } // Handle Viewlet Actions let actionsBinding = this.mapActionsBindingToViewlet[viewlet.getId()]; if (!actionsBinding) { actionsBinding = this.collectViewletActions(viewlet); this.mapActionsBindingToViewlet[viewlet.getId()] = actionsBinding; } actionsBinding(); if (this.telemetryActionsListener) { this.telemetryActionsListener.dispose(); this.telemetryActionsListener = null; } // Action Run Handling this.telemetryActionsListener = this.toolBar.actionRunner.addListener2(events.EventType.RUN, (e: any) => { // Check for Error if (e.error && !errors.isPromiseCanceledError(e.error)) { this.messageService.show(Severity.Error, e.error); } // Log in telemetry if (this.telemetryService) { this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: 'sideBar' }); } }); // Indicate to viewlet that it is now visible return viewlet.setVisible(true).then(() => { // Make sure that the user meanwhile did not open another viewlet or closed the sidebar if (!this.activeViewlet || viewlet.getId() !== this.activeViewlet.getId()) { return; } // Make sure the viewlet is layed out if (this.contentAreaSize) { viewlet.layout(this.contentAreaSize); } // Emit Viewlet Opened Event this.emit(WorkbenchEventType.VIEWLET_OPENED, new ViewletEvent(this.activeViewlet.getId())); }); }, (error: any) => this.onError(error)); } private onTitleAreaUpdate(e: ViewletEvent): void { // Active Viewlet if (this.activeViewlet && this.activeViewlet.getId() === e.viewletId) { // Title this.updateTitle(this.activeViewlet.getId(), this.activeViewlet.getTitle()); // Actions let actionsBinding = this.collectViewletActions(this.activeViewlet); this.mapActionsBindingToViewlet[this.activeViewlet.getId()] = actionsBinding; actionsBinding(); } // Otherwise invalidate actions binding for next time when the viewlet becomes visible else { delete this.mapActionsBindingToViewlet[e.viewletId]; } } private updateTitle(viewletId: string, viewletTitle?: string): void { let viewletDescriptor = this.registry.getViewlet(viewletId); if (!viewletDescriptor) { return; } if (!viewletTitle) { viewletTitle = viewletDescriptor.name; } let keybinding: string = null; let keys = this.keybindingService.lookupKeybindings(viewletId).map(k => this.keybindingService.getLabelFor(k)); if (keys && keys.length) { keybinding = keys[0]; } this.titleLabel.safeInnerHtml(viewletTitle); this.titleLabel.title(keybinding ? nls.localize('viewletTitleTooltip', "{0} ({1})", viewletTitle, keybinding) : viewletTitle); } private collectViewletActions(viewlet: Viewlet): () => void { // From Viewlet let primaryActions: IAction[] = viewlet.getActions(); let secondaryActions: IAction[] = viewlet.getSecondaryActions(); // From Contributions let actionBarRegistry = Registry.as(Extensions.Actionbar); primaryActions.push(...actionBarRegistry.getActionBarActionsForContext(Scope.VIEW, viewlet)); secondaryActions.push(...actionBarRegistry.getSecondaryActionBarActionsForContext(Scope.VIEW, viewlet)); // Return fn to set into toolbar return this.toolBar.setActions(prepareActions(primaryActions), prepareActions(secondaryActions)); } public getActiveViewlet(): IViewlet { return this.activeViewlet; } public getLastActiveViewletId(): string { return this.lastActiveViewletId; } public hideActiveViewlet(): TPromise { if (!this.activeViewlet) { return TPromise.as(null); // Nothing to do } let viewlet = this.activeViewlet; this.activeViewlet = null; let viewletContainer = this.mapViewletToViewletContainer[viewlet.getId()]; // Indicate to Viewlet return viewlet.setVisible(false).then(() => { // Take Container Off-DOM and hide viewletContainer.offDOM(); viewletContainer.hide(); // Clear any running Progress this.progressBar.stop().getContainer().hide(); // Empty Actions this.toolBar.setActions([])(); // Clear Listeners while (this.activeViewletListeners.length) { this.activeViewletListeners.pop()(); } // Emit Viewlet Closed Event this.emit(WorkbenchEventType.VIEWLET_CLOSED, new ViewletEvent(viewlet.getId())); }); } public createTitleArea(parent: Builder): Builder { // Title Area Container let titleArea = $(parent).div({ 'class': 'title' }); // Right Actions Container $(titleArea).div({ 'class': 'title-actions' }, (div) => { // Toolbar this.toolBar = new ToolBar(div.getHTMLElement(), this.contextMenuService, { actionItemProvider: (action: Action) => this.actionItemProvider(action), orientation: ActionsOrientation.HORIZONTAL }); }); // Left Title Label $(titleArea).div({ 'class': 'title-label' }, (div) => { this.titleLabel = div.span(); }); return titleArea; } private actionItemProvider(action: Action): IActionItem { let actionItem: IActionItem; // Check Active Viewlet if (this.activeViewlet) { actionItem = this.activeViewlet.getActionItem(action); } // Check Registry if (!actionItem) { let actionBarRegistry = Registry.as(Extensions.Actionbar); actionItem = actionBarRegistry.getActionItemForContext(Scope.VIEW, ToolBarContext, action); } return actionItem; } public createContentArea(parent: Builder): Builder { return $(parent).div({ 'class': 'content' }, (div: Builder) => { this.progressBar = new ProgressBar(div); this.progressBar.getContainer().hide(); }); } private onError(error: any): void { this.messageService.show(Severity.Error, types.isString(error) ? new Error(error) : error); } public layout(dimension: Dimension): Dimension[] { // Pass to super let sizes = super.layout(dimension); // Pass Contentsize to viewlet this.contentAreaSize = sizes[1]; if (this.activeViewlet) { this.activeViewlet.layout(this.contentAreaSize); } return sizes; } public shutdown(): void { this.instantiatedViewlets.forEach(i => i.shutdown()); super.shutdown(); } public dispose(): void { this.mapViewletToViewletContainer = null; this.mapProgressServiceToViewlet = null; this.mapActionsBindingToViewlet = null; for (let i = 0; i < this.instantiatedViewlets.length; i++) { this.instantiatedViewlets[i].dispose(); } this.instantiatedViewlets = []; while (this.activeViewletListeners.length) { this.activeViewletListeners.pop()(); } while (this.instantiatedViewletListeners.length) { this.instantiatedViewletListeners.pop()(); } this.progressBar.dispose(); this.toolBar.dispose(); // Super Dispose super.dispose(); } } export class FocusSideBarAction extends Action { public static ID = 'workbench.action.focusSideBar'; public static LABEL = nls.localize('focusSideBar', "Focus into Side Bar"); constructor( id: string, label: string, @IViewletService private viewletService: IViewletService, @IPartService private partService: IPartService ) { super(id, label); } public run(): TPromise { // Show side bar if (this.partService.isSideBarHidden()) { this.partService.setSideBarHidden(false); } // Focus into active viewlet else { let viewlet = this.viewletService.getActiveViewlet(); if (viewlet) { viewlet.focus(); } } return TPromise.as(true); } } let registry = Registry.as(ActionExtensions.WorkbenchActions); registry.registerWorkbenchAction(new SyncActionDescriptor(FocusSideBarAction, FocusSideBarAction.ID, FocusSideBarAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_0 }), nls.localize('viewCategory', "View"));