activitybarPart.ts 14.9 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
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
24
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
25 26 27 28 29 30
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 已提交
31

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

37
export class ActivitybarPart extends Part implements IActivityBarService {
38

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

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

44 45
	private dimension: Dimension;

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

50
	private viewletIdToActions: { [viewletId: string]: ActivityAction; };
B
Benjamin Pasero 已提交
51
	private viewletIdToActionItems: { [viewletId: string]: IActionItem; };
52
	private viewletIdToActivity: { [viewletId: string]: IViewletActivity; };
E
Erich Gamma 已提交
53

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

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

B
Benjamin Pasero 已提交
69
		this.viewletIdToActionItems = Object.create(null);
70 71
		this.viewletIdToActions = Object.create(null);
		this.viewletIdToActivity = Object.create(null);
E
Erich Gamma 已提交
72

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

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

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

	private registerListeners(): void {

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

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

91 92 93 94 95 96 97 98 99 100 101
	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 已提交
102 103 104
		}
	}

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

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

113 114 115
	public showActivity(viewletId: string, badge?: IBadge, clazz?: string): void {

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

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

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

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

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

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

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

E
Erich Gamma 已提交
150 151 152
		return $result;
	}

153 154 155 156 157 158 159 160 161 162 163 164 165 166
	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 已提交
167 168
	private createViewletSwitcher(div: Builder): void {
		this.viewletSwitcherBar = new ActionBar(div, {
B
Benjamin Pasero 已提交
169
			actionItemProvider: (action: Action) => action instanceof ViewletOverflowActivityAction ? this.viewletOverflowActionItem : this.viewletIdToActionItems[action.id],
170
			orientation: ActionsOrientation.VERTICAL,
171 172
			ariaLabel: nls.localize('activityBarAriaLabel', "Active View Switcher"),
			animated: false
E
Erich Gamma 已提交
173 174
		});

175
		this.updateViewletSwitcher();
P
Pine Wu 已提交
176 177
	}

178
	private updateViewletSwitcher() {
179 180 181 182
		let viewletsToShow = this.getPinnedViewlets();

		// Always show the active viewlet even if it is marked to be hidden
		const activeViewlet = this.viewletService.getActiveViewlet();
B
Benjamin Pasero 已提交
183
		if (activeViewlet && !viewletsToShow.some(viewlet => viewlet.id === activeViewlet.getId())) {
184 185 186 187 188
			this.activeUnpinnedViewlet = this.viewletService.getViewlet(activeViewlet.getId());
			viewletsToShow.push(this.activeUnpinnedViewlet);
		} else {
			this.activeUnpinnedViewlet = void 0;
		}
189 190 191 192 193

		// 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);
194
			overflows = viewletsToShow.length > maxVisible;
195 196

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

		const visibleViewlets = Object.keys(this.viewletIdToActions);
B
Benjamin Pasero 已提交
202
		const visibleViewletsChange = !arrays.equals(viewletsToShow.map(viewlet => viewlet.id), visibleViewlets);
203 204 205 206 207 208 209 210 211 212 213

		// 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;
		}
214

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

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

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

231 232 233 234 235 236 237 238 239 240
			// 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();
				}
241
			}
242 243 244 245 246 247 248 249 250 251 252 253 254 255

			// 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) {
256 257
			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);
258 259

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

263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280
	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[] {
B
Benjamin Pasero 已提交
281
		return this.pinnedViewlets.map(viewletId => this.viewletService.getViewlet(viewletId)).filter(v => !!v); // ensure to remove those that might no longer exist
282 283
	}

P
Pine Wu 已提交
284
	private pullViewlet(viewletId: string): void {
285
		const index = Object.keys(this.viewletIdToActions).indexOf(viewletId);
B
Benjamin Pasero 已提交
286 287
		if (index >= 0) {
			this.viewletSwitcherBar.pull(index);
288

B
Benjamin Pasero 已提交
289 290 291
			const action = this.viewletIdToActions[viewletId];
			action.dispose();
			delete this.viewletIdToActions[viewletId];
292

B
Benjamin Pasero 已提交
293
			const actionItem = this.viewletIdToActionItems[action.id];
B
Benjamin Pasero 已提交
294
			actionItem.dispose();
B
Benjamin Pasero 已提交
295
			delete this.viewletIdToActionItems[action.id];
B
Benjamin Pasero 已提交
296
		}
I
isidor 已提交
297
	}
J
Joao Moreno 已提交
298

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

B
Benjamin Pasero 已提交
302
		this.viewletIdToActionItems[action.id] = this.instantiationService.createInstance(ActivityActionItem, action, viewlet);
303
		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
	}

B
Benjamin Pasero 已提交
357
	public pin(viewletId: string, update = true): void {
358 359 360 361 362 363 364 365
		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

B
Benjamin Pasero 已提交
369 370 371
			if (update) {
				this.updateViewletSwitcher();
			}
372 373 374
		});
	}

375
	public move(viewletId: string, toViewletId: string): void {
B
Benjamin Pasero 已提交
376 377 378 379 380 381

		// Make sure a moved viewlet gets pinned
		if (!this.isPinned(viewletId)) {
			this.pin(viewletId, false /* defer update, we take care of it */);
		}

382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
		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);
	}

400 401 402 403 404 405 406 407 408 409 410 411 412 413 414
	/**
	 * 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 已提交
415

E
Erich Gamma 已提交
416
	public dispose(): void {
I
isidor 已提交
417 418 419 420 421
		if (this.viewletSwitcherBar) {
			this.viewletSwitcherBar.dispose();
			this.viewletSwitcherBar = null;
		}

E
Erich Gamma 已提交
422 423
		super.dispose();
	}
424 425 426 427

	public shutdown(): void {

		// Persist Hidden State
428
		this.memento[ActivitybarPart.PINNED_VIEWLETS] = this.pinnedViewlets;
429 430 431 432

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