tabsTitleControl.ts 32.2 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 nls = require('vs/nls');
B
Benjamin Pasero 已提交
10
import { TPromise } from 'vs/base/common/winjs.base';
B
Benjamin Pasero 已提交
11 12
import errors = require('vs/base/common/errors');
import DOM = require('vs/base/browser/dom');
J
Johannes Rieken 已提交
13 14
import { isMacintosh } from 'vs/base/common/platform';
import { MIME_BINARY } from 'vs/base/common/mime';
B
Benjamin Pasero 已提交
15
import { shorten, getPathLabel } from 'vs/base/common/labels';
B
Benjamin Pasero 已提交
16
import { ActionRunner, IAction } from 'vs/base/common/actions';
17
import { Position, IEditorInput, Verbosity, IUntitledResourceInput } from 'vs/platform/editor/common/editor';
18
import { IEditorGroup, toResource } from 'vs/workbench/common/editor';
J
Johannes Rieken 已提交
19 20
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
B
Benjamin Pasero 已提交
21
import { ResourceLabel } from 'vs/workbench/browser/labels';
J
Johannes Rieken 已提交
22 23 24 25 26 27 28 29 30 31
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
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/keybinding';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IMenuService } from 'vs/platform/actions/common/actions';
32
import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows';
33
import { TitleControl, handleWorkspaceExternalDrop } from 'vs/workbench/browser/parts/editor/titleControl';
J
Johannes Rieken 已提交
34
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
35
import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
J
Johannes Rieken 已提交
36 37 38
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { extractResources } from 'vs/base/browser/dnd';
39
import { getOrSet } from 'vs/base/common/map';
40 41
import { DelegatingWorkbenchEditorService } from 'vs/workbench/services/editor/browser/editorService';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
B
Benjamin Pasero 已提交
42
import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
43
import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER } from 'vs/workbench/common/theme';
44
import { activeContrastBorder, contrastBorder } from 'vs/platform/theme/common/colorRegistry';
45
import { IFileService } from 'vs/platform/files/common/files';
46
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
B
Benjamin Pasero 已提交
47 48 49 50

interface IEditorInputLabel {
	name: string;
	description?: string;
51
	title?: string;
B
Benjamin Pasero 已提交
52
}
B
wip  
Benjamin Pasero 已提交
53

54 55
type AugmentedLabel = IEditorInputLabel & { editor: IEditorInput };

B
wip  
Benjamin Pasero 已提交
56
export class TabsTitleControl extends TitleControl {
B
Benjamin Pasero 已提交
57 58 59
	private titleContainer: HTMLElement;
	private tabsContainer: HTMLElement;
	private activeTab: HTMLElement;
B
Benjamin Pasero 已提交
60
	private editorLabels: ResourceLabel[];
61
	private scrollbar: ScrollableElement;
62
	private tabDisposeables: IDisposable[];
63
	private blockRevealActiveTab: boolean;
B
wip  
Benjamin Pasero 已提交
64 65 66 67 68 69

	constructor(
		@IContextMenuService contextMenuService: IContextMenuService,
		@IInstantiationService instantiationService: IInstantiationService,
		@IWorkbenchEditorService editorService: IWorkbenchEditorService,
		@IEditorGroupService editorGroupService: IEditorGroupService,
70
		@IContextKeyService contextKeyService: IContextKeyService,
71
		@IKeybindingService keybindingService: IKeybindingService,
B
wip  
Benjamin Pasero 已提交
72
		@ITelemetryService telemetryService: ITelemetryService,
73
		@IMessageService messageService: IMessageService,
74
		@IMenuService menuService: IMenuService,
J
Joao Moreno 已提交
75
		@IQuickOpenService quickOpenService: IQuickOpenService,
76
		@IWindowService private windowService: IWindowService,
77
		@IWindowsService private windowsService: IWindowsService,
78
		@IThemeService themeService: IThemeService,
79 80
		@IFileService private fileService: IFileService,
		@IWorkspacesService private workspacesService: IWorkspacesService
B
wip  
Benjamin Pasero 已提交
81
	) {
82
		super(contextMenuService, instantiationService, editorService, editorGroupService, contextKeyService, keybindingService, telemetryService, messageService, menuService, quickOpenService, themeService);
B
wip  
Benjamin Pasero 已提交
83

B
Benjamin Pasero 已提交
84
		this.tabDisposeables = [];
85
		this.editorLabels = [];
B
wip  
Benjamin Pasero 已提交
86 87
	}

88 89 90 91 92 93 94
	protected initActions(services: IInstantiationService): void {
		super.initActions(this.createScopedInstantiationService());
	}

	private createScopedInstantiationService(): IInstantiationService {
		const stacks = this.editorGroupService.getStacksModel();
		const delegatingEditorService = this.instantiationService.createInstance(DelegatingWorkbenchEditorService);
95 96 97 98 99

		// We create a scoped instantiation service to override the behaviour when closing an inactive editor
		// Specifically we want to move focus back to the editor when an inactive editor is closed from anywhere
		// in the tabs title control (e.g. mouse middle click, context menu on tab). This is only needed for
		// the inactive editors because closing the active one will always cause a tab switch that sets focus.
100 101
		// We also want to block the tabs container to reveal the currently active tab because that makes it very
		// hard to close multiple inactive tabs next to each other.
102 103 104
		delegatingEditorService.setEditorCloseHandler((position, editor) => {
			const group = stacks.groupAt(position);
			if (group && stacks.isActive(group) && !group.isActive(editor)) {
105
				this.editorGroupService.focusGroup(group);
106 107
			}

108 109
			this.blockRevealActiveTab = true;

110 111 112
			return TPromise.as(void 0);
		});

113
		return this.instantiationService.createChild(new ServiceCollection([IWorkbenchEditorService, delegatingEditorService]));
114 115
	}

B
wip  
Benjamin Pasero 已提交
116 117 118
	public setContext(group: IEditorGroup): void {
		super.setContext(group);

119
		this.editorActionsToolbar.context = { group };
B
wip  
Benjamin Pasero 已提交
120 121
	}

122
	public create(parent: HTMLElement): void {
123
		super.create(parent);
124

125
		this.titleContainer = parent;
B
wip  
Benjamin Pasero 已提交
126

127
		// Tabs Container
B
Benjamin Pasero 已提交
128
		this.tabsContainer = document.createElement('div');
129
		this.tabsContainer.setAttribute('role', 'tablist');
B
Benjamin Pasero 已提交
130
		DOM.addClass(this.tabsContainer, 'tabs-container');
131

132
		// Forward scrolling inside the container to our custom scrollbar
133
		this.toUnbind.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.SCROLL, e => {
B
Benjamin Pasero 已提交
134
			if (DOM.hasClass(this.tabsContainer, 'scroll')) {
135
				this.scrollbar.setScrollPosition({
B
Benjamin Pasero 已提交
136 137 138 139 140
					scrollLeft: this.tabsContainer.scrollLeft // during DND the  container gets scrolled so we need to update the custom scrollbar
				});
			}
		}));

141
		// New file when double clicking on tabs container (but not tabs)
142
		this.toUnbind.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DBLCLICK, e => {
143 144 145 146
			const target = e.target;
			if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) {
				DOM.EventHelper.stop(e);

147
				const group = this.context;
B
Benjamin Pasero 已提交
148
				if (group) {
149
					this.editorService.openEditor({ options: { pinned: true, index: group.count /* always at the end */ } } as IUntitledResourceInput).done(null, errors.onUnexpectedError); // untitled are always pinned
B
Benjamin Pasero 已提交
150
				}
151 152 153
			}
		}));

154 155 156 157 158 159 160 161 162 163 164 165 166 167
		// Custom Scrollbar
		this.scrollbar = new ScrollableElement(this.tabsContainer, {
			horizontal: ScrollbarVisibility.Auto,
			vertical: ScrollbarVisibility.Hidden,
			scrollYToX: true,
			useShadows: false,
			horizontalScrollbarSize: 3
		});

		this.scrollbar.onScroll(e => {
			this.tabsContainer.scrollLeft = e.scrollLeft;
		});

		this.titleContainer.appendChild(this.scrollbar.getDomNode());
B
Benjamin Pasero 已提交
168

B
Benjamin Pasero 已提交
169
		// Drag over
170
		this.toUnbind.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DRAG_OVER, (e: DragEvent) => {
B
Benjamin Pasero 已提交
171 172 173 174 175 176

			// update the dropEffect, otherwise it would look like a "move" operation. but only if we are
			// not dragging a tab actually because there we support both moving as well as copying
			if (!TabsTitleControl.getDraggedEditor()) {
				e.dataTransfer.dropEffect = 'copy';
			}
177

B
Benjamin Pasero 已提交
178 179
			DOM.addClass(this.tabsContainer, 'scroll'); // enable support to scroll while dragging

B
Benjamin Pasero 已提交
180 181
			const target = e.target;
			if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) {
B
Benjamin Pasero 已提交
182
				this.updateDropFeedback(this.tabsContainer, true);
B
Benjamin Pasero 已提交
183 184 185 186
			}
		}));

		// Drag leave
187
		this.toUnbind.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DRAG_LEAVE, (e: DragEvent) => {
B
Benjamin Pasero 已提交
188
			this.updateDropFeedback(this.tabsContainer, false);
B
Benjamin Pasero 已提交
189
			DOM.removeClass(this.tabsContainer, 'scroll');
B
Benjamin Pasero 已提交
190 191 192
		}));

		// Drag end
193
		this.toUnbind.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DRAG_END, (e: DragEvent) => {
B
Benjamin Pasero 已提交
194
			this.updateDropFeedback(this.tabsContainer, false);
B
Benjamin Pasero 已提交
195
			DOM.removeClass(this.tabsContainer, 'scroll');
B
Benjamin Pasero 已提交
196 197
		}));

B
Benjamin Pasero 已提交
198
		// Drop onto tabs container
199
		this.toUnbind.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DROP, (e: DragEvent) => {
B
Benjamin Pasero 已提交
200
			this.updateDropFeedback(this.tabsContainer, false);
B
Benjamin Pasero 已提交
201
			DOM.removeClass(this.tabsContainer, 'scroll');
B
Benjamin Pasero 已提交
202

B
Benjamin Pasero 已提交
203
			const target = e.target;
B
Benjamin Pasero 已提交
204
			if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) {
B
Benjamin Pasero 已提交
205 206
				const group = this.context;
				if (group) {
207 208 209
					const targetPosition = this.stacks.positionOfGroup(group);
					const targetIndex = group.count;

B
Benjamin Pasero 已提交
210
					this.onDrop(e, group, targetPosition, targetIndex);
B
Benjamin Pasero 已提交
211 212 213 214
				}
			}
		}));

215
		// Editor Actions Container
216 217 218
		const editorActionsContainer = document.createElement('div');
		DOM.addClass(editorActionsContainer, 'editor-actions');
		this.titleContainer.appendChild(editorActionsContainer);
219 220 221

		// Editor Actions Toolbar
		this.createEditorActionsToolBar(editorActionsContainer);
B
wip  
Benjamin Pasero 已提交
222 223
	}

B
Benjamin Pasero 已提交
224 225
	private updateDropFeedback(element: HTMLElement, isDND: boolean, index?: number): void {
		const isTab = (typeof index === 'number');
226
		const isActiveTab = isTab && this.context && this.context.isActive(this.context.getEditor(index));
B
Benjamin Pasero 已提交
227 228

		// Background
B
Benjamin Pasero 已提交
229
		const noDNDBackgroundColor = isTab ? this.getColor(isActiveTab ? TAB_ACTIVE_BACKGROUND : TAB_INACTIVE_BACKGROUND) : null;
B
Benjamin Pasero 已提交
230 231 232
		element.style.backgroundColor = isDND ? this.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND) : noDNDBackgroundColor;

		// Outline
233 234
		const activeContrastBorderColor = this.getColor(activeContrastBorder);
		if (activeContrastBorderColor && isDND) {
B
Benjamin Pasero 已提交
235 236
			element.style.outlineWidth = '2px';
			element.style.outlineStyle = 'dashed';
237
			element.style.outlineColor = activeContrastBorderColor;
B
Benjamin Pasero 已提交
238
			element.style.outlineOffset = isTab ? '-5px' : '-3px';
B
Benjamin Pasero 已提交
239 240 241
		} else {
			element.style.outlineWidth = null;
			element.style.outlineStyle = null;
242
			element.style.outlineColor = activeContrastBorderColor;
B
Benjamin Pasero 已提交
243
			element.style.outlineOffset = null;
B
Benjamin Pasero 已提交
244 245 246
		}
	}

B
Benjamin Pasero 已提交
247 248 249 250
	public allowDragging(element: HTMLElement): boolean {
		return (element.className === 'tabs-container');
	}

251 252 253 254 255 256 257 258
	protected doUpdate(): void {
		if (!this.context) {
			return;
		}

		const group = this.context;

		// Tabs container activity state
259 260
		const isGroupActive = this.stacks.isActive(group);
		if (isGroupActive) {
261 262 263 264 265
			DOM.addClass(this.titleContainer, 'active');
		} else {
			DOM.removeClass(this.titleContainer, 'active');
		}

266 267
		// Compute labels and protect against duplicates
		const editorsOfGroup = this.context.getEditors();
268
		const labels = this.getTabLabels(editorsOfGroup);
269

270
		// Tab label and styles
271
		editorsOfGroup.forEach((editor, index) => {
272 273
			const tabContainer = this.tabsContainer.children[index];
			if (tabContainer instanceof HTMLElement) {
274
				const isPinned = group.isPinned(index);
275
				const isTabActive = group.isActive(editor);
276 277
				const isDirty = editor.isDirty();

278 279
				const label = labels[index];
				const name = label.name;
280
				const description = label.description || '';
281
				const title = label.title || '';
282

283
				// Container
B
Benjamin Pasero 已提交
284
				tabContainer.setAttribute('aria-label', `${name}, tab`);
285
				tabContainer.title = title;
286 287
				tabContainer.style.borderLeftColor = (index !== 0) ? (this.getColor(TAB_BORDER) || this.getColor(contrastBorder)) : null;
				tabContainer.style.borderRightColor = (index === editorsOfGroup.length - 1) ? (this.getColor(TAB_BORDER) || this.getColor(contrastBorder)) : null;
288
				tabContainer.style.outlineColor = this.getColor(activeContrastBorder);
B
Benjamin Pasero 已提交
289

290
				const tabOptions = this.editorGroupService.getTabOptions();
291
				['off', 'left'].forEach(option => {
292
					const domAction = tabOptions.tabCloseButton === option ? DOM.addClass : DOM.removeClass;
293 294
					domAction(tabContainer, `close-button-${option}`);
				});
295

296 297
				// Label
				const tabLabel = this.editorLabels[index];
298
				tabLabel.setLabel({ name, description, resource: toResource(editor, { supportSideBySide: true }) }, { extraClasses: ['tab-label'], italic: !isPinned });
299 300

				// Active state
301
				if (isTabActive) {
302
					DOM.addClass(tabContainer, 'active');
303
					tabContainer.setAttribute('aria-selected', 'true');
B
Benjamin Pasero 已提交
304
					tabContainer.style.backgroundColor = this.getColor(TAB_ACTIVE_BACKGROUND);
305
					tabLabel.element.style.color = this.getColor(isGroupActive ? TAB_ACTIVE_FOREGROUND : TAB_UNFOCUSED_ACTIVE_FOREGROUND);
B
Benjamin Pasero 已提交
306 307 308 309 310 311 312 313 314 315

					// Use boxShadow for the active tab border because if we also have a editor group header
					// color, the two colors would collide and the tab border never shows up.
					// see https://github.com/Microsoft/vscode/issues/33111
					const activeTabBorderColor = this.getColor(isGroupActive ? TAB_ACTIVE_BORDER : TAB_UNFOCUSED_ACTIVE_BORDER);
					if (activeTabBorderColor) {
						tabContainer.style.boxShadow = `${activeTabBorderColor} 0 -1px inset`;
					} else {
						tabContainer.style.boxShadow = null;
					}
316

317 318 319
					this.activeTab = tabContainer;
				} else {
					DOM.removeClass(tabContainer, 'active');
320
					tabContainer.setAttribute('aria-selected', 'false');
B
Benjamin Pasero 已提交
321
					tabContainer.style.backgroundColor = this.getColor(TAB_INACTIVE_BACKGROUND);
322
					tabLabel.element.style.color = this.getColor(isGroupActive ? TAB_INACTIVE_FOREGROUND : TAB_UNFOCUSED_INACTIVE_FOREGROUND);
B
Benjamin Pasero 已提交
323
					tabContainer.style.boxShadow = null;
324 325 326 327 328 329 330 331 332 333
				}

				// Dirty State
				if (isDirty) {
					DOM.addClass(tabContainer, 'dirty');
				} else {
					DOM.removeClass(tabContainer, 'dirty');
				}
			}
		});
334

335
		// Update Editor Actions Toolbar
336
		this.updateEditorActionsToolbar();
337

B
Benjamin Pasero 已提交
338
		// Ensure the active tab is always revealed
339
		this.layout();
340 341
	}

342
	private getTabLabels(editors: IEditorInput[]): IEditorInputLabel[] {
343 344
		const labelFormat = this.editorGroupService.getTabOptions().labelFormat;
		const { verbosity, shortenDuplicates } = this.getLabelConfigFlags(labelFormat);
B
Benjamin Pasero 已提交
345 346

		// Build labels and descriptions for each editor
347 348 349 350 351 352
		const labels = editors.map(editor => ({
			editor,
			name: editor.getName(),
			description: editor.getDescription(verbosity),
			title: editor.getTitle(Verbosity.LONG)
		}));
353

354
		// Shorten labels as needed
355
		if (shortenDuplicates) {
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
			this.shortenTabLabels(labels);
		}

		return labels;
	}

	private shortenTabLabels(labels: AugmentedLabel[]): void {

		// Gather duplicate titles, while filtering out invalid descriptions
		const mapTitleToDuplicates = new Map<string, AugmentedLabel[]>();
		for (const label of labels) {
			if (typeof label.description === 'string' && label.description) {
				getOrSet(mapTitleToDuplicates, label.name, []).push(label);
			} else {
				label.description = '';
371
			}
372
		}
B
Benjamin Pasero 已提交
373

374 375
		// Identify duplicate titles and shorten descriptions
		mapTitleToDuplicates.forEach(duplicateTitles => {
B
Benjamin Pasero 已提交
376

377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395
			// Remove description if the title isn't duplicated
			if (duplicateTitles.length === 1) {
				duplicateTitles[0].description = '';

				return;
			}

			// Identify duplicate descriptions
			const mapDescriptionToDuplicates = new Map<string, AugmentedLabel[]>();
			for (const label of duplicateTitles) {
				getOrSet(mapDescriptionToDuplicates, label.description, []).push(label);
			}

			// For editors with duplicate descriptions, check whether any long descriptions differ
			let useLongDescriptions = false;
			mapDescriptionToDuplicates.forEach((duplicateDescriptions, name) => {
				if (!useLongDescriptions && duplicateDescriptions.length > 1) {
					const [first, ...rest] = duplicateDescriptions.map(({ editor }) => editor.getDescription(Verbosity.LONG));
					useLongDescriptions = rest.some(description => description !== first);
396
				}
397
			});
B
Benjamin Pasero 已提交
398

399 400 401 402 403 404
			// If so, replace all descriptions with long descriptions
			if (useLongDescriptions) {
				mapDescriptionToDuplicates.clear();
				duplicateTitles.forEach(label => {
					label.description = label.editor.getDescription(Verbosity.LONG);
					getOrSet(mapDescriptionToDuplicates, label.description, []).push(label);
B
Benjamin Pasero 已提交
405
				});
406 407 408 409 410
			}

			// Obtain final set of descriptions
			const descriptions: string[] = [];
			mapDescriptionToDuplicates.forEach((_, description) => descriptions.push(description));
B
Benjamin Pasero 已提交
411

412 413 414 415
			// Remove description if all descriptions are identical
			if (descriptions.length === 1) {
				for (const label of mapDescriptionToDuplicates.get(descriptions[0])) {
					label.description = '';
B
Benjamin Pasero 已提交
416
				}
417

418 419
				return;
			}
420

421 422 423 424 425
			// Shorten descriptions
			const shortenedDescriptions = shorten(descriptions);
			descriptions.forEach((description, i) => {
				for (const label of mapDescriptionToDuplicates.get(description)) {
					label.description = shortenedDescriptions[i];
426 427
				}
			});
428
		});
B
Benjamin Pasero 已提交
429 430
	}

431
	private getLabelConfigFlags(value: string) {
432 433 434 435 436 437 438 439 440 441 442 443
		switch (value) {
			case 'short':
				return { verbosity: Verbosity.SHORT, shortenDuplicates: false };
			case 'medium':
				return { verbosity: Verbosity.MEDIUM, shortenDuplicates: false };
			case 'long':
				return { verbosity: Verbosity.LONG, shortenDuplicates: false };
			default:
				return { verbosity: Verbosity.MEDIUM, shortenDuplicates: true };
		}
	}

444
	protected doRefresh(): void {
B
wip  
Benjamin Pasero 已提交
445
		const group = this.context;
446
		const editor = group && group.activeEditor;
B
wip  
Benjamin Pasero 已提交
447
		if (!editor) {
448 449
			this.clearTabs();

450
			this.clearEditorActionsToolbar();
B
wip  
Benjamin Pasero 已提交
451 452 453 454

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

B
Benjamin Pasero 已提交
455 456
		// Handle Tabs
		this.handleTabs(group.count);
457
		DOM.removeClass(this.titleContainer, 'empty');
458

459
		// Update Tabs
460
		this.doUpdate();
B
wip  
Benjamin Pasero 已提交
461 462
	}

463
	private clearTabs(): void {
B
Benjamin Pasero 已提交
464
		DOM.clearNode(this.tabsContainer);
465 466

		this.tabDisposeables = dispose(this.tabDisposeables);
467
		this.editorLabels = [];
B
Benjamin Pasero 已提交
468

469
		DOM.addClass(this.titleContainer, 'empty');
470 471
	}

B
Benjamin Pasero 已提交
472 473 474
	private handleTabs(tabsNeeded: number): void {
		const tabs = this.tabsContainer.children;
		const tabsCount = tabs.length;
B
Benjamin Pasero 已提交
475

B
Benjamin Pasero 已提交
476 477 478 479
		// Nothing to do if count did not change
		if (tabsCount === tabsNeeded) {
			return;
		}
B
Benjamin Pasero 已提交
480

B
Benjamin Pasero 已提交
481 482 483 484 485 486
		// We need more tabs: create new ones
		if (tabsCount < tabsNeeded) {
			for (let i = tabsCount; i < tabsNeeded; i++) {
				this.tabsContainer.appendChild(this.createTab(i));
			}
		}
487

B
Benjamin Pasero 已提交
488 489 490 491
		// We need less tabs: delete the ones we do not need
		else {
			for (let i = 0; i < tabsCount - tabsNeeded; i++) {
				(this.tabsContainer.lastChild as HTMLElement).remove();
492
				this.editorLabels.pop();
B
Benjamin Pasero 已提交
493
				this.tabDisposeables.pop().dispose();
494
			}
B
Benjamin Pasero 已提交
495 496
		}
	}
497

B
Benjamin Pasero 已提交
498
	private createTab(index: number): HTMLElement {
499

B
Benjamin Pasero 已提交
500 501 502 503 504
		// Tab Container
		const tabContainer = document.createElement('div');
		tabContainer.draggable = true;
		tabContainer.tabIndex = 0;
		tabContainer.setAttribute('role', 'presentation'); // cannot use role "tab" here due to https://github.com/Microsoft/vscode/issues/8659
505
		DOM.addClass(tabContainer, 'tab');
B
Benjamin Pasero 已提交
506

B
Benjamin Pasero 已提交
507
		// Tab Editor Label
B
Benjamin Pasero 已提交
508
		const editorLabel = this.instantiationService.createInstance(ResourceLabel, tabContainer, void 0);
B
Benjamin Pasero 已提交
509
		this.editorLabels.push(editorLabel);
B
Benjamin Pasero 已提交
510

B
Benjamin Pasero 已提交
511 512 513 514
		// Tab Close
		const tabCloseContainer = document.createElement('div');
		DOM.addClass(tabCloseContainer, 'tab-close');
		tabContainer.appendChild(tabCloseContainer);
B
Benjamin Pasero 已提交
515

B
Benjamin Pasero 已提交
516 517
		const bar = new ActionBar(tabCloseContainer, { ariaLabel: nls.localize('araLabelTabActions', "Tab actions"), actionRunner: new TabActionRunner(() => this.context, index) });
		bar.push(this.closeEditorAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.closeEditorAction) });
518

B
Benjamin Pasero 已提交
519
		// Eventing
520 521 522
		const disposable = this.hookTabListeners(tabContainer, index);

		this.tabDisposeables.push(combinedDisposable([disposable, bar, editorLabel]));
B
Benjamin Pasero 已提交
523 524

		return tabContainer;
525 526 527 528 529 530 531
	}

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

532 533 534 535
		const visibleContainerWidth = this.tabsContainer.offsetWidth;
		const totalContainerWidth = this.tabsContainer.scrollWidth;

		// Update scrollbar
536
		this.scrollbar.setScrollDimensions({
537 538 539 540
			width: visibleContainerWidth,
			scrollWidth: totalContainerWidth
		});

541 542 543 544 545 546 547
		// Return now if we are blocked to reveal the active tab and clear flag
		if (this.blockRevealActiveTab) {
			this.blockRevealActiveTab = false;
			return;
		}

		// Reveal the active one
B
Benjamin Pasero 已提交
548 549 550
		const containerScrollPosX = this.tabsContainer.scrollLeft;
		const activeTabPosX = this.activeTab.offsetLeft;
		const activeTabWidth = this.activeTab.offsetWidth;
551
		const activeTabFits = activeTabWidth <= visibleContainerWidth;
B
Benjamin Pasero 已提交
552 553

		// Tab is overflowing to the right: Scroll minimally until the element is fully visible to the right
554 555
		// Note: only try to do this if we actually have enough width to give to show the tab fully!
		if (activeTabFits && containerScrollPosX + visibleContainerWidth < activeTabPosX + activeTabWidth) {
556
			this.scrollbar.setScrollPosition({
557 558
				scrollLeft: containerScrollPosX + ((activeTabPosX + activeTabWidth) /* right corner of tab */ - (containerScrollPosX + visibleContainerWidth) /* right corner of view port */)
			});
B
Benjamin Pasero 已提交
559 560
		}

561 562
		// Tab is overlflowng to the left or does not fit: Scroll it into view to the left
		else if (containerScrollPosX > activeTabPosX || !activeTabFits) {
563
			this.scrollbar.setScrollPosition({
564 565
				scrollLeft: this.activeTab.offsetLeft
			});
B
Benjamin Pasero 已提交
566
		}
B
Benjamin Pasero 已提交
567 568
	}

569
	private hookTabListeners(tab: HTMLElement, index: number): IDisposable {
B
Benjamin Pasero 已提交
570
		const disposables: IDisposable[] = [];
B
Benjamin Pasero 已提交
571 572

		// Open on Click
573
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
574 575
			tab.blur();

B
Benjamin Pasero 已提交
576
			const { editor, position } = this.toTabContext(index);
577
			if (e.button === 0 /* Left Button */ && !this.isTabActionBar((e.target || e.srcElement) as HTMLElement)) {
578 579 580 581 582
				setTimeout(() => this.editorService.openEditor(editor, null, position).done(null, errors.onUnexpectedError)); // timeout to keep focus in editor after mouse up
			}
		}));

		// Close on mouse middle click
583
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.MOUSE_UP, (e: MouseEvent) => {
584
			DOM.EventHelper.stop(e);
585
			tab.blur();
586

587
			if (e.button === 1 /* Middle Button*/ && !this.isTabActionBar((e.target || e.srcElement) as HTMLElement)) {
588
				this.closeEditorAction.run(this.toTabContext(index)).done(null, errors.onUnexpectedError);
B
Benjamin Pasero 已提交
589
			}
B
Benjamin Pasero 已提交
590
		}));
B
Benjamin Pasero 已提交
591

592
		// Context menu on Shift+F10
593
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
594 595 596 597
			const event = new StandardKeyboardEvent(e);
			if (event.shiftKey && event.keyCode === KeyCode.F10) {
				DOM.EventHelper.stop(e);

B
Benjamin Pasero 已提交
598 599 600
				const { group, editor } = this.toTabContext(index);

				this.onContextMenu({ group, editor }, e, tab);
601 602 603
			}
		}));

B
Benjamin Pasero 已提交
604
		// Keyboard accessibility
605
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.KEY_UP, (e: KeyboardEvent) => {
B
Benjamin Pasero 已提交
606
			const event = new StandardKeyboardEvent(e);
B
Benjamin Pasero 已提交
607
			let handled = false;
B
Benjamin Pasero 已提交
608

B
Benjamin Pasero 已提交
609 610
			const { group, position, editor } = this.toTabContext(index);

B
Benjamin Pasero 已提交
611
			// Run action on Enter/Space
A
Alexandru Dima 已提交
612
			if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
B
Benjamin Pasero 已提交
613
				handled = true;
B
Benjamin Pasero 已提交
614
				this.editorService.openEditor(editor, null, position).done(null, errors.onUnexpectedError);
B
Benjamin Pasero 已提交
615 616
			}

B
Benjamin Pasero 已提交
617
			// Navigate in editors
A
Alexandru Dima 已提交
618
			else if ([KeyCode.LeftArrow, KeyCode.RightArrow, KeyCode.UpArrow, KeyCode.DownArrow, KeyCode.Home, KeyCode.End].some(kb => event.equals(kb))) {
B
Benjamin Pasero 已提交
619
				let targetIndex: number;
A
Alexandru Dima 已提交
620
				if (event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.UpArrow)) {
B
Benjamin Pasero 已提交
621
					targetIndex = index - 1;
A
Alexandru Dima 已提交
622
				} else if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.DownArrow)) {
B
Benjamin Pasero 已提交
623
					targetIndex = index + 1;
A
Alexandru Dima 已提交
624
				} else if (event.equals(KeyCode.Home)) {
B
Benjamin Pasero 已提交
625 626 627 628 629
					targetIndex = 0;
				} else {
					targetIndex = group.count - 1;
				}

B
Benjamin Pasero 已提交
630 631 632
				const target = group.getEditor(targetIndex);
				if (target) {
					handled = true;
633
					this.editorService.openEditor(target, { preserveFocus: true }, position).done(null, errors.onUnexpectedError);
B
Benjamin Pasero 已提交
634 635 636
					(<HTMLElement>this.tabsContainer.childNodes[targetIndex]).focus();
				}
			}
B
Benjamin Pasero 已提交
637

B
Benjamin Pasero 已提交
638
			if (handled) {
639
				DOM.EventHelper.stop(e, true);
B
Benjamin Pasero 已提交
640
			}
641 642

			// moving in the tabs container can have an impact on scrolling position, so we need to update the custom scrollbar
643
			this.scrollbar.setScrollPosition({
644 645
				scrollLeft: this.tabsContainer.scrollLeft
			});
B
Benjamin Pasero 已提交
646 647
		}));

B
Benjamin Pasero 已提交
648
		// Pin on double click
649
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DBLCLICK, (e: MouseEvent) => {
B
Benjamin Pasero 已提交
650 651
			DOM.EventHelper.stop(e);

B
Benjamin Pasero 已提交
652 653
			const { group, editor } = this.toTabContext(index);

654
			this.editorGroupService.pinEditor(group, editor);
B
Benjamin Pasero 已提交
655
		}));
B
Benjamin Pasero 已提交
656

B
Benjamin Pasero 已提交
657
		// Context menu
658
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.CONTEXT_MENU, (e: Event) => {
659
			DOM.EventHelper.stop(e, true);
B
Benjamin Pasero 已提交
660 661 662
			const { group, editor } = this.toTabContext(index);

			this.onContextMenu({ group, editor }, e, tab);
663
		}, true /* use capture to fix https://github.com/Microsoft/vscode/issues/19145 */));
B
Benjamin Pasero 已提交
664 665

		// Drag start
666
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_START, (e: DragEvent) => {
B
Benjamin Pasero 已提交
667 668
			const { group, editor } = this.toTabContext(index);

669
			this.onEditorDragStart({ editor, group });
670
			e.dataTransfer.effectAllowed = 'copyMove';
B
Benjamin Pasero 已提交
671

B
Benjamin Pasero 已提交
672
			// Insert transfer accordingly
673 674 675 676
			const resource = toResource(editor, { supportSideBySide: true });
			if (resource) {
				const resourceStr = resource.toString();
				e.dataTransfer.setData('URL', resourceStr); // enables cross window DND of tabs
B
Benjamin Pasero 已提交
677
				e.dataTransfer.setData('text/plain', getPathLabel(resource)); // enables dropping tab resource path into text controls
678 679 680 681

				if (resource.scheme === 'file') {
					e.dataTransfer.setData('DownloadURL', [MIME_BINARY, editor.getName(), resourceStr].join(':')); // enables support to drag a tab as file to desktop
				}
B
Benjamin Pasero 已提交
682
			}
B
Benjamin Pasero 已提交
683 684
		}));

685 686 687 688 689 690
		// We need to keep track of DRAG_ENTER and DRAG_LEAVE events because a tab is not just a div without children,
		// it contains a label and a close button. HTML gives us DRAG_ENTER and DRAG_LEAVE events when hovering over
		// these children and this can cause flicker of the drop feedback. The workaround is to count the events and only
		// remove the drop feedback when the counter is 0 (see https://github.com/Microsoft/vscode/issues/14470)
		let counter = 0;

B
Benjamin Pasero 已提交
691
		// Drag over
692
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_ENTER, (e: DragEvent) => {
693
			counter++;
B
Benjamin Pasero 已提交
694 695 696 697 698 699 700 701 702 703 704 705 706 707 708

			// Find out if the currently dragged editor is this tab and in that
			// case we do not want to show any drop feedback
			let draggedEditorIsTab = false;
			const draggedEditor = TabsTitleControl.getDraggedEditor();
			if (draggedEditor) {
				const { group, editor } = this.toTabContext(index);
				if (draggedEditor.editor === editor && draggedEditor.group === group) {
					draggedEditorIsTab = true;
				}
			}

			if (!draggedEditorIsTab) {
				this.updateDropFeedback(tab, true, index);
			}
B
Benjamin Pasero 已提交
709 710 711
		}));

		// Drag leave
712
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_LEAVE, (e: DragEvent) => {
713 714
			counter--;
			if (counter === 0) {
B
Benjamin Pasero 已提交
715
				this.updateDropFeedback(tab, false, index);
716
			}
B
Benjamin Pasero 已提交
717 718 719
		}));

		// Drag end
720
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_END, (e: DragEvent) => {
721
			counter = 0;
B
Benjamin Pasero 已提交
722
			this.updateDropFeedback(tab, false, index);
B
Benjamin Pasero 已提交
723

724
			this.onEditorDragEnd();
B
Benjamin Pasero 已提交
725 726 727
		}));

		// Drop
728
		disposables.push(DOM.addDisposableListener(tab, DOM.EventType.DROP, (e: DragEvent) => {
729
			counter = 0;
B
Benjamin Pasero 已提交
730
			this.updateDropFeedback(tab, false, index);
B
Benjamin Pasero 已提交
731

B
Benjamin Pasero 已提交
732
			const { group, position } = this.toTabContext(index);
733

B
Benjamin Pasero 已提交
734
			this.onDrop(e, group, position, index);
B
Benjamin Pasero 已提交
735
		}));
736 737

		return combinedDisposable(disposables);
B
Benjamin Pasero 已提交
738
	}
B
Benjamin Pasero 已提交
739

740 741 742 743
	private isTabActionBar(element: HTMLElement): boolean {
		return !!DOM.findParentWithClass(element, 'monaco-action-bar', 'tab');
	}

B
Benjamin Pasero 已提交
744 745 746 747 748 749 750 751
	private toTabContext(index: number): { group: IEditorGroup, position: Position, editor: IEditorInput } {
		const group = this.context;
		const position = this.stacks.positionOfGroup(group);
		const editor = group.getEditor(index);

		return { group, position, editor };
	}

B
Benjamin Pasero 已提交
752
	private onDrop(e: DragEvent, group: IEditorGroup, targetPosition: Position, targetIndex: number): void {
B
Benjamin Pasero 已提交
753
		this.updateDropFeedback(this.tabsContainer, false);
754
		DOM.removeClass(this.tabsContainer, 'scroll');
755

B
Benjamin Pasero 已提交
756 757 758 759
		// Local DND
		const draggedEditor = TabsTitleControl.getDraggedEditor();
		if (draggedEditor) {
			DOM.EventHelper.stop(e, true);
760

B
Benjamin Pasero 已提交
761 762
			// Move editor to target position and index
			if (this.isMoveOperation(e, draggedEditor.group, group)) {
763
				this.editorGroupService.moveEditor(draggedEditor.editor, draggedEditor.group, group, { index: targetIndex });
B
Benjamin Pasero 已提交
764
			}
765

B
Benjamin Pasero 已提交
766
			// Copy: just open editor at target index
767
			else {
B
Benjamin Pasero 已提交
768
				this.editorService.openEditor(draggedEditor.editor, { pinned: true, index: targetIndex }, targetPosition).done(null, errors.onUnexpectedError);
769
			}
B
Benjamin Pasero 已提交
770 771 772 773 774 775 776 777

			this.onEditorDragEnd();
		}

		// External DND
		else {
			this.handleExternalDrop(e, targetPosition, targetIndex);
		}
B
Benjamin Pasero 已提交
778
	}
779

780
	private handleExternalDrop(e: DragEvent, targetPosition: Position, targetIndex: number): void {
781 782
		const droppedResources = extractResources(e).filter(r => r.resource.scheme === 'file' || r.resource.scheme === 'untitled');
		if (droppedResources.length) {
B
Benjamin Pasero 已提交
783
			DOM.EventHelper.stop(e, true);
784

785
			handleWorkspaceExternalDrop(droppedResources, this.fileService, this.messageService, this.windowsService, this.windowService, this.workspacesService).then(handled => {
786 787 788
				if (handled) {
					return;
				}
789

790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
				// Add external ones to recently open list
				const externalResources = droppedResources.filter(d => d.isExternal).map(d => d.resource);
				if (externalResources.length) {
					this.windowsService.addRecentlyOpened(externalResources.map(resource => resource.fsPath));
				}

				// Open in Editor
				this.windowService.focusWindow()
					.then(() => this.editorService.openEditors(droppedResources.map(d => {
						return {
							input: { resource: d.resource, options: { pinned: true, index: targetIndex } },
							position: targetPosition
						};
					}))).then(() => {
						this.editorGroupService.focusGroup(targetPosition);
					}).done(null, errors.onUnexpectedError);
			});
807 808 809
		}
	}

810 811 812 813 814
	private isMoveOperation(e: DragEvent, source: IEditorGroup, target: IEditorGroup) {
		const isCopy = (e.ctrlKey && !isMacintosh) || (e.altKey && isMacintosh);

		return !isCopy || source.id === target.id;
	}
J
Johannes Rieken 已提交
815
}
B
Benjamin Pasero 已提交
816 817 818 819 820 821 822

class TabActionRunner extends ActionRunner {

	constructor(private group: () => IEditorGroup, private index: number) {
		super();
	}

B
Benjamin Pasero 已提交
823
	public run(action: IAction, context?: any): TPromise<void> {
B
Benjamin Pasero 已提交
824
		const group = this.group();
B
Benjamin Pasero 已提交
825 826 827
		if (!group) {
			return TPromise.as(void 0);
		}
B
Benjamin Pasero 已提交
828 829 830

		return super.run(action, { group, editor: group.getEditor(this.index) });
	}
B
Benjamin Pasero 已提交
831 832 833 834
}

registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => {

B
Benjamin Pasero 已提交
835
	// Styling with Outline color (e.g. high contrast theme)
836 837
	const activeContrastBorderColor = theme.getColor(activeContrastBorder);
	if (activeContrastBorderColor) {
B
Benjamin Pasero 已提交
838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858
		collector.addRule(`
			.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active,
			.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active:hover  {
				outline: 1px solid;
				outline-offset: -5px;
			}

			.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:hover  {
				outline: 1px dashed;
				outline-offset: -5px;
			}

			.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active > .tab-close .action-label,
			.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active:hover > .tab-close .action-label,
			.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty > .tab-close .action-label,
			.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:hover > .tab-close .action-label {
				opacity: 1 !important;
			}
		`);
	}
});