tabsTitleControl.ts 21.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 11
import errors = require('vs/base/common/errors');
import DOM = require('vs/base/browser/dom');
J
Johannes Rieken 已提交
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
import { isMacintosh } from 'vs/base/common/platform';
import { MIME_BINARY } from 'vs/base/common/mime';
import { Position, IEditorInput } from 'vs/platform/editor/common/editor';
import { IEditorGroup, IEditorIdentifier, asFileEditorInput, getResource } from 'vs/workbench/common/editor';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { EditorLabel } from 'vs/workbench/browser/labels';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
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 { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
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';
import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl';
import { IQuickOpenService } from 'vs/workbench/services/quickopen/common/quickOpenService';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { extractResources } from 'vs/base/browser/dnd';
import { LinkedMap } from 'vs/base/common/map';
B
Benjamin Pasero 已提交
38 39 40 41 42 43 44 45 46
import paths = require('vs/base/common/paths');

interface IEditorInputLabel {
	editor: IEditorInput;
	name: string;
	hasAmbiguousName?: boolean;
	description?: string;
	verboseDescription?: string;
}
B
wip  
Benjamin Pasero 已提交
47 48

export class TabsTitleControl extends TitleControl {
B
Benjamin Pasero 已提交
49 50 51
	private titleContainer: HTMLElement;
	private tabsContainer: HTMLElement;
	private activeTab: HTMLElement;
52
	private editorLabels: EditorLabel[];
53
	private scrollbar: ScrollableElement;
54
	private tabDisposeables: IDisposable[] = [];
B
wip  
Benjamin Pasero 已提交
55 56 57 58

	constructor(
		@IContextMenuService contextMenuService: IContextMenuService,
		@IInstantiationService instantiationService: IInstantiationService,
59
		@IConfigurationService configurationService: IConfigurationService,
B
wip  
Benjamin Pasero 已提交
60 61
		@IWorkbenchEditorService editorService: IWorkbenchEditorService,
		@IEditorGroupService editorGroupService: IEditorGroupService,
62
		@IUntitledEditorService private untitledEditorService: IUntitledEditorService,
63
		@IContextKeyService contextKeyService: IContextKeyService,
64
		@IKeybindingService keybindingService: IKeybindingService,
B
wip  
Benjamin Pasero 已提交
65
		@ITelemetryService telemetryService: ITelemetryService,
66
		@IMessageService messageService: IMessageService,
67 68
		@IMenuService menuService: IMenuService,
		@IQuickOpenService quickOpenService: IQuickOpenService
B
wip  
Benjamin Pasero 已提交
69
	) {
70
		super(contextMenuService, instantiationService, configurationService, editorService, editorGroupService, contextKeyService, keybindingService, telemetryService, messageService, menuService, quickOpenService);
B
wip  
Benjamin Pasero 已提交
71

B
Benjamin Pasero 已提交
72
		this.tabDisposeables = [];
73
		this.editorLabels = [];
B
wip  
Benjamin Pasero 已提交
74 75 76 77 78
	}

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

79
		this.editorActionsToolbar.context = { group };
B
wip  
Benjamin Pasero 已提交
80 81
	}

82
	public create(parent: HTMLElement): void {
83
		super.create(parent);
84

85
		this.titleContainer = parent;
B
wip  
Benjamin Pasero 已提交
86

87
		// Tabs Container
B
Benjamin Pasero 已提交
88
		this.tabsContainer = document.createElement('div');
89
		this.tabsContainer.setAttribute('role', 'tablist');
B
Benjamin Pasero 已提交
90
		DOM.addClass(this.tabsContainer, 'tabs-container');
91

92
		// Forward scrolling inside the container to our custom scrollbar
B
Benjamin Pasero 已提交
93 94 95 96 97 98 99 100
		this.toDispose.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.SCROLL, e => {
			if (DOM.hasClass(this.tabsContainer, 'scroll')) {
				this.scrollbar.updateState({
					scrollLeft: this.tabsContainer.scrollLeft // during DND the  container gets scrolled so we need to update the custom scrollbar
				});
			}
		}));

101 102 103 104 105 106
		// New file when double clicking on tabs container (but not tabs)
		this.toDispose.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DBLCLICK, e => {
			const target = e.target;
			if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) {
				DOM.EventHelper.stop(e);

107 108 109
				const group = this.context;

				return this.editorService.openEditor(this.untitledEditorService.createOrGet(), { pinned: true, index: group.count /* always at the end */ }); // untitled are always pinned
110 111 112
			}
		}));

113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
		// Custom Scrollbar
		this.scrollbar = new ScrollableElement(this.tabsContainer, {
			horizontal: ScrollbarVisibility.Auto,
			vertical: ScrollbarVisibility.Hidden,
			scrollYToX: true,
			useShadows: false,
			canUseTranslate3d: true,
			horizontalScrollbarSize: 3
		});

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

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

B
Benjamin Pasero 已提交
129 130
		// Drag over
		this.toDispose.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DRAG_OVER, (e: DragEvent) => {
B
Benjamin Pasero 已提交
131 132
			DOM.addClass(this.tabsContainer, 'scroll'); // enable support to scroll while dragging

B
Benjamin Pasero 已提交
133 134 135 136 137 138 139 140 141
			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');
B
Benjamin Pasero 已提交
142
			DOM.removeClass(this.tabsContainer, 'scroll');
B
Benjamin Pasero 已提交
143 144 145 146 147
		}));

		// Drag end
		this.toDispose.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DRAG_END, (e: DragEvent) => {
			DOM.removeClass(this.tabsContainer, 'dropfeedback');
B
Benjamin Pasero 已提交
148
			DOM.removeClass(this.tabsContainer, 'scroll');
B
Benjamin Pasero 已提交
149 150
		}));

B
Benjamin Pasero 已提交
151 152
		// Drop onto tabs container
		this.toDispose.push(DOM.addDisposableListener(this.tabsContainer, DOM.EventType.DROP, (e: DragEvent) => {
B
Benjamin Pasero 已提交
153
			DOM.removeClass(this.tabsContainer, 'dropfeedback');
B
Benjamin Pasero 已提交
154
			DOM.removeClass(this.tabsContainer, 'scroll');
B
Benjamin Pasero 已提交
155

B
Benjamin Pasero 已提交
156
			const target = e.target;
B
Benjamin Pasero 已提交
157
			if (target instanceof HTMLElement && target.className.indexOf('tabs-container') === 0) {
B
Benjamin Pasero 已提交
158 159
				const group = this.context;
				if (group) {
160 161 162
					const targetPosition = this.stacks.positionOfGroup(group);
					const targetIndex = group.count;

B
Benjamin Pasero 已提交
163
					this.onDrop(e, group, targetPosition, targetIndex);
B
Benjamin Pasero 已提交
164 165 166 167
				}
			}
		}));

168
		// Editor Actions Container
169 170 171
		const editorActionsContainer = document.createElement('div');
		DOM.addClass(editorActionsContainer, 'editor-actions');
		this.titleContainer.appendChild(editorActionsContainer);
172 173 174

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

B
Benjamin Pasero 已提交
177 178 179 180
	public allowDragging(element: HTMLElement): boolean {
		return (element.className === 'tabs-container');
	}

181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
	protected doUpdate(): void {
		if (!this.context) {
			return;
		}

		const group = this.context;

		// Tabs container activity state
		const isActive = this.stacks.isActive(group);
		if (isActive) {
			DOM.addClass(this.titleContainer, 'active');
		} else {
			DOM.removeClass(this.titleContainer, 'active');
		}

196 197
		// Compute labels and protect against duplicates
		const editorsOfGroup = this.context.getEditors();
B
Benjamin Pasero 已提交
198
		const labels = this.getUniqueTabLabels(editorsOfGroup);
199

200
		// Tab label and styles
201
		editorsOfGroup.forEach((editor, index) => {
202 203 204 205 206 207
			const tabContainer = this.tabsContainer.children[index];
			if (tabContainer instanceof HTMLElement) {
				const isPinned = group.isPinned(editor);
				const isActive = group.isActive(editor);
				const isDirty = editor.isDirty();

208 209
				const label = labels[index];
				const name = label.name;
B
Benjamin Pasero 已提交
210
				const description = label.hasAmbiguousName && label.description ? label.description : '';
211
				const verboseDescription = label.verboseDescription || '';
212

213
				// Container
214
				tabContainer.setAttribute('aria-label', `tab, ${name}`);
215
				tabContainer.title = verboseDescription;
216

217 218 219
				// Label
				const tabLabel = this.editorLabels[index];
				tabLabel.setLabel({ name, description, resource: getResource(editor) }, { extraClasses: ['tab-label'], italic: !isPinned });
220 221 222 223

				// Active state
				if (isActive) {
					DOM.addClass(tabContainer, 'active');
224
					tabContainer.setAttribute('aria-selected', 'true');
225 226 227
					this.activeTab = tabContainer;
				} else {
					DOM.removeClass(tabContainer, 'active');
228
					tabContainer.setAttribute('aria-selected', 'false');
229 230 231 232 233 234 235 236 237 238
				}

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

240
		// Update Editor Actions Toolbar
241
		this.updateEditorActionsToolbar();
242

B
Benjamin Pasero 已提交
243
		// Ensure the active tab is always revealed
244
		this.layout();
245 246
	}

B
Benjamin Pasero 已提交
247 248 249 250
	private getUniqueTabLabels(editors: IEditorInput[]): IEditorInputLabel[] {
		const labels: IEditorInputLabel[] = [];

		const mapLabelToDuplicates = new LinkedMap<string, IEditorInputLabel[]>();
251
		const mapLabelAndDescriptionToDuplicates = new LinkedMap<string, IEditorInputLabel[]>();
B
Benjamin Pasero 已提交
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269

		// Build labels and descriptions for each editor
		editors.forEach(editor => {
			let description = editor.getDescription();
			if (description && description.indexOf(paths.nativeSep) >= 0) {
				description = paths.basename(description); // optimize for editors that show paths and build a shorter description to keep tab width small
			}

			const item: IEditorInputLabel = {
				editor,
				name: editor.getName(),
				description,
				verboseDescription: editor.getDescription(true)
			};
			labels.push(item);

			mapLabelToDuplicates.getOrSet(item.name, []).push(item);
			if (item.description) {
270
				mapLabelAndDescriptionToDuplicates.getOrSet(item.name + item.description, []).push(item);
B
Benjamin Pasero 已提交
271 272 273 274 275 276 277 278 279 280 281 282 283
			}
		});

		// Mark label duplicates
		const labelDuplicates = mapLabelToDuplicates.values();
		labelDuplicates.forEach(duplicates => {
			if (duplicates.length > 1) {
				duplicates.forEach(duplicate => {
					duplicate.hasAmbiguousName = true;
				});
			}
		});

284 285
		// React to duplicates for combination of label and description
		const descriptionDuplicates = mapLabelAndDescriptionToDuplicates.values();
B
Benjamin Pasero 已提交
286 287 288 289 290 291 292 293 294 295 296
		descriptionDuplicates.forEach(duplicates => {
			if (duplicates.length > 1) {
				duplicates.forEach(duplicate => {
					duplicate.description = duplicate.editor.getDescription(); // fallback to full description if the short description still has duplicates
				});
			}
		});

		return labels;
	}

297
	protected doRefresh(): void {
B
wip  
Benjamin Pasero 已提交
298
		const group = this.context;
299
		const editor = group && group.activeEditor;
B
wip  
Benjamin Pasero 已提交
300
		if (!editor) {
301 302
			this.clearTabs();

303
			this.clearEditorActionsToolbar();
B
wip  
Benjamin Pasero 已提交
304 305 306 307

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

B
Benjamin Pasero 已提交
308 309
		// Refresh Tabs
		this.refreshTabs(group);
310

311
		// Update Tabs
312
		this.doUpdate();
B
wip  
Benjamin Pasero 已提交
313 314
	}

315
	private clearTabs(): void {
B
Benjamin Pasero 已提交
316
		DOM.clearNode(this.tabsContainer);
317 318 319

		this.tabDisposeables = dispose(this.tabDisposeables);
		this.editorLabels = dispose(this.editorLabels);
320 321 322 323 324 325
	}

	private refreshTabs(group: IEditorGroup): void {

		// Empty container first
		this.clearTabs();
B
Benjamin Pasero 已提交
326 327

		const tabContainers: HTMLElement[] = [];
B
Benjamin Pasero 已提交
328 329 330

		// Add a tab for each opened editor
		this.context.getEditors().forEach(editor => {
331

332
			// Tab Container
B
Benjamin Pasero 已提交
333
			const tabContainer = document.createElement('div');
B
Benjamin Pasero 已提交
334
			tabContainer.draggable = true;
B
Benjamin Pasero 已提交
335
			tabContainer.tabIndex = 0;
336
			tabContainer.setAttribute('role', 'presentation'); // cannot use role "tab" here due to https://github.com/Microsoft/vscode/issues/8659
B
Benjamin Pasero 已提交
337 338
			DOM.addClass(tabContainer, 'tab monaco-editor-background');
			tabContainers.push(tabContainer);
B
Benjamin Pasero 已提交
339

340 341 342 343 344 345
			if (!this.showTabCloseButton) {
				DOM.addClass(tabContainer, 'no-close-button');
			} else {
				DOM.removeClass(tabContainer, 'no-close-button');
			}

346
			// Tab Editor Label
B
Benjamin Pasero 已提交
347
			const editorLabel = this.instantiationService.createInstance(EditorLabel, tabContainer, void 0);
348
			this.editorLabels.push(editorLabel);
349

B
Benjamin Pasero 已提交
350 351 352 353 354 355
			// 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") });
B
Benjamin Pasero 已提交
356
			bar.push(this.closeEditorAction, { icon: true, label: false, keybinding: this.getKeybindingLabel(this.closeEditorAction) });
B
Benjamin Pasero 已提交
357 358

			this.tabDisposeables.push(bar);
B
Benjamin Pasero 已提交
359 360 361

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

B
Benjamin Pasero 已提交
364 365
		// Add to tabs container
		tabContainers.forEach(tab => this.tabsContainer.appendChild(tab));
366 367 368 369 370 371 372
	}

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

373 374 375 376 377 378 379 380 381
		const visibleContainerWidth = this.tabsContainer.offsetWidth;
		const totalContainerWidth = this.tabsContainer.scrollWidth;

		// Update scrollbar
		this.scrollbar.updateState({
			width: visibleContainerWidth,
			scrollWidth: totalContainerWidth
		});

B
Benjamin Pasero 已提交
382
		// Always reveal the active one
B
Benjamin Pasero 已提交
383 384 385
		const containerScrollPosX = this.tabsContainer.scrollLeft;
		const activeTabPosX = this.activeTab.offsetLeft;
		const activeTabWidth = this.activeTab.offsetWidth;
386
		const activeTabFits = activeTabWidth <= visibleContainerWidth;
B
Benjamin Pasero 已提交
387 388

		// Tab is overflowing to the right: Scroll minimally until the element is fully visible to the right
389 390
		// 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) {
391 392 393
			this.scrollbar.updateState({
				scrollLeft: containerScrollPosX + ((activeTabPosX + activeTabWidth) /* right corner of tab */ - (containerScrollPosX + visibleContainerWidth) /* right corner of view port */)
			});
B
Benjamin Pasero 已提交
394 395
		}

396 397
		// Tab is overlflowng to the left or does not fit: Scroll it into view to the left
		else if (containerScrollPosX > activeTabPosX || !activeTabFits) {
398 399 400
			this.scrollbar.updateState({
				scrollLeft: this.activeTab.offsetLeft
			});
B
Benjamin Pasero 已提交
401
		}
B
Benjamin Pasero 已提交
402 403
	}

B
Benjamin Pasero 已提交
404
	private hookTabListeners(tab: HTMLElement, identifier: IEditorIdentifier): void {
B
Benjamin Pasero 已提交
405 406
		const {editor, group} = identifier;
		const position = this.stacks.positionOfGroup(group);
B
Benjamin Pasero 已提交
407 408

		// Open on Click
B
Benjamin Pasero 已提交
409
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
410 411
			tab.blur();

412
			if (e.button === 0 /* Left Button */ && !DOM.findParentWithClass(<any>e.target || e.srcElement, 'monaco-action-bar', 'tab')) {
413 414 415 416 417 418 419
				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
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.MOUSE_UP, (e: MouseEvent) => {
			DOM.EventHelper.stop(e);
420
			tab.blur();
421 422 423

			if (e.button === 1 /* Middle Button */) {
				this.editorService.closeEditor(position, editor).done(null, errors.onUnexpectedError);
B
Benjamin Pasero 已提交
424
			}
B
Benjamin Pasero 已提交
425
		}));
B
Benjamin Pasero 已提交
426

427 428 429 430 431 432 433 434 435 436
		// Context menu on Shift+F10
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
			const event = new StandardKeyboardEvent(e);
			if (event.shiftKey && event.keyCode === KeyCode.F10) {
				DOM.EventHelper.stop(e);

				this.onContextMenu(identifier, e, tab);
			}
		}));

B
Benjamin Pasero 已提交
437
		// Keyboard accessibility
B
Benjamin Pasero 已提交
438 439
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.KEY_UP, (e: KeyboardEvent) => {
			const event = new StandardKeyboardEvent(e);
B
Benjamin Pasero 已提交
440
			let handled = false;
B
Benjamin Pasero 已提交
441 442

			// Run action on Enter/Space
A
Alexandru Dima 已提交
443
			if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
B
Benjamin Pasero 已提交
444
				handled = true;
B
Benjamin Pasero 已提交
445
				this.editorService.openEditor(editor, null, position).done(null, errors.onUnexpectedError);
B
Benjamin Pasero 已提交
446 447
			}

B
Benjamin Pasero 已提交
448
			// Navigate in editors
A
Alexandru Dima 已提交
449
			else if ([KeyCode.LeftArrow, KeyCode.RightArrow, KeyCode.UpArrow, KeyCode.DownArrow, KeyCode.Home, KeyCode.End].some(kb => event.equals(kb))) {
B
Benjamin Pasero 已提交
450
				const index = group.indexOf(editor);
B
Benjamin Pasero 已提交
451

B
Benjamin Pasero 已提交
452
				let targetIndex: number;
A
Alexandru Dima 已提交
453
				if (event.equals(KeyCode.LeftArrow) || event.equals(KeyCode.UpArrow)) {
B
Benjamin Pasero 已提交
454
					targetIndex = index - 1;
A
Alexandru Dima 已提交
455
				} else if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.DownArrow)) {
B
Benjamin Pasero 已提交
456
					targetIndex = index + 1;
A
Alexandru Dima 已提交
457
				} else if (event.equals(KeyCode.Home)) {
B
Benjamin Pasero 已提交
458 459 460 461 462
					targetIndex = 0;
				} else {
					targetIndex = group.count - 1;
				}

B
Benjamin Pasero 已提交
463 464 465
				const target = group.getEditor(targetIndex);
				if (target) {
					handled = true;
466
					this.editorService.openEditor(target, { preserveFocus: true }, position).done(null, errors.onUnexpectedError);
B
Benjamin Pasero 已提交
467 468 469
					(<HTMLElement>this.tabsContainer.childNodes[targetIndex]).focus();
				}
			}
B
Benjamin Pasero 已提交
470

B
Benjamin Pasero 已提交
471
			if (handled) {
472
				DOM.EventHelper.stop(e, true);
B
Benjamin Pasero 已提交
473
			}
474 475 476 477 478

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

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

485
			this.editorGroupService.pinEditor(group, editor);
B
Benjamin Pasero 已提交
486
		}));
B
Benjamin Pasero 已提交
487

B
Benjamin Pasero 已提交
488
		// Context menu
489
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.CONTEXT_MENU, (e: Event) => this.onContextMenu(identifier, e, tab)));
B
Benjamin Pasero 已提交
490 491 492

		// Drag start
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_START, (e: DragEvent) => {
493
			this.onEditorDragStart({ editor, group });
494
			e.dataTransfer.effectAllowed = 'copyMove';
B
Benjamin Pasero 已提交
495

B
Benjamin Pasero 已提交
496
			// Insert transfer accordingly
B
Benjamin Pasero 已提交
497 498
			const fileInput = asFileEditorInput(editor, true);
			if (fileInput) {
B
Benjamin Pasero 已提交
499 500 501
				const resource = fileInput.getResource().toString();
				e.dataTransfer.setData('URL', resource); // enables cross window DND of tabs
				e.dataTransfer.setData('DownloadURL', [MIME_BINARY, editor.getName(), resource].join(':')); // enables support to drag a tab as file to desktop
B
Benjamin Pasero 已提交
502
			}
B
Benjamin Pasero 已提交
503 504
		}));

505 506 507 508 509 510
		// 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 已提交
511
		// Drag over
512 513
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_ENTER, (e: DragEvent) => {
			counter++;
B
Benjamin Pasero 已提交
514 515 516 517 518
			DOM.addClass(tab, 'dropfeedback');
		}));

		// Drag leave
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_LEAVE, (e: DragEvent) => {
519 520 521 522
			counter--;
			if (counter === 0) {
				DOM.removeClass(tab, 'dropfeedback');
			}
B
Benjamin Pasero 已提交
523 524 525 526
		}));

		// Drag end
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.DRAG_END, (e: DragEvent) => {
527
			counter = 0;
B
Benjamin Pasero 已提交
528
			DOM.removeClass(tab, 'dropfeedback');
B
Benjamin Pasero 已提交
529

530
			this.onEditorDragEnd();
B
Benjamin Pasero 已提交
531 532 533 534
		}));

		// Drop
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.DROP, (e: DragEvent) => {
535
			counter = 0;
B
Benjamin Pasero 已提交
536 537
			DOM.removeClass(tab, 'dropfeedback');

538 539 540
			const targetPosition = this.stacks.positionOfGroup(group);
			const targetIndex = group.indexOf(editor);

B
Benjamin Pasero 已提交
541 542 543
			this.onDrop(e, group, targetPosition, targetIndex);
		}));
	}
B
Benjamin Pasero 已提交
544

B
Benjamin Pasero 已提交
545
	private onDrop(e: DragEvent, group: IEditorGroup, targetPosition: Position, targetIndex: number): void {
546 547
		DOM.removeClass(this.tabsContainer, 'dropfeedback');
		DOM.removeClass(this.tabsContainer, 'scroll');
548

B
Benjamin Pasero 已提交
549 550 551 552
		// Local DND
		const draggedEditor = TabsTitleControl.getDraggedEditor();
		if (draggedEditor) {
			DOM.EventHelper.stop(e, true);
553

B
Benjamin Pasero 已提交
554 555 556
			// Move editor to target position and index
			if (this.isMoveOperation(e, draggedEditor.group, group)) {
				this.editorGroupService.moveEditor(draggedEditor.editor, draggedEditor.group, group, targetIndex);
B
Benjamin Pasero 已提交
557
			}
558

B
Benjamin Pasero 已提交
559
			// Copy: just open editor at target index
560
			else {
B
Benjamin Pasero 已提交
561
				this.editorService.openEditor(draggedEditor.editor, { pinned: true, index: targetIndex }, targetPosition).done(null, errors.onUnexpectedError);
562
			}
B
Benjamin Pasero 已提交
563 564 565 566 567 568 569 570

			this.onEditorDragEnd();
		}

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

573
	private handleExternalDrop(e: DragEvent, targetPosition: Position, targetIndex: number): void {
574
		const resources = extractResources(e).filter(r => r.scheme === 'file' || r.scheme === 'untitled');
575 576 577

		// Open resources if found
		if (resources.length) {
B
Benjamin Pasero 已提交
578
			DOM.EventHelper.stop(e, true);
579 580 581 582 583 584

			this.editorService.openEditors(resources.map(resource => {
				return {
					input: { resource, options: { pinned: true, index: targetIndex } },
					position: targetPosition
				};
585 586 587 588
			})).done(() => {
				this.editorGroupService.focusGroup(targetPosition);
				window.focus();
			}, errors.onUnexpectedError);
589 590 591
		}
	}

592 593 594 595 596
	private isMoveOperation(e: DragEvent, source: IEditorGroup, target: IEditorGroup) {
		const isCopy = (e.ctrlKey && !isMacintosh) || (e.altKey && isMacintosh);

		return !isCopy || source.id === target.id;
	}
B
wip  
Benjamin Pasero 已提交
597
}