menuItemActionItem.ts 8.0 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
		private 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 154
		this._updateItemClass(this.action.item);

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 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
				this._updateItemClass(this.action.item);
			}
		}
	}

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

		if (item.iconPath) {
			let iconClass: string;
			if (typeof item.iconPath === 'string') {
				if (MenuItemActionItem.ICON_PATH_TO_CSS_RULES.has(item.iconPath)) {
					iconClass = MenuItemActionItem.ICON_PATH_TO_CSS_RULES.get(item.iconPath);
				} else {
					iconClass = ids.nextId();
					createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(item.iconPath).toString()}")`);
					MenuItemActionItem.ICON_PATH_TO_CSS_RULES.set(item.iconPath, iconClass);
				}
			} else {
				if (MenuItemActionItem.ICON_PATH_TO_CSS_RULES.has(item.iconPath.dark)) {
					iconClass = MenuItemActionItem.ICON_PATH_TO_CSS_RULES.get(item.iconPath.dark);
				} else {
					iconClass = ids.nextId();
					createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(item.iconPath.light).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);
				}
233
			}
B
Benjamin Pasero 已提交
234 235 236

			this.$e.getHTMLElement().classList.add('icon', iconClass);
			this._itemClassDispose = { dispose: () => this.$e.getHTMLElement().classList.remove('icon', iconClass) };
237 238 239
		}
	}
}