activitybarPart.ts 14.8 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

B
Benjamin Pasero 已提交
8
import 'vs/css!./media/activitybarpart';
9
import nls = require('vs/nls');
10 11 12
import { TPromise } from 'vs/base/common/winjs.base';
import DOM = require('vs/base/browser/dom');
import * as arrays from 'vs/base/common/arrays';
13
import { Builder, $, Dimension } from 'vs/base/browser/builder';
J
Johannes Rieken 已提交
14
import { Action } from 'vs/base/common/actions';
15
import { ActionsOrientation, ActionBar, IActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
16
import { ViewletDescriptor } from 'vs/workbench/browser/viewlet';
J
Johannes Rieken 已提交
17
import { Part } from 'vs/workbench/browser/part';
18 19
import { IViewlet } from 'vs/workbench/common/viewlet';
import { ToggleViewletPinnedAction, ViewletActivityAction, ActivityAction, ActivityActionItem, ViewletOverflowActivityAction, ViewletOverflowActivityActionItem } from 'vs/workbench/browser/parts/activitybar/activitybarActions';
B
Benjamin Pasero 已提交
20
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
21
import { IActivityBarService, IBadge } from 'vs/workbench/services/activity/common/activityBarService';
22
import { IPartService } from 'vs/workbench/services/part/common/partService';
J
Johannes Rieken 已提交
23 24
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
25
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
26 27 28 29 30 31
import { IStorageService } from 'vs/platform/storage/common/storage';
import { Scope as MementoScope } from 'vs/workbench/common/memento';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { dispose } from 'vs/base/common/lifecycle';
import { ToggleActivityBarVisibilityAction } from 'vs/workbench/browser/actions/toggleActivityBarVisibility';
E
Erich Gamma 已提交
32

33 34 35 36 37
interface IViewletActivity {
	badge: IBadge;
	clazz: string;
}

38
export class ActivitybarPart extends Part implements IActivityBarService {
39

40
	private static readonly ACTIVITY_ACTION_HEIGHT = 50;
41
	private static readonly PINNED_VIEWLETS = 'workbench.activity.pinnedViewlets';
42

43
	public _serviceBrand: any;
P
Pine Wu 已提交
44

45 46
	private dimension: Dimension;

I
isidor 已提交
47
	private viewletSwitcherBar: ActionBar;
48 49 50
	private viewletOverflowAction: ViewletOverflowActivityAction;
	private viewletOverflowActionItem: ViewletOverflowActivityActionItem;

E
Erich Gamma 已提交
51
	private activityActionItems: { [actionId: string]: IActionItem; };
52
	private viewletIdToActions: { [viewletId: string]: ActivityAction; };
53
	private viewletIdToActivity: { [viewletId: string]: IViewletActivity; };
E
Erich Gamma 已提交
54

55
	private memento: any;
56
	private pinnedViewlets: string[];
57 58
	private activeUnpinnedViewlet: ViewletDescriptor;

E
Erich Gamma 已提交
59
	constructor(
60 61
		id: string,
		@IViewletService private viewletService: IViewletService,
62
		@IExtensionService private extensionService: IExtensionService,
63
		@IKeybindingService private keybindingService: IKeybindingService,
64 65
		@IStorageService private storageService: IStorageService,
		@IContextMenuService private contextMenuService: IContextMenuService,
I
isidor 已提交
66
		@IInstantiationService private instantiationService: IInstantiationService,
67
		@IPartService private partService: IPartService
E
Erich Gamma 已提交
68 69 70
	) {
		super(id);

71 72 73
		this.activityActionItems = Object.create(null);
		this.viewletIdToActions = Object.create(null);
		this.viewletIdToActivity = Object.create(null);
E
Erich Gamma 已提交
74

75
		this.memento = this.getMemento(this.storageService, MementoScope.GLOBAL);
76
		this.pinnedViewlets = this.memento[ActivitybarPart.PINNED_VIEWLETS] || this.viewletService.getViewlets().map(v => v.id);
77

78
		// Update viewlet switcher when external viewlets become ready
79
		this.extensionService.onReady().then(() => this.updateViewletSwitcher());
80

E
Erich Gamma 已提交
81 82 83 84 85 86
		this.registerListeners();
	}

	private registerListeners(): void {

		// Activate viewlet action on opening of a viewlet
87
		this.toUnbind.push(this.viewletService.onDidViewletOpen(viewlet => this.onDidViewletOpen(viewlet)));
E
Erich Gamma 已提交
88 89

		// Deactivate viewlet action on close
90
		this.toUnbind.push(this.viewletService.onDidViewletClose(viewlet => this.onDidViewletClose(viewlet)));
P
Pine Wu 已提交
91 92
	}

93 94 95 96 97 98 99 100 101 102 103
	private onDidViewletOpen(viewlet: IViewlet): void {
		const id = viewlet.getId();

		if (this.viewletIdToActions[id]) {
			this.viewletIdToActions[id].activate();
		}

		const activeUnpinnedViewletShouldClose = this.activeUnpinnedViewlet && this.activeUnpinnedViewlet.id !== viewlet.getId();
		const activeUnpinnedViewletShouldShow = !this.getPinnedViewlets().some(v => v.id === viewlet.getId());
		if (activeUnpinnedViewletShouldShow || activeUnpinnedViewletShouldClose) {
			this.updateViewletSwitcher();
E
Erich Gamma 已提交
104 105 106
		}
	}

107 108 109 110 111
	private onDidViewletClose(viewlet: IViewlet): void {
		const id = viewlet.getId();

		if (this.viewletIdToActions[id]) {
			this.viewletIdToActions[id].deactivate();
E
Erich Gamma 已提交
112 113 114
		}
	}

115 116 117
	public showActivity(viewletId: string, badge?: IBadge, clazz?: string): void {

		// Update Action with activity
118
		const action = this.viewletIdToActions[viewletId];
E
Erich Gamma 已提交
119 120 121 122 123 124
		if (action) {
			action.setBadge(badge);
			if (clazz) {
				action.class = clazz;
			}
		}
125 126 127 128 129 130 131

		// Keep for future use
		if (badge) {
			this.viewletIdToActivity[viewletId] = { badge, clazz };
		} else {
			delete this.viewletIdToActivity[viewletId];
		}
E
Erich Gamma 已提交
132 133
	}

134 135
	public clearActivity(viewletId: string): void {
		this.showActivity(viewletId, null);
E
Erich Gamma 已提交
136 137 138
	}

	public createContentArea(parent: Builder): Builder {
139 140
		const $el = $(parent);
		const $result = $('.content').appendTo($el);
E
Erich Gamma 已提交
141 142

		// Top Actionbar with action items for each viewlet action
P
Pine Wu 已提交
143
		this.createViewletSwitcher($result.clone());
E
Erich Gamma 已提交
144

145 146 147 148 149 150 151
		// Contextmenu for viewlets
		$(parent).on('contextmenu', (e: MouseEvent) => {
			DOM.EventHelper.stop(e, true);

			this.showContextMenu(e);
		}, this.toUnbind);

E
Erich Gamma 已提交
152 153 154
		return $result;
	}

155 156 157 158 159 160 161 162 163 164 165 166 167 168
	private showContextMenu(e: MouseEvent): void {
		const event = new StandardMouseEvent(e);

		const actions: Action[] = this.viewletService.getViewlets().map(viewlet => this.instantiationService.createInstance(ToggleViewletPinnedAction, viewlet));
		actions.push(new Separator());
		actions.push(this.instantiationService.createInstance(ToggleActivityBarVisibilityAction, ToggleActivityBarVisibilityAction.ID, nls.localize('hideActivitBar', "Hide Activity Bar")));

		this.contextMenuService.showContextMenu({
			getAnchor: () => { return { x: event.posx + 1, y: event.posy }; },
			getActions: () => TPromise.as(actions),
			onHide: () => dispose(actions)
		});
	}

I
isidor 已提交
169 170
	private createViewletSwitcher(div: Builder): void {
		this.viewletSwitcherBar = new ActionBar(div, {
171
			actionItemProvider: (action: Action) => action instanceof ViewletOverflowActivityAction ? this.viewletOverflowActionItem : this.activityActionItems[action.id],
172
			orientation: ActionsOrientation.VERTICAL,
173 174
			ariaLabel: nls.localize('activityBarAriaLabel', "Active View Switcher"),
			animated: false
E
Erich Gamma 已提交
175 176
		});

177
		this.updateViewletSwitcher();
P
Pine Wu 已提交
178 179
	}

180
	private updateViewletSwitcher() {
181 182 183 184 185 186 187 188 189 190
		let viewletsToShow = this.getPinnedViewlets();

		// Always show the active viewlet even if it is marked to be hidden
		const activeViewlet = this.viewletService.getActiveViewlet();
		if (activeViewlet && !viewletsToShow.some(v => v.id === activeViewlet.getId())) {
			this.activeUnpinnedViewlet = this.viewletService.getViewlet(activeViewlet.getId());
			viewletsToShow.push(this.activeUnpinnedViewlet);
		} else {
			this.activeUnpinnedViewlet = void 0;
		}
191 192 193 194 195

		// Ensure we are not showing more viewlets than we have height for
		let overflows = false;
		if (this.dimension) {
			const maxVisible = Math.floor(this.dimension.height / ActivitybarPart.ACTIVITY_ACTION_HEIGHT);
196
			overflows = viewletsToShow.length > maxVisible;
197 198

			if (overflows) {
199
				viewletsToShow = viewletsToShow.slice(0, maxVisible - 1 /* make room for overflow action */);
200 201 202 203
			}
		}

		const visibleViewlets = Object.keys(this.viewletIdToActions);
204
		const visibleViewletsChange = !arrays.equals(viewletsToShow.map(v => v.id), visibleViewlets);
205 206 207 208 209 210 211 212 213 214 215

		// Pull out overflow action if there is a viewlet change so that we can add it to the end later
		if (this.viewletOverflowAction && visibleViewletsChange) {
			this.viewletSwitcherBar.pull(this.viewletSwitcherBar.length() - 1);

			this.viewletOverflowAction.dispose();
			this.viewletOverflowAction = null;

			this.viewletOverflowActionItem.dispose();
			this.viewletOverflowActionItem = null;
		}
216

217
		// Pull out viewlets that overflow or got hidden
218 219 220
		const viewletIdsToShow = viewletsToShow.map(v => v.id);
		visibleViewlets.forEach(viewletId => {
			if (viewletIdsToShow.indexOf(viewletId) === -1) {
P
Pine Wu 已提交
221 222 223 224
				this.pullViewlet(viewletId);
			}
		});

225
		// Built actions for viewlets to show
226
		const newViewletsToShow = viewletsToShow
227
			.filter(viewlet => !this.viewletIdToActions[viewlet.id])
P
Pine Wu 已提交
228 229
			.map(viewlet => this.toAction(viewlet));

230 231
		// Update when we have new viewlets to show
		if (newViewletsToShow.length) {
232

233 234 235 236 237 238 239 240 241 242
			// Add to viewlet switcher
			this.viewletSwitcherBar.push(newViewletsToShow, { label: true, icon: true });

			// Make sure to activate the active one
			const activeViewlet = this.viewletService.getActiveViewlet();
			if (activeViewlet) {
				const activeViewletEntry = this.viewletIdToActions[activeViewlet.getId()];
				if (activeViewletEntry) {
					activeViewletEntry.activate();
				}
243
			}
244 245 246 247 248 249 250 251 252 253 254 255 256 257

			// Make sure to restore activity
			Object.keys(this.viewletIdToActions).forEach(viewletId => {
				const activity = this.viewletIdToActivity[viewletId];
				if (activity) {
					this.showActivity(viewletId, activity.badge, activity.clazz);
				} else {
					this.showActivity(viewletId);
				}
			});
		}

		// Add overflow action as needed
		if (visibleViewletsChange && overflows) {
258 259
			this.viewletOverflowAction = this.instantiationService.createInstance(ViewletOverflowActivityAction, () => this.viewletOverflowActionItem.showMenu());
			this.viewletOverflowActionItem = this.instantiationService.createInstance(ViewletOverflowActivityActionItem, this.viewletOverflowAction, () => this.getOverflowingViewlets(), viewlet => this.viewletIdToActivity[viewlet.id] && this.viewletIdToActivity[viewlet.id].badge);
260 261

			this.viewletSwitcherBar.push(this.viewletOverflowAction, { label: true, icon: true });
262
		}
P
Pine Wu 已提交
263 264
	}

265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
	private getOverflowingViewlets(): ViewletDescriptor[] {
		const viewlets = this.getPinnedViewlets();
		if (this.activeUnpinnedViewlet) {
			viewlets.push(this.activeUnpinnedViewlet);
		}
		const visibleViewlets = Object.keys(this.viewletIdToActions);

		return viewlets.filter(viewlet => visibleViewlets.indexOf(viewlet.id) === -1);
	}

	private getVisibleViewlets(): ViewletDescriptor[] {
		const viewlets = this.viewletService.getViewlets();
		const visibleViewlets = Object.keys(this.viewletIdToActions);

		return viewlets.filter(viewlet => visibleViewlets.indexOf(viewlet.id) >= 0);
	}

	private getPinnedViewlets(): ViewletDescriptor[] {
283
		return this.pinnedViewlets.map(viewletId => this.viewletService.getViewlet(viewletId));
284 285
	}

P
Pine Wu 已提交
286
	private pullViewlet(viewletId: string): void {
287
		const index = Object.keys(this.viewletIdToActions).indexOf(viewletId);
288
		this.viewletSwitcherBar.pull(index);
289

290
		const action = this.viewletIdToActions[viewletId];
P
Pine Wu 已提交
291
		action.dispose();
292
		delete this.viewletIdToActions[viewletId];
293 294

		const actionItem = this.activityActionItems[action.id];
P
Pine Wu 已提交
295
		actionItem.dispose();
296
		delete this.activityActionItems[action.id];
I
isidor 已提交
297
	}
J
Joao Moreno 已提交
298

299
	private toAction(viewlet: ViewletDescriptor): ActivityAction {
300
		const action = this.instantiationService.createInstance(ViewletActivityAction, viewlet);
I
isidor 已提交
301

302 303
		this.activityActionItems[action.id] = this.instantiationService.createInstance(ActivityActionItem, action, viewlet);
		this.viewletIdToActions[viewlet.id] = action;
I
isidor 已提交
304 305

		return action;
306 307
	}

308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
	public unpin(viewletId: string): void {
		if (!this.isPinned(viewletId)) {
			return;
		}

		const activeViewlet = this.viewletService.getActiveViewlet();
		const defaultViewletId = this.viewletService.getDefaultViewletId();
		const visibleViewlets = this.getVisibleViewlets();

		let unpinPromise: TPromise<any>;

		// Case: viewlet is not the active one or the active one is a different one
		// Solv: we do nothing
		if (!activeViewlet || activeViewlet.getId() !== viewletId) {
			unpinPromise = TPromise.as(null);
		}

		// Case: viewlet is not the default viewlet and default viewlet is still showing
		// Solv: we open the default viewlet
		else if (defaultViewletId !== viewletId && this.isPinned(defaultViewletId)) {
			unpinPromise = this.viewletService.openViewlet(defaultViewletId, true);
		}

		// Case: we closed the last visible viewlet
		// Solv: we hide the sidebar
		else if (visibleViewlets.length === 1) {
			unpinPromise = TPromise.as(this.partService.setSideBarHidden(true));
		}

		// Case: we closed the default viewlet
		// Solv: we open the next visible viewlet from top
		else {
			unpinPromise = this.viewletService.openViewlet(visibleViewlets.filter(viewlet => viewlet.id !== viewletId)[0].id, true);
		}

		unpinPromise.then(() => {

345 346 347
			// then remove from pinned and update switcher
			const index = this.pinnedViewlets.indexOf(viewletId);
			this.pinnedViewlets.splice(index, 1);
348 349 350 351 352 353

			this.updateViewletSwitcher();
		});
	}

	public isPinned(viewletId: string): boolean {
354
		return this.pinnedViewlets.indexOf(viewletId) >= 0;
355 356 357 358 359 360 361 362 363 364 365
	}

	public pin(viewletId: string): void {
		if (this.isPinned(viewletId)) {
			return;
		}

		// first open that viewlet
		this.viewletService.openViewlet(viewletId, true).then(() => {

			// then update
366 367
			this.pinnedViewlets.push(viewletId);
			this.pinnedViewlets = arrays.distinct(this.pinnedViewlets);
368 369 370 371 372

			this.updateViewletSwitcher();
		});
	}

373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
	public move(viewletId: string, toViewletId: string): void {
		const fromIndex = this.pinnedViewlets.indexOf(viewletId);
		const toIndex = this.pinnedViewlets.indexOf(toViewletId);

		this.pinnedViewlets.splice(fromIndex, 1);
		this.pinnedViewlets.splice(toIndex, 0, viewletId);

		// Clear viewlets that are impacted by the move
		const visibleViewlets = Object.keys(this.viewletIdToActions);
		for (let i = Math.min(fromIndex, toIndex); i < visibleViewlets.length; i++) {
			this.pullViewlet(visibleViewlets[i]);
		}

		// timeout helps to prevent artifacts from showing up
		setTimeout(() => {
			this.updateViewletSwitcher();
		}, 0);
	}

392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
	/**
	 * Layout title, content and status area in the given dimension.
	 */
	public layout(dimension: Dimension): Dimension[] {

		// Pass to super
		const sizes = super.layout(dimension);

		this.dimension = sizes[1];

		// Update switcher to handle overflow issues
		this.updateViewletSwitcher();

		return sizes;
	}
I
isidor 已提交
407

E
Erich Gamma 已提交
408
	public dispose(): void {
I
isidor 已提交
409 410 411 412 413
		if (this.viewletSwitcherBar) {
			this.viewletSwitcherBar.dispose();
			this.viewletSwitcherBar = null;
		}

E
Erich Gamma 已提交
414 415
		super.dispose();
	}
416 417 418 419

	public shutdown(): void {

		// Persist Hidden State
420
		this.memento[ActivitybarPart.PINNED_VIEWLETS] = this.pinnedViewlets;
421 422 423 424

		// Pass to super
		super.shutdown();
	}
425
}