titleControl.ts 17.9 KB
Newer Older
B
Benjamin Pasero 已提交
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

B
Benjamin Pasero 已提交
8
import 'vs/css!./media/titlecontrol';
B
Benjamin Pasero 已提交
9 10
import nls = require('vs/nls');
import {Registry} from 'vs/platform/platform';
11
import {Scope, IActionBarRegistry, Extensions, prepareActions} from 'vs/workbench/browser/actionBarRegistry';
B
Benjamin Pasero 已提交
12 13
import {IAction, Action} from 'vs/base/common/actions';
import errors = require('vs/base/common/errors');
B
Benjamin Pasero 已提交
14
import DOM = require('vs/base/browser/dom');
15
import {TPromise} from 'vs/base/common/winjs.base';
B
Benjamin Pasero 已提交
16 17
import {BaseEditor, IEditorInputActionContext} from 'vs/workbench/browser/parts/editor/baseEditor';
import {RunOnceScheduler} from 'vs/base/common/async';
18
import {isCommonCodeEditor, isCommonDiffEditor} from 'vs/editor/common/editorCommon';
19
import arrays = require('vs/base/common/arrays');
20
import {IEditorStacksModel, IEditorGroup, IEditorIdentifier, EditorInput, IWorkbenchEditorConfiguration, IStacksModelChangeEvent, getResource} from 'vs/workbench/common/editor';
B
Benjamin Pasero 已提交
21 22 23 24 25
import {EventType as BaseEventType} from 'vs/base/common/events';
import {IActionItem, ActionsOrientation, Separator} from 'vs/base/browser/ui/actionbar/actionbar';
import {ToolBar} from 'vs/base/browser/ui/toolbar/toolbar';
import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService';
import {IContextMenuService} from 'vs/platform/contextview/browser/contextView';
26
import {IConfigurationService} from 'vs/platform/configuration/common/configuration';
B
Benjamin Pasero 已提交
27 28
import {IEditorGroupService} from 'vs/workbench/services/group/common/groupService';
import {IMessageService, Severity} from 'vs/platform/message/common/message';
29
import {StandardMouseEvent} from 'vs/base/browser/mouseEvent';
B
Benjamin Pasero 已提交
30 31
import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
32
import {IQuickOpenService} from 'vs/workbench/services/quickopen/common/quickOpenService';
33
import {IKeybindingService} from 'vs/platform/keybinding/common/keybinding';
34
import {IContextKeyService} from 'vs/platform/contextkey/common/contextkey';
35
import {CloseEditorsInGroupAction, SplitEditorAction, CloseEditorAction, KeepEditorAction, CloseOtherEditorsInGroupAction, CloseRightEditorsInGroupAction, ShowEditorsInGroupAction} from 'vs/workbench/browser/parts/editor/editorActions';
B
Benjamin Pasero 已提交
36
import {IDisposable, dispose} from 'vs/base/common/lifecycle';
37
import {createActionItem, fillInActions} from 'vs/platform/actions/browser/menuItemActionItem';
38
import {IMenuService, MenuId} from 'vs/platform/actions/common/actions';
39
import {ResourceContextKey} from 'vs/platform/actions/common/resourceContextKey';
B
Benjamin Pasero 已提交
40 41 42 43 44 45 46 47

export interface IToolbarActions {
	primary: IAction[];
	secondary: IAction[];
}

export interface ITitleAreaControl {
	setContext(group: IEditorGroup): void;
48
	hasContext(): boolean;
B
Benjamin Pasero 已提交
49
	allowDragging(element: HTMLElement): boolean;
50
	setDragged(dragged: boolean): void;
51
	create(parent: HTMLElement): void;
52
	getContainer(): HTMLElement;
53 54
	refresh(instant?: boolean): void;
	update(instant?: boolean): void;
55
	layout(): void;
B
Benjamin Pasero 已提交
56 57 58
	dispose(): void;
}

59
export abstract class TitleControl implements ITitleAreaControl {
60 61 62

	private static draggedEditor: IEditorIdentifier;

B
Benjamin Pasero 已提交
63 64
	protected stacks: IEditorStacksModel;
	protected context: IEditorGroup;
65
	protected toDispose: IDisposable[];
B
Benjamin Pasero 已提交
66

67 68
	protected dragged: boolean;

B
Benjamin Pasero 已提交
69
	protected closeEditorAction: CloseEditorAction;
B
Benjamin Pasero 已提交
70
	protected pinEditorAction: KeepEditorAction;
B
Benjamin Pasero 已提交
71
	protected closeOtherEditorsAction: CloseOtherEditorsInGroupAction;
B
Benjamin Pasero 已提交
72
	protected closeRightEditorsAction: CloseRightEditorsInGroupAction;
B
Benjamin Pasero 已提交
73 74
	protected closeEditorsInGroupAction: CloseEditorsInGroupAction;
	protected splitEditorAction: SplitEditorAction;
B
Benjamin Pasero 已提交
75
	protected showEditorsInGroupAction: ShowEditorsInGroupAction;
B
Benjamin Pasero 已提交
76

77 78
	private parent: HTMLElement;

79 80 81
	private previewEditors: boolean;
	private showTabs: boolean;

82 83 84 85
	private currentPrimaryEditorActionIds: string[] = [];
	private currentSecondaryEditorActionIds: string[] = [];
	protected editorActionsToolbar: ToolBar;

B
Benjamin Pasero 已提交
86 87
	private mapActionsToEditors: { [editorId: string]: IToolbarActions; };
	private scheduler: RunOnceScheduler;
88
	private refreshScheduled: boolean;
B
Benjamin Pasero 已提交
89

90
	private resourceContext: ResourceContextKey;
91
	private disposeOnEditorActions: IDisposable[] = [];
92

B
Benjamin Pasero 已提交
93 94 95
	constructor(
		@IContextMenuService protected contextMenuService: IContextMenuService,
		@IInstantiationService protected instantiationService: IInstantiationService,
96
		@IConfigurationService protected configurationService: IConfigurationService,
B
Benjamin Pasero 已提交
97 98
		@IWorkbenchEditorService protected editorService: IWorkbenchEditorService,
		@IEditorGroupService protected editorGroupService: IEditorGroupService,
99
		@IContextKeyService protected contextKeyService: IContextKeyService,
100
		@IKeybindingService protected keybindingService: IKeybindingService,
B
Benjamin Pasero 已提交
101
		@ITelemetryService protected telemetryService: ITelemetryService,
102
		@IMessageService protected messageService: IMessageService,
103 104
		@IMenuService protected menuService: IMenuService,
		@IQuickOpenService protected quickOpenService: IQuickOpenService
B
Benjamin Pasero 已提交
105 106 107 108 109
	) {
		this.toDispose = [];
		this.stacks = editorGroupService.getStacksModel();
		this.mapActionsToEditors = Object.create(null);

110 111
		this.onConfigurationUpdated(configurationService.getConfiguration<IWorkbenchEditorConfiguration>());

112
		this.scheduler = new RunOnceScheduler(() => this.onSchedule(), 0);
B
Benjamin Pasero 已提交
113 114
		this.toDispose.push(this.scheduler);

115 116
		this.resourceContext = instantiationService.createInstance(ResourceContextKey);

B
Benjamin Pasero 已提交
117
		this.initActions();
118 119 120
		this.registerListeners();
	}

121 122 123 124
	public static getDraggedEditor(): IEditorIdentifier {
		return TitleControl.draggedEditor;
	}

125 126 127 128
	public setDragged(dragged: boolean): void {
		this.dragged = dragged;
	}

129 130 131 132 133 134 135 136
	protected onEditorDragStart(editor: IEditorIdentifier): void {
		TitleControl.draggedEditor = editor;
	}

	protected onEditorDragEnd(): void {
		TitleControl.draggedEditor = void 0;
	}

137 138
	private registerListeners(): void {
		this.toDispose.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationUpdated(e.config)));
B
Benjamin Pasero 已提交
139 140 141 142 143 144 145
		this.toDispose.push(this.stacks.onModelChanged(e => this.onStacksChanged(e)));
	}

	private onStacksChanged(e: IStacksModelChangeEvent): void {
		if (e.structural) {
			this.updateSplitActionEnablement();
		}
146 147 148
	}

	private onConfigurationUpdated(config: IWorkbenchEditorConfiguration): void {
149 150
		this.previewEditors = config.workbench && config.workbench.editor && config.workbench.editor.enablePreview;
		this.showTabs = config.workbench && config.workbench.editor && config.workbench.editor.showTabs;
B
Benjamin Pasero 已提交
151 152
	}

B
Benjamin Pasero 已提交
153
	private updateSplitActionEnablement(): void {
154 155 156 157 158 159 160 161 162 163
		if (!this.context) {
			return;
		}

		const groupCount = this.stacks.groups.length;

		// Split editor
		this.splitEditorAction.enabled = groupCount < 3;
	}

164 165 166 167 168 169 170 171 172 173
	private onSchedule(): void {
		if (this.refreshScheduled) {
			this.doRefresh();
		} else {
			this.doUpdate();
		}

		this.refreshScheduled = false;
	}

B
Benjamin Pasero 已提交
174 175
	public setContext(group: IEditorGroup): void {
		this.context = group;
176 177
	}

178 179 180 181
	public hasContext(): boolean {
		return !!this.context;
	}

182 183 184 185 186 187 188 189
	public update(instant?: boolean): void {
		if (instant) {
			this.scheduler.cancel();
			this.onSchedule();
		} else {
			this.scheduler.schedule();
		}
	}
B
Benjamin Pasero 已提交
190

191 192 193 194 195 196 197 198 199
	public refresh(instant?: boolean) {
		this.refreshScheduled = true;

		if (instant) {
			this.scheduler.cancel();
			this.onSchedule();
		} else {
			this.scheduler.schedule();
		}
B
Benjamin Pasero 已提交
200 201
	}

202 203 204 205 206 207 208
	public create(parent: HTMLElement): void {
		this.parent = parent;
	}

	public getContainer(): HTMLElement {
		return this.parent;
	}
209

210 211 212 213 214
	protected abstract doRefresh(): void;

	protected doUpdate(): void {
		this.doRefresh();
	}
B
Benjamin Pasero 已提交
215

216 217 218 219
	public layout(): void {
		// Subclasses can opt in to react on layout
	}

B
Benjamin Pasero 已提交
220
	public allowDragging(element: HTMLElement): boolean {
B
Benjamin Pasero 已提交
221
		return !DOM.findParentWithClass(element, 'monaco-action-bar', 'one-editor-silo');
B
Benjamin Pasero 已提交
222 223
	}

B
Benjamin Pasero 已提交
224 225
	private initActions(): void {
		this.closeEditorAction = this.instantiationService.createInstance(CloseEditorAction, CloseEditorAction.ID, nls.localize('close', "Close"));
B
Benjamin Pasero 已提交
226
		this.closeOtherEditorsAction = this.instantiationService.createInstance(CloseOtherEditorsInGroupAction, CloseOtherEditorsInGroupAction.ID, nls.localize('closeOthers', "Close Others"));
B
Benjamin Pasero 已提交
227 228
		this.closeRightEditorsAction = this.instantiationService.createInstance(CloseRightEditorsInGroupAction, CloseRightEditorsInGroupAction.ID, nls.localize('closeRight', "Close to the Right"));
		this.closeEditorsInGroupAction = this.instantiationService.createInstance(CloseEditorsInGroupAction, CloseEditorsInGroupAction.ID, nls.localize('closeAll', "Close All"));
229
		this.pinEditorAction = this.instantiationService.createInstance(KeepEditorAction, KeepEditorAction.ID, nls.localize('keepOpen', "Keep Open"));
230
		this.showEditorsInGroupAction = this.instantiationService.createInstance(ShowEditorsInGroupAction, ShowEditorsInGroupAction.ID, nls.localize('showOpenedEditors', "Show Opened Editors"));
B
Benjamin Pasero 已提交
231 232 233
		this.splitEditorAction = this.instantiationService.createInstance(SplitEditorAction, SplitEditorAction.ID, SplitEditorAction.LABEL);
	}

234 235
	protected createEditorActionsToolBar(container: HTMLElement): void {
		this.editorActionsToolbar = new ToolBar(container, this.contextMenuService, {
B
Benjamin Pasero 已提交
236 237 238 239
			actionItemProvider: (action: Action) => this.actionItemProvider(action),
			orientation: ActionsOrientation.HORIZONTAL,
			ariaLabel: nls.localize('araLabelEditorActions', "Editor actions"),
			getKeyBinding: (action) => {
240
				const opts = this.keybindingService.lookupKeybindings(action.id);
B
Benjamin Pasero 已提交
241 242 243 244 245 246 247 248 249
				if (opts.length > 0) {
					return opts[0]; // only take the first one
				}

				return null;
			}
		});

		// Action Run Handling
250
		this.toDispose.push(this.editorActionsToolbar.actionRunner.addListener2(BaseEventType.RUN, (e: any) => {
B
Benjamin Pasero 已提交
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281

			// Check for Error
			if (e.error && !errors.isPromiseCanceledError(e.error)) {
				this.messageService.show(Severity.Error, e.error);
			}

			// Log in telemetry
			if (this.telemetryService) {
				this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: 'editorPart' });
			}
		}));
	}

	protected actionItemProvider(action: Action): IActionItem {
		if (!this.context) {
			return null;
		}

		const group = this.context;
		const position = this.stacks.positionOfGroup(group);
		const editor = this.editorService.getVisibleEditors()[position];

		let actionItem: IActionItem;

		// Check Active Editor
		if (editor instanceof BaseEditor) {
			actionItem = editor.getActionItem(action);
		}

		// Check Registry
		if (!actionItem) {
282
			const actionBarRegistry = Registry.as<IActionBarRegistry>(Extensions.Actionbar);
B
Benjamin Pasero 已提交
283 284 285
			actionItem = actionBarRegistry.getActionItemForContext(Scope.EDITOR, { input: editor && editor.input, editor, position }, action);
		}

286 287
		// Check extensions
		if (!actionItem) {
288
			actionItem = createActionItem(action, this.keybindingService, this.messageService);
289 290
		}

B
Benjamin Pasero 已提交
291 292 293
		return actionItem;
	}

294
	protected getEditorActions(identifier: IEditorIdentifier): IToolbarActions {
B
Benjamin Pasero 已提交
295 296 297
		const primary: IAction[] = [];
		const secondary: IAction[] = [];

298 299 300
		const {group} = identifier;
		const position = this.stacks.positionOfGroup(group);

301 302 303
		// Update the resource context
		this.resourceContext.set(group && getResource(group.activeEditor));

304 305
		// Editor actions require the editor control to be there, so we retrieve it via service
		const control = this.editorService.getVisibleEditors()[position];
306
		if (control instanceof BaseEditor && control.input && typeof control.position === 'number') {
307 308 309

			// Editor Control Actions
			let editorActions = this.mapActionsToEditors[control.getId()];
B
Benjamin Pasero 已提交
310
			if (!editorActions) {
311 312
				editorActions = this.getEditorActionsForContext(control);
				this.mapActionsToEditors[control.getId()] = editorActions;
B
Benjamin Pasero 已提交
313 314 315 316
			}
			primary.push(...editorActions.primary);
			secondary.push(...editorActions.secondary);

317 318
			// Editor Input Actions
			const editorInputActions = this.getEditorActionsForContext({ input: control.input, editor: control, position: control.position });
B
Benjamin Pasero 已提交
319 320
			primary.push(...editorInputActions.primary);
			secondary.push(...editorInputActions.secondary);
321 322

			// MenuItems
323 324 325 326 327 328 329 330 331 332 333
			// TODO This isn't very proper but needed as we have failed to
			// use the correct context key service per editor only once. Don't
			// take this code as sample of how to work with menus
			this.disposeOnEditorActions = dispose(this.disposeOnEditorActions);
			const widget = control.getControl();
			const codeEditor = isCommonCodeEditor(widget) && widget || isCommonDiffEditor(widget) && widget.getModifiedEditor();
			const scopedContextKeyService = codeEditor && codeEditor.invokeWithinContext(accessor => accessor.get(IContextKeyService)) || this.contextKeyService;
			const titleBarMenu = this.menuService.createMenu(MenuId.EditorTitle, scopedContextKeyService);
			this.disposeOnEditorActions.push(titleBarMenu, titleBarMenu.onDidChange(_ => this.update()));

			fillInActions(titleBarMenu, { primary, secondary });
B
Benjamin Pasero 已提交
334 335 336 337 338
		}

		return { primary, secondary };
	}

J
Johannes Rieken 已提交
339
	private getEditorActionsForContext(context: BaseEditor | IEditorInputActionContext): IToolbarActions {
340 341
		const primaryActions: IAction[] = [];
		const secondaryActions: IAction[] = [];
B
Benjamin Pasero 已提交
342 343 344 345 346 347 348 349

		// From Editor
		if (context instanceof BaseEditor) {
			primaryActions.push(...(<BaseEditor>context).getActions());
			secondaryActions.push(...(<BaseEditor>context).getSecondaryActions());
		}

		// From Contributions
350 351 352 353 354
		else {
			const actionBarRegistry = Registry.as<IActionBarRegistry>(Extensions.Actionbar);
			primaryActions.push(...actionBarRegistry.getActionBarActionsForContext(Scope.EDITOR, context));
			secondaryActions.push(...actionBarRegistry.getSecondaryActionBarActionsForContext(Scope.EDITOR, context));
		}
B
Benjamin Pasero 已提交
355 356 357 358 359 360 361

		return {
			primary: primaryActions,
			secondary: secondaryActions
		};
	}

362 363 364 365 366
	protected updateEditorActionsToolbar(): void {
		const group = this.context;
		if (!group) {
			return;
		}
B
Benjamin Pasero 已提交
367

368 369 370 371 372 373 374 375 376 377
		const editor = group && group.activeEditor;
		const isActive = this.stacks.isActive(group);

		// Update Editor Actions Toolbar
		let primaryEditorActions: IAction[] = [];
		let secondaryEditorActions: IAction[] = [];
		if (isActive) {
			const editorActions = this.getEditorActions({ group, editor });
			primaryEditorActions = prepareActions(editorActions.primary);
			if (isActive && editor instanceof EditorInput && editor.supportsSplitEditor()) {
378
				this.updateSplitActionEnablement();
379 380 381 382
				primaryEditorActions.push(this.splitEditorAction);
			}
			secondaryEditorActions = prepareActions(editorActions.secondary);
		}
383

384
		if (this.showTabs) {
385 386
			if (secondaryEditorActions.length > 0) {
				secondaryEditorActions.push(new Separator());
387
			}
388
			secondaryEditorActions.push(this.showEditorsInGroupAction);
389 390
			secondaryEditorActions.push(new Separator());
			secondaryEditorActions.push(this.closeEditorsInGroupAction);
B
Benjamin Pasero 已提交
391 392
		}

393 394 395
		const primaryEditorActionIds = primaryEditorActions.map(a => a.id);
		if (!this.showTabs) {
			primaryEditorActionIds.push(this.closeEditorAction.id); // always show "Close" when tabs are disabled
B
Benjamin Pasero 已提交
396 397
		}

398
		const secondaryEditorActionIds = secondaryEditorActions.map(a => a.id);
B
Benjamin Pasero 已提交
399

400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
		if (!arrays.equals(primaryEditorActionIds, this.currentPrimaryEditorActionIds) || !arrays.equals(secondaryEditorActionIds, this.currentSecondaryEditorActionIds)) {
			this.editorActionsToolbar.setActions(primaryEditorActions, secondaryEditorActions)();

			if (!this.showTabs) {
				this.editorActionsToolbar.addPrimaryAction(this.closeEditorAction)();
			}

			this.currentPrimaryEditorActionIds = primaryEditorActionIds;
			this.currentSecondaryEditorActionIds = secondaryEditorActionIds;
		}
	}

	protected clearEditorActionsToolbar(): void {
		this.editorActionsToolbar.setActions([], [])();

		this.currentPrimaryEditorActionIds = [];
		this.currentSecondaryEditorActionIds = [];
B
Benjamin Pasero 已提交
417 418
	}

419
	protected onContextMenu(identifier: IEditorIdentifier, e: Event, node: HTMLElement): void {
420 421 422 423 424 425

		// Update the resource context
		const currentContext = this.resourceContext.get();
		this.resourceContext.set(identifier.editor && getResource(identifier.editor));

		// Find target anchor
426 427 428 429 430 431 432 433 434 435 436
		let anchor: HTMLElement | { x: number, y: number } = node;
		if (e instanceof MouseEvent) {
			const event = new StandardMouseEvent(e);
			anchor = { x: event.posx, y: event.posy };
		}

		this.contextMenuService.showContextMenu({
			getAnchor: () => anchor,
			getActions: () => TPromise.as(this.getContextMenuActions(identifier)),
			getActionsContext: () => identifier,
			getKeyBinding: (action) => {
437
				const opts = this.keybindingService.lookupKeybindings(action.id);
438 439 440 441 442
				if (opts.length > 0) {
					return opts[0]; // only take the first one
				}

				return null;
443 444 445
			},
			onHide: (cancel) => {
				this.resourceContext.set(currentContext); // restore previous context
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473
			}
		});
	}

	protected getContextMenuActions(identifier: IEditorIdentifier): IAction[] {
		const {editor, group} = identifier;

		// Enablement
		this.closeOtherEditorsAction.enabled = group.count > 1;
		this.pinEditorAction.enabled = !group.isPinned(editor);
		this.closeRightEditorsAction.enabled = group.indexOf(editor) !== group.count - 1;

		// Actions: For all editors
		const actions: IAction[] = [
			this.closeEditorAction,
			this.closeOtherEditorsAction
		];

		if (this.showTabs) {
			actions.push(this.closeRightEditorsAction);
		}

		actions.push(this.closeEditorsInGroupAction);

		if (this.previewEditors) {
			actions.push(new Separator(), this.pinEditorAction);
		}

474 475 476 477
		const titleBarMenu = this.menuService.createMenu(MenuId.EditorTab, this.contextKeyService);
		fillInActions(titleBarMenu, actions);
		titleBarMenu.dispose(); // not needed anymore

478 479 480
		return actions;
	}

B
Benjamin Pasero 已提交
481 482 483 484 485 486
	public dispose(): void {
		dispose(this.toDispose);

		// Actions
		[
			this.splitEditorAction,
B
Benjamin Pasero 已提交
487
			this.showEditorsInGroupAction,
B
Benjamin Pasero 已提交
488
			this.closeEditorAction,
B
Benjamin Pasero 已提交
489
			this.closeRightEditorsAction,
B
Benjamin Pasero 已提交
490
			this.closeOtherEditorsAction,
B
Benjamin Pasero 已提交
491 492
			this.closeEditorsInGroupAction,
			this.pinEditorAction
B
Benjamin Pasero 已提交
493 494 495
		].forEach((action) => {
			action.dispose();
		});
496 497 498

		// Toolbar
		this.editorActionsToolbar.dispose();
B
Benjamin Pasero 已提交
499 500
	}
}