tabsTitleControl.ts 11.1 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 73 74 75 76 77 78 79
		this.tabsContainer = document.createElement('div');
		DOM.addClass(this.tabsContainer, 'tabs-container');
		this.titleContainer.appendChild(this.tabsContainer);

		// 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;
			}
		}));
80

B
Benjamin Pasero 已提交
81
		// Group Actions
B
Benjamin Pasero 已提交
82 83 84 85
		const groupActionsContainer = document.createElement('div');
		DOM.addClass(groupActionsContainer, 'group-actions');
		this.titleContainer.appendChild(groupActionsContainer);
		this.groupActionsToolbar = this.doCreateToolbar($(groupActionsContainer));
B
wip  
Benjamin Pasero 已提交
86 87
	}

B
Benjamin Pasero 已提交
88 89 90 91
	public allowDragging(element: HTMLElement): boolean {
		return (element.className === 'tabs-container');
	}

B
wip  
Benjamin Pasero 已提交
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
	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
		}

108
		// Activity state
B
Benjamin Pasero 已提交
109
		const isActive = this.stacks.isActive(group);
110
		if (isActive) {
B
Benjamin Pasero 已提交
111
			DOM.addClass(this.titleContainer, 'active');
112
		} else {
B
Benjamin Pasero 已提交
113
			DOM.removeClass(this.titleContainer, 'active');
114 115
		}

B
wip  
Benjamin Pasero 已提交
116 117
		// Update Group Actions Toolbar
		const groupActions = this.getGroupActions(group);
B
Benjamin Pasero 已提交
118 119
		const primaryGroupActions = groupActions.primary;
		const secondaryGroupActions = groupActions.secondary;
B
wip  
Benjamin Pasero 已提交
120 121 122 123 124 125 126 127
		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 已提交
128 129 130

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

B
Benjamin Pasero 已提交
133 134 135
	private refreshTabs(group: IEditorGroup): void {

		// Empty container first
B
Benjamin Pasero 已提交
136 137 138 139 140
		DOM.clearNode(this.tabsContainer);
		dispose(this.tabDisposeables);
		this.tabDisposeables = [];

		const tabContainers: HTMLElement[] = [];
B
Benjamin Pasero 已提交
141 142 143 144 145 146 147

		// 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 已提交
148 149 150
			const tabContainer = document.createElement('div');
			DOM.addClass(tabContainer, 'tab monaco-editor-background');
			tabContainers.push(tabContainer);
B
Benjamin Pasero 已提交
151

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

B
Benjamin Pasero 已提交
155 156 157 158 159 160
			// Pinned state
			if (isPinned) {
				DOM.addClass(tabContainer, 'pinned');
			} else {
				DOM.removeClass(tabContainer, 'pinned');
			}
B
Benjamin Pasero 已提交
161

B
Benjamin Pasero 已提交
162 163 164 165 166 167 168
			// Active state
			if (isActive) {
				DOM.addClass(tabContainer, 'active');
				this.activeTab = tabContainer;
			} else {
				DOM.removeClass(tabContainer, 'active');
			}
B
Benjamin Pasero 已提交
169

B
Benjamin Pasero 已提交
170 171 172 173 174 175
			// Dirty State
			if (isDirty) {
				DOM.addClass(tabContainer, 'dirty');
			} else {
				DOM.removeClass(tabContainer, 'dirty');
			}
B
Benjamin Pasero 已提交
176

B
Benjamin Pasero 已提交
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
			// 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 已提交
197
		});
B
Benjamin Pasero 已提交
198

B
Benjamin Pasero 已提交
199 200 201
		// Add to tabs container
		tabContainers.forEach(tab => this.tabsContainer.appendChild(tab));

202 203 204 205 206 207 208 209
		this.layout();
	}

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

B
Benjamin Pasero 已提交
210
		// Always reveal the active one
B
Benjamin Pasero 已提交
211 212 213 214
		const containerWidth = this.tabsContainer.offsetWidth;
		const containerScrollPosX = this.tabsContainer.scrollLeft;
		const activeTabPosX = this.activeTab.offsetLeft;
		const activeTabWidth = this.activeTab.offsetWidth;
B
Benjamin Pasero 已提交
215 216 217

		// 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 已提交
218
			this.tabsContainer.scrollLeft += ((activeTabPosX + activeTabWidth) /* right corner of tab */ - (containerScrollPosX + containerWidth) /* right corner of view port */);
B
Benjamin Pasero 已提交
219 220 221 222
		}

		// Tab is overlflowng to the left: Scroll it into view to the left
		else if (containerScrollPosX > activeTabPosX) {
B
Benjamin Pasero 已提交
223
			this.tabsContainer.scrollLeft = this.activeTab.offsetLeft;
B
Benjamin Pasero 已提交
224
		}
225 226 227 228 229 230

		// 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 已提交
231 232
	}

B
Benjamin Pasero 已提交
233
	private hookTabListeners(tab: HTMLElement, identifier: IEditorIdentifier): void {
B
Benjamin Pasero 已提交
234 235
		const {editor, group} = identifier;
		const position = this.stacks.positionOfGroup(group);
B
Benjamin Pasero 已提交
236 237

		// Open on Click
B
Benjamin Pasero 已提交
238
		this.tabDisposeables.push(DOM.addDisposableListener(tab, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
B
Benjamin Pasero 已提交
239 240
			DOM.EventHelper.stop(e);

241
			if (e.button === 0 /* Left Button */ && !DOM.findParentWithClass(<any>e.target || e.srcElement, 'monaco-action-bar', 'tab')) {
B
Benjamin Pasero 已提交
242
				this.editorService.openEditor(editor, null, position).done(null, errors.onUnexpectedError);
B
Benjamin Pasero 已提交
243
			}
B
Benjamin Pasero 已提交
244
		}));
B
Benjamin Pasero 已提交
245 246

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

B
Benjamin Pasero 已提交
250
			this.editorGroupService.pinEditor(position, editor);
B
Benjamin Pasero 已提交
251
		}));
B
Benjamin Pasero 已提交
252 253

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

			if (e.button === 1 /* Middle Button */) {
B
Benjamin Pasero 已提交
258
				this.editorService.closeEditor(position, editor).done(null, errors.onUnexpectedError);
B
Benjamin Pasero 已提交
259
			}
B
Benjamin Pasero 已提交
260
		}));
B
Benjamin Pasero 已提交
261 262

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

B
Benjamin Pasero 已提交
266
			let anchor: HTMLElement | { x: number, y: number } = tab;
B
Benjamin Pasero 已提交
267 268 269 270 271 272 273
			if (e instanceof MouseEvent) {
				const event = new StandardMouseEvent(e);
				anchor = { x: event.posx, y: event.posy };
			}

			this.contextMenuService.showContextMenu({
				getAnchor: () => anchor,
B
Benjamin Pasero 已提交
274 275
				getActions: () => TPromise.as(this.getTabActions(identifier)),
				getActionsContext: () => identifier,
B
Benjamin Pasero 已提交
276 277 278 279 280 281 282 283 284
				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 已提交
285
		}));
B
Benjamin Pasero 已提交
286 287
	}

B
Benjamin Pasero 已提交
288
	private getTabActions(identifier: IEditorIdentifier): IAction[] {
B
Benjamin Pasero 已提交
289
		const {editor, group} = identifier;
B
Benjamin Pasero 已提交
290 291

		// Enablement
B
Benjamin Pasero 已提交
292 293 294
		this.closeOtherEditorsAction.enabled = group.count > 1;
		this.pinEditorAction.enabled = !group.isPinned(editor);
		this.closeRightEditorsAction.enabled = group.indexOf(editor) !== group.count - 1;
B
Benjamin Pasero 已提交
295

296
		// Actions: For all editors
B
Benjamin Pasero 已提交
297
		const actions: IAction[] = [
B
Benjamin Pasero 已提交
298 299
			this.closeEditorAction,
			this.closeOtherEditorsAction,
B
Benjamin Pasero 已提交
300
			this.closeRightEditorsAction,
B
Benjamin Pasero 已提交
301
			new Separator(),
302
			this.pinEditorAction,
B
Benjamin Pasero 已提交
303
		];
304

305
		// Actions: For active editor
B
Benjamin Pasero 已提交
306 307
		if (group.isActive(editor)) {
			const editorActions = this.getEditorActions(group);
308 309 310
			if (editorActions.primary.length) {
				actions.push(new Separator(), ...prepareActions(editorActions.primary));
			}
311

312 313 314
			if (editorActions.secondary.length) {
				actions.push(new Separator(), ...prepareActions(editorActions.secondary));
			}
315 316 317
		}

		return actions;
B
Benjamin Pasero 已提交
318 319
	}

B
wip  
Benjamin Pasero 已提交
320 321 322
	public dispose(): void {
		super.dispose();

B
Benjamin Pasero 已提交
323
		// Toolbar
B
wip  
Benjamin Pasero 已提交
324 325 326
		this.groupActionsToolbar.dispose();
	}
}