tabsTitleControl.ts 14.4 KB
Newer Older
B
wip  
Benjamin Pasero 已提交
1 2 3 4 5 6 7 8
/*---------------------------------------------------------------------------------------------
 *  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/tabstitle';
B
Benjamin Pasero 已提交
9
import {TPromise} from 'vs/base/common/winjs.base';
B
Benjamin Pasero 已提交
10
import nls = require('vs/nls');
B
Benjamin Pasero 已提交
11
import {IAction} from 'vs/base/common/actions';
12
import {prepareActions} from 'vs/workbench/browser/actionBarRegistry';
B
wip  
Benjamin Pasero 已提交
13
import arrays = require('vs/base/common/arrays');
B
Benjamin Pasero 已提交
14 15
import errors = require('vs/base/common/errors');
import DOM = require('vs/base/browser/dom');
B
wip  
Benjamin Pasero 已提交
16
import {Builder, $} from 'vs/base/browser/builder';
B
Benjamin Pasero 已提交
17
import {IEditorGroup, IEditorIdentifier} from 'vs/workbench/common/editor';
B
wip  
Benjamin Pasero 已提交
18
import {ToolBar} from 'vs/base/browser/ui/toolbar/toolbar';
B
Benjamin Pasero 已提交
19 20
import {ActionBar, Separator} from 'vs/base/browser/ui/actionbar/actionbar';
import {StandardMouseEvent} from 'vs/base/browser/mouseEvent';
B
wip  
Benjamin Pasero 已提交
21 22 23 24 25 26 27 28
import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService';
import {IContextMenuService} from 'vs/platform/contextview/browser/contextView';
import {IEditorGroupService} from 'vs/workbench/services/group/common/groupService';
import {IMessageService} from 'vs/platform/message/common/message';
import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {IKeybindingService} from 'vs/platform/keybinding/common/keybindingService';
import {TitleControl} from 'vs/workbench/browser/parts/editor/titleControl';
B
Benjamin Pasero 已提交
29
import {IDisposable, dispose} from 'vs/base/common/lifecycle';
B
wip  
Benjamin Pasero 已提交
30 31

export class TabsTitleControl extends TitleControl {
B
Benjamin Pasero 已提交
32 33 34
	private titleContainer: HTMLElement;
	private tabsContainer: HTMLElement;
	private activeTab: HTMLElement;
B
wip  
Benjamin Pasero 已提交
35 36

	private groupActionsToolbar: ToolBar;
B
Benjamin Pasero 已提交
37
	private tabDisposeables: IDisposable[];
B
wip  
Benjamin Pasero 已提交
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54

	private currentPrimaryGroupActionIds: string[];
	private currentSecondaryGroupActionIds: string[];

	constructor(
		@IContextMenuService contextMenuService: IContextMenuService,
		@IInstantiationService instantiationService: IInstantiationService,
		@IWorkbenchEditorService editorService: IWorkbenchEditorService,
		@IEditorGroupService editorGroupService: IEditorGroupService,
		@IKeybindingService keybindingService: IKeybindingService,
		@ITelemetryService telemetryService: ITelemetryService,
		@IMessageService messageService: IMessageService
	) {
		super(contextMenuService, instantiationService, editorService, editorGroupService, keybindingService, telemetryService, messageService);

		this.currentPrimaryGroupActionIds = [];
		this.currentSecondaryGroupActionIds = [];
B
Benjamin Pasero 已提交
55

B
Benjamin Pasero 已提交
56
		this.tabDisposeables = [];
B
wip  
Benjamin Pasero 已提交
57 58 59 60 61 62 63 64 65
	}

	public setContext(group: IEditorGroup): void {
		super.setContext(group);

		this.groupActionsToolbar.context = { group };
	}

	public create(parent: Builder): void {
B
Benjamin Pasero 已提交
66
		this.titleContainer = parent.getHTMLElement();
B
wip  
Benjamin Pasero 已提交
67

68
		// Tabs Container
B
Benjamin Pasero 已提交
69 70 71 72
		this.tabsContainer = document.createElement('div');
		DOM.addClass(this.tabsContainer, 'tabs-container');
		this.titleContainer.appendChild(this.tabsContainer);

B
Benjamin Pasero 已提交
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
		// Drag over
		this.toDispose.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DRAG_OVER, (e: DragEvent) => {
			const target = e.target;
			if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) {
				DOM.addClass(this.tabsContainer, 'dropfeedback');
			}
		}));

		// Drag leave
		this.toDispose.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DRAG_LEAVE, (e: DragEvent) => {
			DOM.removeClass(this.tabsContainer, 'dropfeedback');
		}));

		// Drag end
		this.toDispose.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DRAG_END, (e: DragEvent) => {
			DOM.removeClass(this.tabsContainer, 'dropfeedback');
		}));

B
Benjamin Pasero 已提交
91 92
		// Drop onto tabs container
		this.toDispose.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DROP, (e: DragEvent) => {
B
Benjamin Pasero 已提交
93 94
			DOM.removeClass(this.tabsContainer, 'dropfeedback');

B
Benjamin Pasero 已提交
95
			const target = e.target;
B
Benjamin Pasero 已提交
96
			if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) {
B
Benjamin Pasero 已提交
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
				const group = this.context;
				if (group) {
					const identifier = this.stringToId(e.dataTransfer.getData('text'));
					if (identifier) {
						e.preventDefault();

						const sourcePosition = this.stacks.positionOfGroup(identifier.group);
						const targetPosition = this.stacks.positionOfGroup(group);

						// Move editor to target position at the end
						this.editorGroupService.moveEditor(identifier.editor, sourcePosition, targetPosition, group.count);
					}
				}
			}
		}));

B
Benjamin Pasero 已提交
113 114 115 116 117 118 119
		// Convert mouse wheel vertical scroll to horizontal
		this.toDispose.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.WHEEL, (e: WheelEvent) => {
			if (e.deltaY && !e.deltaX) {
				DOM.EventHelper.stop(e);
				this.tabsContainer.scrollLeft += e.deltaY;
			}
		}));
120

B
Benjamin Pasero 已提交
121
		// Group Actions
B
Benjamin Pasero 已提交
122 123 124 125
		const groupActionsContainer = document.createElement('div');
		DOM.addClass(groupActionsContainer, 'group-actions');
		this.titleContainer.appendChild(groupActionsContainer);
		this.groupActionsToolbar = this.doCreateToolbar($(groupActionsContainer));
B
wip  
Benjamin Pasero 已提交
126 127
	}

B
Benjamin Pasero 已提交
128 129 130 131
	public allowDragging(element: HTMLElement): boolean {
		return (element.className === 'tabs-container');
	}

B
wip  
Benjamin Pasero 已提交
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
	public refresh(): void {
		if (!this.context) {
			return;
		}

		const group = this.context;
		const editor = group.activeEditor;
		if (!editor) {
			this.groupActionsToolbar.setActions([], [])();

			this.currentPrimaryGroupActionIds = [];
			this.currentSecondaryGroupActionIds = [];

			return; // return early if we are being closed
		}

148
		// Activity state
B
Benjamin Pasero 已提交
149
		const isActive = this.stacks.isActive(group);
150
		if (isActive) {
B
Benjamin Pasero 已提交
151
			DOM.addClass(this.titleContainer, 'active');
152
		} else {
B
Benjamin Pasero 已提交
153
			DOM.removeClass(this.titleContainer, 'active');
154 155
		}

B
wip  
Benjamin Pasero 已提交
156 157
		// Update Group Actions Toolbar
		const groupActions = this.getGroupActions(group);
B
Benjamin Pasero 已提交
158 159
		const primaryGroupActions = groupActions.primary;
		const secondaryGroupActions = groupActions.secondary;
B
wip  
Benjamin Pasero 已提交
160 161 162 163 164 165 166 167
		const primaryGroupActionIds = primaryGroupActions.map(a => a.id);
		const secondaryGroupActionIds = secondaryGroupActions.map(a => a.id);

		if (!arrays.equals(primaryGroupActionIds, this.currentPrimaryGroupActionIds) || !arrays.equals(secondaryGroupActionIds, this.currentSecondaryGroupActionIds)) {
			this.groupActionsToolbar.setActions(primaryGroupActions, secondaryGroupActions)();
			this.currentPrimaryGroupActionIds = primaryGroupActionIds;
			this.currentSecondaryGroupActionIds = secondaryGroupActionIds;
		}
B
Benjamin Pasero 已提交
168 169 170

		// Refresh Tabs
		this.refreshTabs(group);
B
wip  
Benjamin Pasero 已提交
171 172
	}

B
Benjamin Pasero 已提交
173 174 175
	private refreshTabs(group: IEditorGroup): void {

		// Empty container first
B
Benjamin Pasero 已提交
176 177 178 179 180
		DOM.clearNode(this.tabsContainer);
		dispose(this.tabDisposeables);
		this.tabDisposeables = [];

		const tabContainers: HTMLElement[] = [];
B
Benjamin Pasero 已提交
181 182 183 184 185 186 187

		// Add a tab for each opened editor
		this.context.getEditors().forEach(editor => {
			const isPinned = group.isPinned(editor);
			const isActive = group.isActive(editor);
			const isDirty = editor.isDirty();

B
Benjamin Pasero 已提交
188
			const tabContainer = document.createElement('div');
B
Benjamin Pasero 已提交
189
			tabContainer.draggable = true;
B
Benjamin Pasero 已提交
190 191
			DOM.addClass(tabContainer, 'tab monaco-editor-background');
			tabContainers.push(tabContainer);
B
Benjamin Pasero 已提交
192

B
Benjamin Pasero 已提交
193 194 195 196 197 198
			// Pinned state
			if (isPinned) {
				DOM.addClass(tabContainer, 'pinned');
			} else {
				DOM.removeClass(tabContainer, 'pinned');
			}
B
Benjamin Pasero 已提交
199

B
Benjamin Pasero 已提交
200 201 202 203 204 205 206
			// Active state
			if (isActive) {
				DOM.addClass(tabContainer, 'active');
				this.activeTab = tabContainer;
			} else {
				DOM.removeClass(tabContainer, 'active');
			}
B
Benjamin Pasero 已提交
207

B
Benjamin Pasero 已提交
208 209 210 211 212 213
			// Dirty State
			if (isDirty) {
				DOM.addClass(tabContainer, 'dirty');
			} else {
				DOM.removeClass(tabContainer, 'dirty');
			}
B
Benjamin Pasero 已提交
214

B
Benjamin Pasero 已提交
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234
			// Tab Label Container
			const tabLabelContainer = document.createElement('div');
			tabContainer.appendChild(tabLabelContainer);
			DOM.addClass(tabLabelContainer, 'tab-label');

			// Tab Label
			const tabLabel = document.createElement('a');
			tabLabel.innerText = editor.getName();
			tabLabel.title = editor.getDescription(true) || '';
			tabLabelContainer.appendChild(tabLabel);

			// Tab Close
			const tabCloseContainer = document.createElement('div');
			DOM.addClass(tabCloseContainer, 'tab-close');
			tabContainer.appendChild(tabCloseContainer);

			const bar = new ActionBar(tabCloseContainer, { context: { editor, group }, ariaLabel: nls.localize('araLabelTabActions', "Tab actions") });
			bar.push(this.closeEditorAction, { icon: true, label: false });

			this.tabDisposeables.push(bar);
B
Benjamin Pasero 已提交
235 236 237

			// Eventing
			this.hookTabListeners(tabContainer, { editor, group });
B
Benjamin Pasero 已提交
238
		});
B
Benjamin Pasero 已提交
239

B
Benjamin Pasero 已提交
240 241 242
		// Add to tabs container
		tabContainers.forEach(tab => this.tabsContainer.appendChild(tab));

243 244 245 246 247 248 249 250
		this.layout();
	}

	public layout(): void {
		if (!this.activeTab) {
			return;
		}

B
Benjamin Pasero 已提交
251
		// Always reveal the active one
B
Benjamin Pasero 已提交
252 253 254 255
		const containerWidth = this.tabsContainer.offsetWidth;
		const containerScrollPosX = this.tabsContainer.scrollLeft;
		const activeTabPosX = this.activeTab.offsetLeft;
		const activeTabWidth = this.activeTab.offsetWidth;
B
Benjamin Pasero 已提交
256 257 258

		// Tab is overflowing to the right: Scroll minimally until the element is fully visible to the right
		if (containerScrollPosX + containerWidth < activeTabPosX + activeTabWidth) {
B
Benjamin Pasero 已提交
259
			this.tabsContainer.scrollLeft += ((activeTabPosX + activeTabWidth) /* right corner of tab */ - (containerScrollPosX + containerWidth) /* right corner of view port */);
B
Benjamin Pasero 已提交
260 261 262 263
		}

		// Tab is overlflowng to the left: Scroll it into view to the left
		else if (containerScrollPosX > activeTabPosX) {
B
Benjamin Pasero 已提交
264
			this.tabsContainer.scrollLeft = this.activeTab.offsetLeft;
B
Benjamin Pasero 已提交
265
		}
266 267 268 269 270 271

		// Update enablement of certain actions that depend on overflow
		const isOverflowing = (this.tabsContainer.scrollWidth > containerWidth);
		this.showEditorsOfLeftGroup.enabled = isOverflowing;
		this.showEditorsOfCenterGroup.enabled = isOverflowing;
		this.showEditorsOfRightGroup.enabled = isOverflowing;
B
Benjamin Pasero 已提交
272 273
	}

B
Benjamin Pasero 已提交
274
	private hookTabListeners(tab: HTMLElement, identifier: IEditorIdentifier): void {
B
Benjamin Pasero 已提交
275 276
		const {editor, group} = identifier;
		const position = this.stacks.positionOfGroup(group);
B
Benjamin Pasero 已提交
277 278

		// Open on Click
B
Benjamin Pasero 已提交
279
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
280
			if (e.button === 0 /* Left Button */ && !DOM.findParentWithClass(<any>e.target || e.srcElement, 'monaco-action-bar', 'tab')) {
B
Benjamin Pasero 已提交
281
				this.editorService.openEditor(editor, null, position).done(null, errors.onUnexpectedError);
B
Benjamin Pasero 已提交
282
			}
B
Benjamin Pasero 已提交
283
		}));
B
Benjamin Pasero 已提交
284 285

		// Pin on double click
B
Benjamin Pasero 已提交
286
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.DBLCLICK, (e: MouseEvent) => {
B
Benjamin Pasero 已提交
287 288
			DOM.EventHelper.stop(e);

B
Benjamin Pasero 已提交
289
			this.editorGroupService.pinEditor(position, editor);
B
Benjamin Pasero 已提交
290
		}));
B
Benjamin Pasero 已提交
291 292

		// Close on mouse middle click
B
Benjamin Pasero 已提交
293
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.MOUSE_UP, (e: MouseEvent) => {
B
Benjamin Pasero 已提交
294 295 296
			DOM.EventHelper.stop(e);

			if (e.button === 1 /* Middle Button */) {
B
Benjamin Pasero 已提交
297
				this.editorService.closeEditor(position, editor).done(null, errors.onUnexpectedError);
B
Benjamin Pasero 已提交
298
			}
B
Benjamin Pasero 已提交
299
		}));
B
Benjamin Pasero 已提交
300 301

		// Context menu
B
Benjamin Pasero 已提交
302
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.CONTEXT_MENU, (e: Event) => {
B
Benjamin Pasero 已提交
303 304
			DOM.EventHelper.stop(e);

B
Benjamin Pasero 已提交
305
			let anchor: HTMLElement | { x: number, y: number } = tab;
B
Benjamin Pasero 已提交
306 307 308 309 310 311 312
			if (e instanceof MouseEvent) {
				const event = new StandardMouseEvent(e);
				anchor = { x: event.posx, y: event.posy };
			}

			this.contextMenuService.showContextMenu({
				getAnchor: () => anchor,
B
Benjamin Pasero 已提交
313 314
				getActions: () => TPromise.as(this.getTabActions(identifier)),
				getActionsContext: () => identifier,
B
Benjamin Pasero 已提交
315 316 317 318 319 320 321 322 323
				getKeyBinding: (action) => {
					var opts = this.keybindingService.lookupKeybindings(action.id);
					if (opts.length > 0) {
						return opts[0]; // only take the first one
					}

					return null;
				}
			});
B
Benjamin Pasero 已提交
324
		}));
B
Benjamin Pasero 已提交
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383

		// Drag start
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_START, (e: DragEvent) => {
			DOM.addClass(tab, 'dragged');
			e.dataTransfer.setData('text', this.idToString(identifier));
			e.dataTransfer.effectAllowed = 'move';
		}));

		// Drag over
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_OVER, (e: DragEvent) => {
			DOM.addClass(tab, 'dropfeedback');
		}));

		// Drag leave
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_LEAVE, (e: DragEvent) => {
			DOM.removeClass(tab, 'dropfeedback');
		}));

		// Drag end
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_END, (e: DragEvent) => {
			DOM.removeClass(tab, 'dragged');
			DOM.removeClass(tab, 'dropfeedback');
		}));

		// Drop
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.DROP, (e: DragEvent) => {
			const identifier = this.stringToId(e.dataTransfer.getData('text'));
			if (identifier) {
				e.preventDefault();

				const sourcePosition = this.stacks.positionOfGroup(identifier.group);
				const targetPosition = this.stacks.positionOfGroup(group);
				const targetIndex = group.indexOf(editor);

				// Move editor to target position and index
				this.editorGroupService.moveEditor(identifier.editor, sourcePosition, targetPosition, targetIndex);
			}
		}));
	}

	private idToString(identifier: IEditorIdentifier): string {
		return [identifier.group.id, identifier.group.indexOf(identifier.editor)].join(',');
	}

	private stringToId(str: string): IEditorIdentifier {
		if (str) {
			const parts = str.split(',');
			if (parts.length === 2) {
				const group = this.stacks.getGroup(Number(parts[0]));
				if (group) {
					const editor = group.getEditor(Number(parts[1]));
					if (editor) {
						return { group, editor };
					}
				}
			}
		}

		return void 0;
B
Benjamin Pasero 已提交
384 385
	}

B
Benjamin Pasero 已提交
386
	private getTabActions(identifier: IEditorIdentifier): IAction[] {
B
Benjamin Pasero 已提交
387
		const {editor, group} = identifier;
B
Benjamin Pasero 已提交
388 389

		// Enablement
B
Benjamin Pasero 已提交
390 391 392
		this.closeOtherEditorsAction.enabled = group.count > 1;
		this.pinEditorAction.enabled = !group.isPinned(editor);
		this.closeRightEditorsAction.enabled = group.indexOf(editor) !== group.count - 1;
B
Benjamin Pasero 已提交
393

394
		// Actions: For all editors
B
Benjamin Pasero 已提交
395
		const actions: IAction[] = [
B
Benjamin Pasero 已提交
396 397
			this.closeEditorAction,
			this.closeOtherEditorsAction,
B
Benjamin Pasero 已提交
398
			this.closeRightEditorsAction,
B
Benjamin Pasero 已提交
399
			new Separator(),
400
			this.pinEditorAction,
B
Benjamin Pasero 已提交
401
		];
402

403
		// Actions: For active editor
B
Benjamin Pasero 已提交
404 405
		if (group.isActive(editor)) {
			const editorActions = this.getEditorActions(group);
406 407 408
			if (editorActions.primary.length) {
				actions.push(new Separator(), ...prepareActions(editorActions.primary));
			}
409

410 411 412
			if (editorActions.secondary.length) {
				actions.push(new Separator(), ...prepareActions(editorActions.secondary));
			}
413 414 415
		}

		return actions;
B
Benjamin Pasero 已提交
416 417
	}

B
wip  
Benjamin Pasero 已提交
418 419 420
	public dispose(): void {
		super.dispose();

B
Benjamin Pasero 已提交
421
		// Toolbar
B
wip  
Benjamin Pasero 已提交
422 423 424
		this.groupActionsToolbar.dispose();
	}
}