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

6 7 8 9 10 11 12 13 14
import { applyDragImage } from 'vs/base/browser/dnd';
import { addDisposableListener, Dimension, EventType } from 'vs/base/browser/dom';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { ActionsOrientation, IActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { Action, IAction, IRunEvent } from 'vs/base/common/actions';
import * as arrays from 'vs/base/common/arrays';
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
15
import 'vs/css!./media/titlecontrol';
16
import { getCodeEditor } from 'vs/editor/browser/editorBrowser';
17
import { localize } from 'vs/nls';
18 19 20 21
import { createActionItem, fillInActionBarActions, fillInContextMenuActions } from 'vs/platform/actions/browser/menuItemActionItem';
import { ExecuteCommandAction, IMenu, IMenuService, MenuId } from 'vs/platform/actions/common/actions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
22 23 24
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
25 26 27 28 29 30 31 32 33 34 35 36
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry';
import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { prepareActions } from 'vs/workbench/browser/actions';
import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillResourceDataTransfers, LocalSelectionTransfer } from 'vs/workbench/browser/dnd';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs';
import { BreadcrumbsControl, IBreadcrumbsControlOptions } from 'vs/workbench/browser/parts/editor/breadcrumbsControl';
import { EDITOR_TITLE_HEIGHT, IEditorGroupsAccessor, IEditorGroupView, IEditorPartOptions } from 'vs/workbench/browser/parts/editor/editor';
import { EditorCommandsContextActionRunner, IEditorCommandsContext, IEditorInput, toResource } from 'vs/workbench/common/editor';
37
import { ResourceContextKey } from 'vs/workbench/common/resources';
B
Benjamin Pasero 已提交
38
import { Themable } from 'vs/workbench/common/theme';
39
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
40
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
J
Johannes Rieken 已提交
41
import { IFileService } from 'vs/platform/files/common/files';
B
Benjamin Pasero 已提交
42 43 44 45 46 47

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

48
export abstract class TitleControl extends Themable {
49

B
Benjamin Pasero 已提交
50 51 52
	protected readonly groupTransfer = LocalSelectionTransfer.getInstance<DraggedEditorGroupIdentifier>();
	protected readonly editorTransfer = LocalSelectionTransfer.getInstance<DraggedEditorIdentifier>();

M
Matt Bierner 已提交
53
	protected breadcrumbsControl?: BreadcrumbsControl;
54

55 56 57 58
	private currentPrimaryEditorActionIds: string[] = [];
	private currentSecondaryEditorActionIds: string[] = [];
	protected editorActionsToolbar: ToolBar;

59
	private resourceContext: ResourceContextKey;
B
Benjamin Pasero 已提交
60
	private editorToolBarMenuDisposables: IDisposable[] = [];
61

B
Benjamin Pasero 已提交
62 63
	private contextMenu: IMenu;

B
Benjamin Pasero 已提交
64
	constructor(
65
		parent: HTMLElement,
66
		protected accessor: IEditorGroupsAccessor,
B
Benjamin Pasero 已提交
67
		protected group: IEditorGroupView,
68
		@IContextMenuService private readonly contextMenuService: IContextMenuService,
B
Benjamin Pasero 已提交
69
		@IInstantiationService protected instantiationService: IInstantiationService,
70 71 72 73 74
		@IContextKeyService private readonly contextKeyService: IContextKeyService,
		@IKeybindingService private readonly keybindingService: IKeybindingService,
		@ITelemetryService private readonly telemetryService: ITelemetryService,
		@INotificationService private readonly notificationService: INotificationService,
		@IMenuService private readonly menuService: IMenuService,
75
		@IQuickOpenService protected quickOpenService: IQuickOpenService,
76
		@IThemeService themeService: IThemeService,
77
		@IExtensionService private readonly extensionService: IExtensionService,
J
Johannes Rieken 已提交
78 79
		@IConfigurationService protected configurationService: IConfigurationService,
		@IFileService private readonly fileService: IFileService,
B
Benjamin Pasero 已提交
80
	) {
B
Benjamin Pasero 已提交
81
		super(themeService);
82

83
		this.resourceContext = this._register(instantiationService.createInstance(ResourceContextKey));
84
		this.contextMenu = this._register(this.menuService.createMenu(MenuId.EditorTitleContext, this.contextKeyService));
B
Benjamin Pasero 已提交
85

86
		this.create(parent);
87
		this.registerListeners();
88 89
	}

90
	private registerListeners(): void {
91
		this._register(this.extensionService.onDidRegisterExtensions(() => this.updateEditorActionsToolbar()));
B
Benjamin Pasero 已提交
92 93
	}

94
	protected abstract create(parent: HTMLElement): void;
B
Benjamin Pasero 已提交
95

96
	protected createBreadcrumbsControl(container: HTMLElement, options: IBreadcrumbsControlOptions): void {
97
		const config = this._register(BreadcrumbsConfig.IsEnabled.bindTo(this.configurationService));
98 99
		this._register(config.onDidChange(() => {
			const value = config.getValue();
100 101 102
			if (!value && this.breadcrumbsControl) {
				this.breadcrumbsControl.dispose();
				this.breadcrumbsControl = undefined;
J
Johannes Rieken 已提交
103
				this.handleBreadcrumbsEnablementChange();
104
			} else if (value && !this.breadcrumbsControl) {
105
				this.breadcrumbsControl = this.instantiationService.createInstance(BreadcrumbsControl, container, options, this.group);
106
				this.breadcrumbsControl.update();
J
Johannes Rieken 已提交
107
				this.handleBreadcrumbsEnablementChange();
108
			}
109
		}));
J
Johannes Rieken 已提交
110
		if (config.getValue()) {
111
			this.breadcrumbsControl = this.instantiationService.createInstance(BreadcrumbsControl, container, options, this.group);
112
		}
J
Johannes Rieken 已提交
113 114 115 116 117 118

		this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(() => {
			if (this.breadcrumbsControl && this.breadcrumbsControl.update()) {
				this.handleBreadcrumbsEnablementChange();
			}
		}));
119 120
	}

J
Johannes Rieken 已提交
121 122
	protected abstract handleBreadcrumbsEnablementChange(): void;

123
	protected createEditorActionsToolBar(container: HTMLElement): void {
B
Benjamin Pasero 已提交
124 125
		const context = { groupId: this.group.id } as IEditorCommandsContext;

126
		this.editorActionsToolbar = this._register(new ToolBar(container, this.contextMenuService, {
127
			actionItemProvider: action => this.actionItemProvider(action as Action),
B
Benjamin Pasero 已提交
128
			orientation: ActionsOrientation.HORIZONTAL,
129
			ariaLabel: localize('araLabelEditorActions', "Editor actions"),
B
Benjamin Pasero 已提交
130
			getKeyBinding: action => this.getKeybinding(action),
131 132
			actionRunner: this._register(new EditorCommandsContextActionRunner(context)),
			anchorAlignmentProvider: () => AnchorAlignment.RIGHT
133
		}));
B
Benjamin Pasero 已提交
134

135
		// Context
B
Benjamin Pasero 已提交
136
		this.editorActionsToolbar.context = context;
137

B
Benjamin Pasero 已提交
138
		// Action Run Handling
139
		this._register(this.editorActionsToolbar.actionRunner.onDidRun((e: IRunEvent) => {
B
Benjamin Pasero 已提交
140

141 142
			// Notify for Error
			this.notificationService.error(e.error);
B
Benjamin Pasero 已提交
143 144 145

			// Log in telemetry
			if (this.telemetryService) {
K
kieferrm 已提交
146
				/* __GDPR__
K
kieferrm 已提交
147 148 149 150 151
					"workbenchActionExecuted" : {
						"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
						"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
B
Benjamin Pasero 已提交
152 153 154 155 156
				this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: 'editorPart' });
			}
		}));
	}

M
Matt Bierner 已提交
157
	private actionItemProvider(action: Action): IActionItem | null {
158
		const activeControl = this.group.activeControl;
B
Benjamin Pasero 已提交
159 160

		// Check Active Editor
M
Matt Bierner 已提交
161
		let actionItem: IActionItem | null = null;
162 163
		if (activeControl instanceof BaseEditor) {
			actionItem = activeControl.getActionItem(action);
B
Benjamin Pasero 已提交
164 165
		}

166 167
		// Check extensions
		if (!actionItem) {
M
Matt Bierner 已提交
168
			actionItem = createActionItem(action, this.keybindingService, this.notificationService, this.contextMenuService) || null;
169 170
		}

B
Benjamin Pasero 已提交
171 172 173
		return actionItem;
	}

B
Benjamin Pasero 已提交
174 175 176
	protected updateEditorActionsToolbar(): void {

		// Update Editor Actions Toolbar
B
Benjamin Pasero 已提交
177
		const { primaryEditorActions, secondaryEditorActions } = this.prepareEditorActions(this.getEditorActions());
B
Benjamin Pasero 已提交
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194

		// Only update if something actually has changed
		const primaryEditorActionIds = primaryEditorActions.map(a => a.id);
		const secondaryEditorActionIds = secondaryEditorActions.map(a => a.id);
		if (
			!arrays.equals(primaryEditorActionIds, this.currentPrimaryEditorActionIds) ||
			!arrays.equals(secondaryEditorActionIds, this.currentSecondaryEditorActionIds) ||
			primaryEditorActions.some(action => action instanceof ExecuteCommandAction) || // execute command actions can have the same ID but different arguments
			secondaryEditorActions.some(action => action instanceof ExecuteCommandAction)  // see also https://github.com/Microsoft/vscode/issues/16298
		) {
			this.editorActionsToolbar.setActions(primaryEditorActions, secondaryEditorActions)();

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

B
Benjamin Pasero 已提交
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
	protected prepareEditorActions(editorActions: IToolbarActions): { primaryEditorActions: IAction[]; secondaryEditorActions: IAction[]; } {
		let primaryEditorActions: IAction[];
		let secondaryEditorActions: IAction[];

		// Primary actions only for the active group
		if (this.accessor.activeGroup === this.group) {
			primaryEditorActions = prepareActions(editorActions.primary);
		} else {
			primaryEditorActions = [];
		}

		// Secondary actions for all groups
		secondaryEditorActions = prepareActions(editorActions.secondary);

		return { primaryEditorActions, secondaryEditorActions };
	}

212
	private getEditorActions(): IToolbarActions {
B
Benjamin Pasero 已提交
213 214 215
		const primary: IAction[] = [];
		const secondary: IAction[] = [];

B
Benjamin Pasero 已提交
216 217 218
		// Dispose previous listeners
		this.editorToolBarMenuDisposables = dispose(this.editorToolBarMenuDisposables);

219
		// Update the resource context
M
Matt Bierner 已提交
220
		this.resourceContext.set(this.group.activeEditor ? toResource(this.group.activeEditor, { supportSideBySide: true }) : null);
221

222
		// Editor actions require the editor control to be there, so we retrieve it via service
223
		const activeControl = this.group.activeControl;
224
		if (activeControl instanceof BaseEditor) {
B
Benjamin Pasero 已提交
225
			const codeEditor = getCodeEditor(activeControl.getControl());
226 227
			const scopedContextKeyService = codeEditor && codeEditor.invokeWithinContext(accessor => accessor.get(IContextKeyService)) || this.contextKeyService;
			const titleBarMenu = this.menuService.createMenu(MenuId.EditorTitle, scopedContextKeyService);
B
Benjamin Pasero 已提交
228 229
			this.editorToolBarMenuDisposables.push(titleBarMenu);
			this.editorToolBarMenuDisposables.push(titleBarMenu.onDidChange(() => {
B
Benjamin Pasero 已提交
230
				this.updateEditorActionsToolbar(); // Update editor toolbar whenever contributed actions change
B
Benjamin Pasero 已提交
231
			}));
232

233
			fillInActionBarActions(titleBarMenu, { arg: this.resourceContext.get(), shouldForwardArgs: true }, { primary, secondary });
B
Benjamin Pasero 已提交
234 235 236 237 238
		}

		return { primary, secondary };
	}

239 240 241 242 243
	protected clearEditorActionsToolbar(): void {
		this.editorActionsToolbar.setActions([], [])();

		this.currentPrimaryEditorActionIds = [];
		this.currentSecondaryEditorActionIds = [];
B
Benjamin Pasero 已提交
244 245
	}

B
Benjamin Pasero 已提交
246
	protected enableGroupDragging(element: HTMLElement): void {
B
Benjamin Pasero 已提交
247 248 249 250 251 252 253 254 255

		// Drag start
		this._register(addDisposableListener(element, EventType.DRAG_START, (e: DragEvent) => {
			if (e.target !== element) {
				return; // only if originating from tabs container
			}

			// Set editor group as transfer
			this.groupTransfer.setData([new DraggedEditorGroupIdentifier(this.group.id)], DraggedEditorGroupIdentifier.prototype);
M
Matt Bierner 已提交
256
			e.dataTransfer!.effectAllowed = 'copyMove';
B
Benjamin Pasero 已提交
257

B
Benjamin Pasero 已提交
258 259
			// If tabs are disabled, treat dragging as if an editor tab was dragged
			if (!this.accessor.partOptions.showTabs) {
M
Matt Bierner 已提交
260
				const resource = this.group.activeEditor ? toResource(this.group.activeEditor, { supportSideBySide: true }) : null;
261 262 263
				if (resource) {
					this.instantiationService.invokeFunction(fillResourceDataTransfers, [resource], e);
				}
B
Benjamin Pasero 已提交
264 265 266
			}

			// Drag Image
B
Benjamin Pasero 已提交
267 268 269 270 271 272
			let label = this.group.activeEditor.getName();
			if (this.accessor.partOptions.showTabs && this.group.count > 1) {
				label = localize('draggedEditorGroup', "{0} (+{1})", label, this.group.count - 1);
			}

			applyDragImage(e, label, 'monaco-editor-group-drag-image');
B
Benjamin Pasero 已提交
273 274 275 276 277 278 279 280
		}));

		// Drag end
		this._register(addDisposableListener(element, EventType.DRAG_END, () => {
			this.groupTransfer.clearData(DraggedEditorGroupIdentifier.prototype);
		}));
	}

281
	protected onContextMenu(editor: IEditorInput, e: Event, node: HTMLElement): void {
282 283 284

		// Update the resource context
		const currentContext = this.resourceContext.get();
285
		this.resourceContext.set(toResource(editor, { supportSideBySide: true }));
286 287

		// Find target anchor
288 289 290 291 292 293
		let anchor: HTMLElement | { x: number, y: number } = node;
		if (e instanceof MouseEvent) {
			const event = new StandardMouseEvent(e);
			anchor = { x: event.posx, y: event.posy };
		}

294 295
		// Fill in contributed actions
		const actions: IAction[] = [];
296
		fillInContextMenuActions(this.contextMenu, { shouldForwardArgs: true, arg: this.resourceContext.get() }, actions, this.contextMenuService);
297 298

		// Show it
299 300
		this.contextMenuService.showContextMenu({
			getAnchor: () => anchor,
301
			getActions: () => actions,
302
			getActionsContext: () => ({ groupId: this.group.id, editorIndex: this.group.getIndexOfEditor(editor) } as IEditorCommandsContext),
B
Benjamin Pasero 已提交
303
			getKeyBinding: (action) => this.getKeybinding(action),
B
Benjamin Pasero 已提交
304
			onHide: () => {
B
Benjamin Pasero 已提交
305 306 307 308

				// restore previous context
				this.resourceContext.set(currentContext);

309
				// restore focus to active group
310
				this.accessor.activeGroup.focus();
B
Benjamin Pasero 已提交
311
			}
312 313 314
		});
	}

M
Matt Bierner 已提交
315
	private getKeybinding(action: IAction): ResolvedKeybinding | undefined {
316
		return this.keybindingService.lookupKeybinding(action.id);
B
Benjamin Pasero 已提交
317 318
	}

M
Matt Bierner 已提交
319
	protected getKeybindingLabel(action: IAction): string | undefined {
B
Benjamin Pasero 已提交
320 321
		const keybinding = this.getKeybinding(action);

M
Matt Bierner 已提交
322
		return keybinding ? keybinding.getLabel() || undefined : undefined;
B
Benjamin Pasero 已提交
323 324
	}

325
	abstract openEditor(editor: IEditorInput): void;
326

327 328 329 330 331
	abstract closeEditor(editor: IEditorInput): void;

	abstract closeEditors(editors: IEditorInput[]): void;

	abstract closeAllEditors(): void;
332

333
	abstract moveEditor(editor: IEditorInput, fromIndex: number, targetIndex: number): void;
334

335
	abstract pinEditor(editor: IEditorInput): void;
336

337
	abstract setActive(isActive: boolean): void;
338

339 340 341
	abstract updateEditorLabel(editor: IEditorInput): void;

	abstract updateEditorDirty(editor: IEditorInput): void;
342

343
	abstract updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void;
344 345 346

	abstract updateStyles(): void;

347
	layout(dimension: Dimension): void {
348 349 350
		if (this.breadcrumbsControl) {
			this.breadcrumbsControl.layout(undefined);
		}
351 352
	}

353
	getPreferredHeight(): number {
354
		return EDITOR_TITLE_HEIGHT + (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden() ? BreadcrumbsControl.HEIGHT : 0);
355 356
	}

357
	dispose(): void {
358
		this.breadcrumbsControl = dispose(this.breadcrumbsControl);
B
Benjamin Pasero 已提交
359
		this.editorToolBarMenuDisposables = dispose(this.editorToolBarMenuDisposables);
360 361 362

		super.dispose();
	}
J
Johannes Rieken 已提交
363
}
364 365 366 367

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

	// Drag Feedback
B
Benjamin Pasero 已提交
368 369 370 371 372 373 374 375
	const dragImageBackground = theme.getColor(listActiveSelectionBackground);
	const dragImageForeground = theme.getColor(listActiveSelectionForeground);
	collector.addRule(`
		.monaco-editor-group-drag-image {
			background: ${dragImageBackground};
			color: ${dragImageForeground};
		}
	`);
376
});