menuItemActionItem.ts 7.6 KB
Newer Older
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';

J
Johannes Rieken 已提交
8 9
import { localize } from 'vs/nls';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
B
Benjamin Pasero 已提交
10
import { IMenu, MenuItemAction, IMenuActionOptions, ICommandAction } from 'vs/platform/actions/common/actions';
J
Johannes Rieken 已提交
11
import { IMessageService } from 'vs/platform/message/common/message';
12
import Severity from 'vs/base/common/severity';
J
Johannes Rieken 已提交
13 14 15 16 17
import { IAction } from 'vs/base/common/actions';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { domEvent } from 'vs/base/browser/event';
import { Emitter } from 'vs/base/common/event';
I
isidor 已提交
18 19
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { memoize } from 'vs/base/common/decorators';
B
Benjamin Pasero 已提交
20 21 22
import { IdGenerator } from 'vs/base/common/idGenerator';
import { createCSSRule } from 'vs/base/browser/dom';
import URI from 'vs/base/common/uri';
23

I
isidor 已提交
24
class AltKeyEmitter extends Emitter<boolean> {
25 26 27 28

	private _subscriptions: IDisposable[] = [];
	private _isPressed: boolean;

I
isidor 已提交
29
	private constructor(contextMenuService: IContextMenuService) {
30 31 32 33 34 35
		super();

		this._subscriptions.push(domEvent(document.body, 'keydown')(e => this.isPressed = e.altKey));
		this._subscriptions.push(domEvent(document.body, 'keyup')(e => this.isPressed = false));
		this._subscriptions.push(domEvent(document.body, 'mouseleave')(e => this.isPressed = false));
		this._subscriptions.push(domEvent(document.body, 'blur')(e => this.isPressed = false));
I
isidor 已提交
36 37
		// Workaround since we do not get any events while a context menu is shown
		this._subscriptions.push(contextMenuService.onDidContextMenu(() => this.isPressed = false));
38 39 40 41 42 43 44 45 46 47 48
	}

	get isPressed(): boolean {
		return this._isPressed;
	}

	set isPressed(value: boolean) {
		this._isPressed = value;
		this.fire(this._isPressed);
	}

I
isidor 已提交
49 50 51 52 53
	@memoize
	static getInstance(contextMenuService: IContextMenuService) {
		return new AltKeyEmitter(contextMenuService);
	}

54 55 56 57
	dispose() {
		super.dispose();
		this._subscriptions = dispose(this._subscriptions);
	}
I
isidor 已提交
58
}
59

I
isidor 已提交
60
export function fillInActions(menu: IMenu, options: IMenuActionOptions, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, contextMenuService: IContextMenuService, isPrimaryGroup: (group: string) => boolean = group => group === 'navigation'): void {
61
	const groups = menu.getActions(options);
62
	if (groups.length === 0) {
63 64
		return;
	}
I
isidor 已提交
65
	const altKey = AltKeyEmitter.getInstance(contextMenuService);
66

67
	for (let tuple of groups) {
68
		let [group, actions] = tuple;
I
isidor 已提交
69
		if (altKey.isPressed) {
70 71 72
			actions = actions.map(a => !!a.alt ? a.alt : a);
		}

J
Joao Moreno 已提交
73
		if (isPrimaryGroup(group)) {
74 75 76 77 78 79 80 81 82 83

			const head = Array.isArray<IAction>(target) ? target : target.primary;

			// split contributed actions at the point where order
			// changes form lt zero to gte
			let pivot = 0;
			for (; pivot < actions.length; pivot++) {
				if ((<MenuItemAction>actions[pivot]).order >= 0) {
					break;
				}
84
			}
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
			// prepend contributed actions with order lte zero
			head.unshift(...actions.slice(0, pivot));

			// find the first separator which marks the end of the
			// navigation group - might be the whole array length
			let sep = 0;
			while (sep < head.length) {
				if (head[sep] instanceof Separator) {
					break;
				}
				sep++;
			}
			// append contributed actions with order gt zero
			head.splice(sep, 0, ...actions.slice(pivot));

100
		} else {
J
Joao Moreno 已提交
101 102 103 104
			const to = Array.isArray<IAction>(target) ? target : target.secondary;

			if (to.length > 0) {
				to.push(new Separator());
105
			}
J
Joao Moreno 已提交
106 107

			to.push(...actions);
108 109 110 111 112
		}
	}
}


I
isidor 已提交
113
export function createActionItem(action: IAction, keybindingService: IKeybindingService, messageService: IMessageService, contextMenuService: IContextMenuService): ActionItem {
114
	if (action instanceof MenuItemAction) {
I
isidor 已提交
115
		return new MenuItemActionItem(action, keybindingService, messageService, contextMenuService);
116
	}
117
	return undefined;
118 119
}

B
Benjamin Pasero 已提交
120 121
const ids = new IdGenerator('menu-item-action-item-icon-');

122
export class MenuItemActionItem extends ActionItem {
123

B
Benjamin Pasero 已提交
124 125
	static readonly ICON_PATH_TO_CSS_RULES: Map<string /* path*/, string /* CSS rule */> = new Map<string, string>();

126
	private _wantsAltCommand: boolean = false;
B
Benjamin Pasero 已提交
127
	private _itemClassDispose: IDisposable;
128 129

	constructor(
B
Benjamin Pasero 已提交
130
		public _action: MenuItemAction,
131
		@IKeybindingService private _keybindingService: IKeybindingService,
I
isidor 已提交
132 133
		@IMessageService protected _messageService: IMessageService,
		@IContextMenuService private _contextMenuService: IContextMenuService
134
	) {
B
Benjamin Pasero 已提交
135
		super(undefined, _action, { icon: !!(_action.class || _action.item.iconPath), label: !_action.class && !_action.item.iconPath });
136 137
	}

138
	protected get _commandAction(): IAction {
139
		return this._wantsAltCommand && (<MenuItemAction>this._action).alt || this._action;
140 141 142 143 144 145
	}

	onClick(event: MouseEvent): void {
		event.preventDefault();
		event.stopPropagation();

146 147
		this.actionRunner.run(this._commandAction)
			.done(undefined, err => this._messageService.show(Severity.Error, err));
148 149 150 151 152
	}

	render(container: HTMLElement): void {
		super.render(container);

B
Benjamin Pasero 已提交
153
		this._updateItemClass(this._action.item);
B
Benjamin Pasero 已提交
154

155 156
		let mouseOver = false;
		let altDown = false;
157

158 159 160 161
		const updateAltState = () => {
			const wantsAltCommand = mouseOver && altDown;
			if (wantsAltCommand !== this._wantsAltCommand) {
				this._wantsAltCommand = wantsAltCommand;
162 163 164
				this._updateLabel();
				this._updateTooltip();
				this._updateClass();
165 166
			}
		};
167

I
isidor 已提交
168
		this._callOnDispose.push(AltKeyEmitter.getInstance(this._contextMenuService).event(value => {
169 170 171 172 173 174 175 176 177 178 179 180
			altDown = value;
			updateAltState();
		}));

		this._callOnDispose.push(domEvent(container, 'mouseleave')(_ => {
			mouseOver = false;
			updateAltState();
		}));

		this._callOnDispose.push(domEvent(container, 'mouseenter')(e => {
			mouseOver = true;
			updateAltState();
181 182 183 184 185
		}));
	}

	_updateLabel(): void {
		if (this.options.label) {
186
			this.$e.text(this._commandAction.label);
187 188 189 190 191
		}
	}

	_updateTooltip(): void {
		const element = this.$e.getHTMLElement();
192
		const keybinding = this._keybindingService.lookupKeybinding(this._commandAction.id);
193
		const keybindingLabel = keybinding && keybinding.getLabel();
194 195

		element.title = keybindingLabel
196 197
			? localize('titleAndKb', "{0} ({1})", this._commandAction.label, keybindingLabel)
			: this._commandAction.label;
198 199 200 201
	}

	_updateClass(): void {
		if (this.options.icon) {
202
			if (this._commandAction !== this._action) {
B
Benjamin Pasero 已提交
203
				this._updateItemClass(this._action.alt.item);
204
			} else if ((<MenuItemAction>this._action).alt) {
B
Benjamin Pasero 已提交
205
				this._updateItemClass(this._action.item);
B
Benjamin Pasero 已提交
206 207 208 209 210 211 212 213 214 215
			}
		}
	}

	_updateItemClass(item: ICommandAction): void {
		dispose(this._itemClassDispose);
		this._itemClassDispose = undefined;

		if (item.iconPath) {
			let iconClass: string;
B
Benjamin Pasero 已提交
216 217 218

			if (MenuItemActionItem.ICON_PATH_TO_CSS_RULES.has(item.iconPath.dark)) {
				iconClass = MenuItemActionItem.ICON_PATH_TO_CSS_RULES.get(item.iconPath.dark);
B
Benjamin Pasero 已提交
219
			} else {
B
Benjamin Pasero 已提交
220 221 222 223
				iconClass = ids.nextId();
				createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(item.iconPath.light || item.iconPath.dark).toString()}")`);
				createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: url("${URI.file(item.iconPath.dark).toString()}")`);
				MenuItemActionItem.ICON_PATH_TO_CSS_RULES.set(item.iconPath.dark, iconClass);
224
			}
B
Benjamin Pasero 已提交
225 226 227

			this.$e.getHTMLElement().classList.add('icon', iconClass);
			this._itemClassDispose = { dispose: () => this.$e.getHTMLElement().classList.remove('icon', iconClass) };
228 229 230
		}
	}
}