/*--------------------------------------------------------------------------------------------- * 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 nls = require('vs/nls'); import { Action } from 'vs/base/common/actions'; import { illegalArgument } from 'vs/base/common/errors'; import * as dom from 'vs/base/browser/dom'; import * as arrays from 'vs/base/common/arrays'; import { Dimension } from 'vs/base/browser/builder'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IBadge } from 'vs/workbench/services/activity/common/activity'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ActionBar, IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import Event, { Emitter } from 'vs/base/common/event'; import { CompositeActionItem, CompositeOverflowActivityAction, ICompositeActivity, CompositeOverflowActivityActionItem, ActivityAction, ICompositeBar, ICompositeBarColors } from 'vs/workbench/browser/parts/compositebar/compositeBarActions'; import { TPromise } from 'vs/base/common/winjs.base'; export interface ICompositeBarOptions { icon: boolean; storageId: string; orientation: ActionsOrientation; composites: { id: string, name: string }[]; colors: ICompositeBarColors; overflowActionSize: number; getActivityAction: (compositeId: string) => ActivityAction; getCompositePinnedAction: (compositeId: string) => Action; getOnCompositeClickAction: (compositeId: string) => Action; openComposite: (compositeId: string) => TPromise; getDefaultCompositeId: () => string; hidePart: () => TPromise; } export class CompositeBar implements ICompositeBar { private _onDidContextMenu: Emitter; private dimension: Dimension; private toDispose: IDisposable[]; private compositeSwitcherBar: ActionBar; private compositeOverflowAction: CompositeOverflowActivityAction; private compositeOverflowActionItem: CompositeOverflowActivityActionItem; private compositeIdToActions: { [compositeId: string]: ActivityAction; }; private compositeIdToActionItems: { [compositeId: string]: IActionItem; }; private compositeIdToActivityStack: { [compositeId: string]: ICompositeActivity[]; }; private compositeSizeInBar: Map; private pinnedComposites: string[]; private activeCompositeId: string; private activeUnpinnedCompositeId: string; constructor( private options: ICompositeBarOptions, @IInstantiationService private instantiationService: IInstantiationService, @IStorageService private storageService: IStorageService, ) { this.toDispose = []; this.compositeIdToActionItems = Object.create(null); this.compositeIdToActions = Object.create(null); this.compositeIdToActivityStack = Object.create(null); this.compositeSizeInBar = new Map(); this._onDidContextMenu = new Emitter(); const pinnedComposites = JSON.parse(this.storageService.get(this.options.storageId, StorageScope.GLOBAL, null)) as string[]; if (pinnedComposites) { this.pinnedComposites = pinnedComposites; } else { this.pinnedComposites = this.options.composites.map(c => c.id); } } public get onDidContextMenu(): Event { return this._onDidContextMenu.event; } public activateComposite(id: string): void { if (this.compositeIdToActions[id]) { this.compositeIdToActions[id].activate(); } this.activeCompositeId = id; const activeUnpinnedCompositeShouldClose = this.activeUnpinnedCompositeId && this.activeUnpinnedCompositeId !== id; const activeUnpinnedCompositeShouldShow = !this.pinnedComposites.some(pid => pid === id); if (activeUnpinnedCompositeShouldShow || activeUnpinnedCompositeShouldClose) { this.updateCompositeSwitcher(); } } public deactivateComposite(id: string): void { if (this.compositeIdToActions[id]) { this.compositeIdToActions[id].deactivate(); } } public showActivity(compositeId: string, badge: IBadge, clazz?: string): IDisposable { if (!badge) { throw illegalArgument('badge'); } const activity = { badge, clazz }; const stack = this.compositeIdToActivityStack[compositeId] || (this.compositeIdToActivityStack[compositeId] = []); stack.unshift(activity); this.updateActivity(compositeId); return { dispose: () => { const stack = this.compositeIdToActivityStack[compositeId]; if (!stack) { return; } const idx = stack.indexOf(activity); if (idx < 0) { return; } stack.splice(idx, 1); if (stack.length === 0) { delete this.compositeIdToActivityStack[compositeId]; } this.updateActivity(compositeId); } }; } private updateActivity(compositeId: string) { const action = this.compositeIdToActions[compositeId]; if (!action) { return; } const stack = this.compositeIdToActivityStack[compositeId]; // reset if (!stack || !stack.length) { action.setBadge(undefined); } // update else { const [{ badge, clazz }] = stack; action.setBadge(badge); if (clazz) { action.class = clazz; } } } public create(parent: HTMLElement): HTMLElement { const actionBarDiv = parent.appendChild(dom.$('.composite-bar')); this.compositeSwitcherBar = new ActionBar(actionBarDiv, { actionItemProvider: (action: Action) => action instanceof CompositeOverflowActivityAction ? this.compositeOverflowActionItem : this.compositeIdToActionItems[action.id], orientation: this.options.orientation, ariaLabel: nls.localize('activityBarAriaLabel', "Active View Switcher"), animated: false, }); // Contextmenu for composites this.toDispose.push(dom.addDisposableListener(parent, dom.EventType.CONTEXT_MENU, (e: MouseEvent) => { dom.EventHelper.stop(e, true); this._onDidContextMenu.fire(e); })); // Allow to drop at the end to move composites to the end this.toDispose.push(dom.addDisposableListener(parent, dom.EventType.DROP, (e: DragEvent) => { const draggedCompositeId = CompositeActionItem.getDraggedCompositeId(); if (draggedCompositeId) { dom.EventHelper.stop(e, true); CompositeActionItem.clearDraggedComposite(); const targetId = this.pinnedComposites[this.pinnedComposites.length - 1]; if (targetId !== draggedCompositeId) { this.move(draggedCompositeId, this.pinnedComposites[this.pinnedComposites.length - 1]); } } })); return actionBarDiv; } public getAction(compositeId): ActivityAction { return this.compositeIdToActions[compositeId]; } private updateCompositeSwitcher(): void { if (!this.compositeSwitcherBar) { return; // We have not been rendered yet so there is nothing to update. } let compositesToShow = this.pinnedComposites; // Always show the active composite even if it is marked to be hidden if (this.activeCompositeId && !compositesToShow.some(id => id === this.activeCompositeId)) { this.activeUnpinnedCompositeId = this.activeCompositeId; compositesToShow = compositesToShow.concat(this.activeUnpinnedCompositeId); } else { this.activeUnpinnedCompositeId = void 0; } // Ensure we are not showing more composites than we have height for let overflows = false; if (this.dimension) { let maxVisible = compositesToShow.length; let size = 0; const limit = this.options.orientation === ActionsOrientation.VERTICAL ? this.dimension.height : this.dimension.width; for (let i = 0; i < compositesToShow.length && size <= limit; i++) { size += this.compositeSizeInBar.get(compositesToShow[i]); if (size > limit) { maxVisible = i; } } overflows = compositesToShow.length > maxVisible; if (overflows) { size -= this.compositeSizeInBar.get(compositesToShow[maxVisible]); compositesToShow = compositesToShow.slice(0, maxVisible); } // Check if we need to make extra room for the overflow action if (overflows && (size + this.options.overflowActionSize > limit)) { compositesToShow.pop(); } } const visibleComposites = Object.keys(this.compositeIdToActions); const visibleCompositesChange = !arrays.equals(compositesToShow, visibleComposites); // Pull out overflow action if there is a composite change so that we can add it to the end later if (this.compositeOverflowAction && visibleCompositesChange) { this.compositeSwitcherBar.pull(this.compositeSwitcherBar.length() - 1); this.compositeOverflowAction.dispose(); this.compositeOverflowAction = null; this.compositeOverflowActionItem.dispose(); this.compositeOverflowActionItem = null; } // Pull out composites that overflow or got hidden visibleComposites.forEach(compositeId => { if (compositesToShow.indexOf(compositeId) === -1) { this.pullComposite(compositeId); } }); // Built actions for composites to show const newCompositesToShow = compositesToShow .filter(compositeId => !this.compositeIdToActions[compositeId]) .map(compositeId => this.toAction(compositeId)); // Update when we have new composites to show if (newCompositesToShow.length) { // Add to composite switcher this.compositeSwitcherBar.push(newCompositesToShow, { label: true, icon: this.options.icon }); // Make sure to activate the active one if (this.activeCompositeId) { const activeCompositeEntry = this.compositeIdToActions[this.activeCompositeId]; if (activeCompositeEntry) { activeCompositeEntry.activate(); } } // Make sure to restore activity Object.keys(this.compositeIdToActions).forEach(compositeId => { this.updateActivity(compositeId); }); } // Add overflow action as needed if (visibleCompositesChange && overflows) { this.compositeOverflowAction = this.instantiationService.createInstance(CompositeOverflowActivityAction, () => this.compositeOverflowActionItem.showMenu()); this.compositeOverflowActionItem = this.instantiationService.createInstance( CompositeOverflowActivityActionItem, this.compositeOverflowAction, () => this.getOverflowingComposites(), () => this.activeCompositeId, (compositeId: string) => this.compositeIdToActivityStack[compositeId] && this.compositeIdToActivityStack[compositeId][0].badge, this.options.getOnCompositeClickAction, this.options.colors ); this.compositeSwitcherBar.push(this.compositeOverflowAction, { label: false, icon: true }); } } private getOverflowingComposites(): { id: string, name: string }[] { let overflowingIds = this.pinnedComposites; if (this.activeUnpinnedCompositeId) { overflowingIds = overflowingIds.concat(this.activeUnpinnedCompositeId); } const visibleComposites = Object.keys(this.compositeIdToActions); overflowingIds = overflowingIds.filter(compositeId => visibleComposites.indexOf(compositeId) === -1); return this.options.composites.filter(c => overflowingIds.indexOf(c.id) !== -1); } private getVisibleComposites(): string[] { return Object.keys(this.compositeIdToActions); } private pullComposite(compositeId: string): void { const index = Object.keys(this.compositeIdToActions).indexOf(compositeId); if (index >= 0) { this.compositeSwitcherBar.pull(index); const action = this.compositeIdToActions[compositeId]; action.dispose(); delete this.compositeIdToActions[compositeId]; const actionItem = this.compositeIdToActionItems[action.id]; actionItem.dispose(); delete this.compositeIdToActionItems[action.id]; } } private toAction(compositeId: string): ActivityAction { if (this.compositeIdToActions[compositeId]) { return this.compositeIdToActions[compositeId]; } const compositeActivityAction = this.options.getActivityAction(compositeId); const pinnedAction = this.options.getCompositePinnedAction(compositeId); this.compositeIdToActionItems[compositeId] = this.instantiationService.createInstance(CompositeActionItem, compositeActivityAction, pinnedAction, this.options.colors, this.options.icon, this); this.compositeIdToActions[compositeId] = compositeActivityAction; return compositeActivityAction; } public unpin(compositeId: string): void { if (!this.isPinned(compositeId)) { return; } const defaultCompositeId = this.options.getDefaultCompositeId(); const visibleComposites = this.getVisibleComposites(); let unpinPromise: TPromise; // Case: composite is not the active one or the active one is a different one // Solv: we do nothing if (!this.activeCompositeId || this.activeCompositeId !== compositeId) { unpinPromise = TPromise.as(null); } // Case: composite is not the default composite and default composite is still showing // Solv: we open the default composite else if (defaultCompositeId !== compositeId && this.isPinned(defaultCompositeId)) { unpinPromise = this.options.openComposite(defaultCompositeId); } // Case: we closed the last visible composite // Solv: we hide the part else if (visibleComposites.length === 1) { unpinPromise = this.options.hidePart(); } // Case: we closed the default composite // Solv: we open the next visible composite from top else { unpinPromise = this.options.openComposite(visibleComposites.filter(cid => cid !== compositeId)[0]); } unpinPromise.then(() => { // then remove from pinned and update switcher const index = this.pinnedComposites.indexOf(compositeId); this.pinnedComposites.splice(index, 1); this.updateCompositeSwitcher(); }); } public isPinned(compositeId: string): boolean { return this.pinnedComposites.indexOf(compositeId) >= 0; } public pin(compositeId: string, update = true): void { if (this.isPinned(compositeId)) { return; } this.options.openComposite(compositeId).then(() => { this.pinnedComposites.push(compositeId); this.pinnedComposites = arrays.distinct(this.pinnedComposites); if (update) { this.updateCompositeSwitcher(); } }); } public move(compositeId: string, toCompositeId: string): void { // Make sure a moved composite gets pinned if (!this.isPinned(compositeId)) { this.pin(compositeId, false /* defer update, we take care of it */); } const fromIndex = this.pinnedComposites.indexOf(compositeId); const toIndex = this.pinnedComposites.indexOf(toCompositeId); this.pinnedComposites.splice(fromIndex, 1); this.pinnedComposites.splice(toIndex, 0, compositeId); // Clear composites that are impacted by the move const visibleComposites = Object.keys(this.compositeIdToActions); for (let i = Math.min(fromIndex, toIndex); i < visibleComposites.length; i++) { this.pullComposite(visibleComposites[i]); } // timeout helps to prevent artifacts from showing up setTimeout(() => { this.updateCompositeSwitcher(); }, 0); } public layout(dimension: Dimension): void { this.dimension = dimension; if (dimension.height === 0 || dimension.width === 0) { // Do not layout if not visible. Otherwise the size measurment would be computed wrongly return; } if (this.compositeSizeInBar.size === 0) { // Compute size of each composite by getting the size from the css renderer // Size is later used for overflow computation this.compositeSwitcherBar.clear(); this.compositeSwitcherBar.push(this.options.composites.map(c => this.options.getActivityAction(c.id))); this.options.composites.map((c, index) => this.compositeSizeInBar.set(c.id, this.options.orientation === ActionsOrientation.VERTICAL ? this.compositeSwitcherBar.getHeight(index) : this.compositeSwitcherBar.getWidth(index) )); this.compositeSwitcherBar.clear(); } this.updateCompositeSwitcher(); } public store(): void { this.storageService.store(this.options.storageId, JSON.stringify(this.pinnedComposites), StorageScope.GLOBAL); } public dispose(): void { this.toDispose = dispose(this.toDispose); } }