contextmenu.ts 9.2 KB
Newer Older
E
Erich Gamma 已提交
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.
 *--------------------------------------------------------------------------------------------*/

A
Alex Dima 已提交
6
import * as nls from 'vs/nls';
A
Alex Dima 已提交
7
import * as dom from 'vs/base/browser/dom';
J
Johannes Rieken 已提交
8
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
J
João Moreno 已提交
9
import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
A
Alex Dima 已提交
10
import { IAnchor } from 'vs/base/browser/ui/contextview/contextview';
J
João Moreno 已提交
11
import { IAction, Separator } from 'vs/base/common/actions';
A
Alex Dima 已提交
12
import { KeyCode, KeyMod, ResolvedKeybinding } from 'vs/base/common/keyCodes';
13
import { DisposableStore } from 'vs/base/common/lifecycle';
A
Alex Dima 已提交
14 15
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
16
import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon';
17
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
J
Johannes Rieken 已提交
18
import { IMenuService, MenuId, SubmenuItemAction } from 'vs/platform/actions/common/actions';
A
Alex Dima 已提交
19 20 21
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
22
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
A
Alex Dima 已提交
23
import { ITextModel } from 'vs/editor/common/model';
24
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
A
Alex Dima 已提交
25
import { EditorOption } from 'vs/editor/common/config/editorOptions';
J
Johannes Rieken 已提交
26
import { ContextSubMenu } from 'vs/base/browser/contextmenu';
E
Erich Gamma 已提交
27

28
export class ContextMenuController implements IEditorContribution {
E
Erich Gamma 已提交
29

30
	public static readonly ID = 'editor.contrib.contextmenu';
31

32
	public static get(editor: ICodeEditor): ContextMenuController {
A
Alex Dima 已提交
33
		return editor.getContribution<ContextMenuController>(ContextMenuController.ID);
34
	}
E
Erich Gamma 已提交
35

36
	private readonly _toDispose = new DisposableStore();
37
	private _contextMenuIsBeingShownCount: number = 0;
38
	private readonly _editor: ICodeEditor;
39 40 41

	constructor(
		editor: ICodeEditor,
42 43 44 45 46
		@IContextMenuService private readonly _contextMenuService: IContextMenuService,
		@IContextViewService private readonly _contextViewService: IContextViewService,
		@IContextKeyService private readonly _contextKeyService: IContextKeyService,
		@IKeybindingService private readonly _keybindingService: IKeybindingService,
		@IMenuService private readonly _menuService: IMenuService
47
	) {
E
Erich Gamma 已提交
48 49
		this._editor = editor;

50 51
		this._toDispose.add(this._editor.onContextMenu((e: IEditorMouseEvent) => this._onContextMenu(e)));
		this._toDispose.add(this._editor.onMouseWheel((e: IMouseWheelEvent) => {
52
			if (this._contextMenuIsBeingShownCount > 0) {
53 54 55
				this._contextViewService.hideContextView();
			}
		}));
56
		this._toDispose.add(this._editor.onKeyDown((e: IKeyboardEvent) => {
E
Erich Gamma 已提交
57 58 59 60 61 62 63 64 65
			if (e.keyCode === KeyCode.ContextMenu) {
				// Chrome is funny like that
				e.preventDefault();
				e.stopPropagation();
				this.showContextMenu();
			}
		}));
	}

66
	private _onContextMenu(e: IEditorMouseEvent): void {
A
Alex Dima 已提交
67 68 69 70
		if (!this._editor.hasModel()) {
			return;
		}

A
Alex Dima 已提交
71
		if (!this._editor.getOption(EditorOption.contextmenu)) {
E
Erich Gamma 已提交
72 73 74 75 76 77 78 79
			this._editor.focus();
			// Ensure the cursor is at the position of the mouse click
			if (e.target.position && !this._editor.getSelection().containsPosition(e.target.position)) {
				this._editor.setPosition(e.target.position);
			}
			return; // Context menu is turned off through configuration
		}

A
Alex Dima 已提交
80
		if (e.target.type === MouseTargetType.OVERLAY_WIDGET) {
E
Erich Gamma 已提交
81 82 83 84 85
			return; // allow native menu on widgets to support right click on input field for example in find
		}

		e.event.preventDefault();

A
Alex Dima 已提交
86
		if (e.target.type !== MouseTargetType.CONTENT_TEXT && e.target.type !== MouseTargetType.CONTENT_EMPTY && e.target.type !== MouseTargetType.TEXTAREA) {
E
Erich Gamma 已提交
87 88 89 90 91 92 93
			return; // only support mouse click into text or native context menu key for now
		}

		// Ensure the editor gets focus if it hasn't, so the right events are being sent to other contributions
		this._editor.focus();

		// Ensure the cursor is at the position of the mouse click
A
Alex Dima 已提交
94 95 96 97 98 99 100 101 102 103 104 105
		if (e.target.position) {
			let hasSelectionAtPosition = false;
			for (const selection of this._editor.getSelections()) {
				if (selection.containsPosition(e.target.position)) {
					hasSelectionAtPosition = true;
					break;
				}
			}

			if (!hasSelectionAtPosition) {
				this._editor.setPosition(e.target.position);
			}
E
Erich Gamma 已提交
106 107 108
		}

		// Unless the user triggerd the context menu through Shift+F10, use the mouse position as menu position
A
Alex Dima 已提交
109
		let anchor: IAnchor | null = null;
A
Alex Dima 已提交
110
		if (e.target.type !== MouseTargetType.TEXTAREA) {
111
			anchor = { x: e.event.posx - 1, width: 2, y: e.event.posy - 1, height: 2 };
E
Erich Gamma 已提交
112 113 114
		}

		// Show the context menu
115
		this.showContextMenu(anchor);
E
Erich Gamma 已提交
116 117
	}

A
Alex Dima 已提交
118
	public showContextMenu(anchor?: IAnchor | null): void {
A
Alex Dima 已提交
119
		if (!this._editor.getOption(EditorOption.contextmenu)) {
E
Erich Gamma 已提交
120 121
			return; // Context menu is turned off through configuration
		}
A
Alex Dima 已提交
122 123 124
		if (!this._editor.hasModel()) {
			return;
		}
E
Erich Gamma 已提交
125

126
		if (!this._contextMenuService) {
E
Erich Gamma 已提交
127 128 129 130 131
			this._editor.focus();
			return;	// We need the context menu service to function
		}

		// Find actions available for menu
J
Johannes Rieken 已提交
132
		const menuActions = this._getMenuActions(this._editor.getModel(), MenuId.EditorContext);
E
Erich Gamma 已提交
133 134 135

		// Show menu if we have actions to show
		if (menuActions.length > 0) {
136
			this._doShowContextMenu(menuActions, anchor);
E
Erich Gamma 已提交
137 138 139
		}
	}

J
Johannes Rieken 已提交
140
	private _getMenuActions(model: ITextModel, menuId: MenuId): IAction[] {
141
		const result: IAction[] = [];
J
Johannes Rieken 已提交
142

J
Johannes Rieken 已提交
143 144 145 146
		// get menu groups
		const menu = this._menuService.createMenu(menuId, this._contextKeyService);
		const groups = menu.getActions({ arg: model.uri });
		menu.dispose();
J
Johannes Rieken 已提交
147

J
Johannes Rieken 已提交
148
		// translate them into other actions
J
Johannes Rieken 已提交
149 150
		for (let group of groups) {
			const [, actions] = group;
S
SteVen Batten 已提交
151
			let addedItems = 0;
J
Johannes Rieken 已提交
152 153 154 155 156
			for (const action of actions) {
				if (action instanceof SubmenuItemAction) {
					const subActions = this._getMenuActions(model, action.item.submenu);
					if (subActions.length > 0) {
						result.push(new ContextSubMenu(action.label, subActions));
S
SteVen Batten 已提交
157
						addedItems++;
J
Johannes Rieken 已提交
158 159 160
					}
				} else {
					result.push(action);
S
SteVen Batten 已提交
161
					addedItems++;
J
Johannes Rieken 已提交
162 163
				}
			}
S
SteVen Batten 已提交
164 165 166 167

			if (addedItems) {
				result.push(new Separator());
			}
J
Johannes Rieken 已提交
168
		}
S
SteVen Batten 已提交
169 170 171 172 173

		if (result.length) {
			result.pop(); // remove last separator
		}

174
		return result;
E
Erich Gamma 已提交
175 176
	}

177
	private _doShowContextMenu(actions: ReadonlyArray<IAction>, anchor: IAnchor | null = null): void {
A
Alex Dima 已提交
178 179 180
		if (!this._editor.hasModel()) {
			return;
		}
E
Erich Gamma 已提交
181 182

		// Disable hover
A
Alex Dima 已提交
183
		const oldHoverSetting = this._editor.getOption(EditorOption.hover);
E
Erich Gamma 已提交
184
		this._editor.updateOptions({
A
Alex Dima 已提交
185 186 187
			hover: {
				enabled: false
			}
E
Erich Gamma 已提交
188 189
		});

J
Joao Moreno 已提交
190
		if (!anchor) {
191
			// Ensure selection is visible
192
			this._editor.revealPosition(this._editor.getPosition(), ScrollType.Immediate);
E
Erich Gamma 已提交
193

194
			this._editor.render();
A
Alex Dima 已提交
195
			const cursorCoords = this._editor.getScrolledVisiblePosition(this._editor.getPosition());
E
Erich Gamma 已提交
196 197

			// Translate to absolute editor position
A
Alex Dima 已提交
198 199 200
			const editorCoords = dom.getDomNodePagePosition(this._editor.getDomNode());
			const posx = editorCoords.left + cursorCoords.left;
			const posy = editorCoords.top + cursorCoords.top + cursorCoords.height;
E
Erich Gamma 已提交
201

J
Joao Moreno 已提交
202
			anchor = { x: posx, y: posy };
E
Erich Gamma 已提交
203 204 205
		}

		// Show menu
206
		this._contextMenuIsBeingShownCount++;
207
		this._contextMenuService.showContextMenu({
A
Alex Dima 已提交
208
			getAnchor: () => anchor!,
E
Erich Gamma 已提交
209

210
			getActions: () => actions,
E
Erich Gamma 已提交
211

212
			getActionViewItem: (action) => {
A
Alex Dima 已提交
213
				const keybinding = this._keybindingFor(action);
E
Erich Gamma 已提交
214
				if (keybinding) {
215
					return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel(), isMenu: true });
E
Erich Gamma 已提交
216 217
				}

218 219 220
				const customActionViewItem = <any>action;
				if (typeof customActionViewItem.getActionViewItem === 'function') {
					return customActionViewItem.getActionViewItem();
E
Erich Gamma 已提交
221 222
				}

223
				return new ActionViewItem(action, action, { icon: true, label: true, isMenu: true });
E
Erich Gamma 已提交
224 225
			},

M
Matt Bierner 已提交
226
			getKeyBinding: (action): ResolvedKeybinding | undefined => {
M
Matt Bierner 已提交
227
				return this._keybindingFor(action);
E
Erich Gamma 已提交
228 229
			},

230
			onHide: (wasCancelled: boolean) => {
E
Erich Gamma 已提交
231 232 233 234 235 236 237 238 239
				this._contextMenuIsBeingShownCount--;
				this._editor.focus();
				this._editor.updateOptions({
					hover: oldHoverSetting
				});
			}
		});
	}

M
Matt Bierner 已提交
240
	private _keybindingFor(action: IAction): ResolvedKeybinding | undefined {
241
		return this._keybindingService.lookupKeybinding(action.id);
E
Erich Gamma 已提交
242 243 244 245
	}

	public dispose(): void {
		if (this._contextMenuIsBeingShownCount > 0) {
246
			this._contextViewService.hideContextView();
E
Erich Gamma 已提交
247 248
		}

249
		this._toDispose.dispose();
E
Erich Gamma 已提交
250 251 252
	}
}

A
Alex Dima 已提交
253
class ShowContextMenu extends EditorAction {
E
Erich Gamma 已提交
254

A
Alex Dima 已提交
255
	constructor() {
256 257 258 259
		super({
			id: 'editor.action.showContextMenu',
			label: nls.localize('action.showContextMenu.label', "Show Editor Context Menu"),
			alias: 'Show Editor Context Menu',
260
			precondition: undefined,
261
			kbOpts: {
262
				kbExpr: EditorContextKeys.textInputFocus,
A
Alex Dima 已提交
263
				primary: KeyMod.Shift | KeyCode.F10,
264
				weight: KeybindingWeight.EditorContrib
265 266
			}
		});
E
Erich Gamma 已提交
267 268
	}

269
	public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
270
		let contribution = ContextMenuController.get(editor);
E
Erich Gamma 已提交
271 272 273
		contribution.showContextMenu();
	}
}
274

275
registerEditorContribution(ContextMenuController.ID, ContextMenuController);
276
registerEditorAction(ShowContextMenu);