menu.ts 25.9 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import 'vs/css!./menu';
7
import * as nls from 'vs/nls';
S
SteVen Batten 已提交
8
import * as strings from 'vs/base/common/strings';
J
João Moreno 已提交
9
import { IActionRunner, IAction, SubmenuAction } from 'vs/base/common/actions';
10
import { ActionBar, IActionViewItemProvider, ActionsOrientation, Separator, ActionViewItem, IActionViewItemOptions, BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
S
SteVen Batten 已提交
11
import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes';
12
import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, hasClass, addDisposableListener, removeClass, append, $, addClasses, removeClasses, clearNode } from 'vs/base/browser/dom';
13
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
14
import { RunOnceScheduler } from 'vs/base/common/async';
15
import { DisposableStore } from 'vs/base/common/lifecycle';
16
import { Color } from 'vs/base/common/color';
S
SteVen Batten 已提交
17
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
S
SteVen Batten 已提交
18
import { ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable';
19
import { Event } from 'vs/base/common/event';
20
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
S
SteVen Batten 已提交
21
import { isLinux, isMacintosh } from 'vs/base/common/platform';
M
Martin Aeschlimann 已提交
22
import { Codicon, registerIcon, stripCodicons } from 'vs/base/common/codicons';
E
Erich Gamma 已提交
23

S
SteVen Batten 已提交
24 25
export const MENU_MNEMONIC_REGEX = /\(&([^\s&])\)|(^|[^&])&([^\s&])/;
export const MENU_ESCAPED_MNEMONIC_REGEX = /(&)?(&)([^\s&])/g;
S
SteVen Batten 已提交
26

I
IllusionMH 已提交
27 28
const menuSelectionIcon = registerIcon('menu-selection', Codicon.check);
const menuSubmenuIcon = registerIcon('menu-submenu', Codicon.chevronRight);
29

S
SteVen Batten 已提交
30 31 32 33 34
export enum Direction {
	Right,
	Left
}

E
Erich Gamma 已提交
35
export interface IMenuOptions {
B
Benjamin Pasero 已提交
36
	context?: any;
37
	actionViewItemProvider?: IActionViewItemProvider;
B
Benjamin Pasero 已提交
38
	actionRunner?: IActionRunner;
M
Matt Bierner 已提交
39
	getKeyBinding?: (action: IAction) => ResolvedKeybinding | undefined;
S
SteVen Batten 已提交
40
	ariaLabel?: string;
S
SteVen Batten 已提交
41
	enableMnemonics?: boolean;
42
	anchorAlignment?: AnchorAlignment;
S
SteVen Batten 已提交
43
	expandDirection?: Direction;
S
SteVen Batten 已提交
44
	useEventAsContext?: boolean;
E
Erich Gamma 已提交
45 46
}

47 48 49 50 51 52 53 54 55 56
export interface IMenuStyles {
	shadowColor?: Color;
	borderColor?: Color;
	foregroundColor?: Color;
	backgroundColor?: Color;
	selectionForegroundColor?: Color;
	selectionBackgroundColor?: Color;
	selectionBorderColor?: Color;
	separatorColor?: Color;
}
57 58 59 60 61 62

interface ISubMenuData {
	parent: Menu;
	submenu?: Menu;
}

S
SteVen Batten 已提交
63
export class Menu extends ActionBar {
64
	private mnemonics: Map<string, Array<BaseMenuActionViewItem>>;
65
	private readonly menuDisposables: DisposableStore;
S
SteVen Batten 已提交
66 67
	private scrollableElement: DomScrollableElement;
	private menuElement: HTMLElement;
E
Erich Gamma 已提交
68

69
	constructor(container: HTMLElement, actions: ReadonlyArray<IAction>, options: IMenuOptions = {}) {
70
		addClass(container, 'monaco-menu-container');
S
SteVen Batten 已提交
71
		container.setAttribute('role', 'presentation');
S
SteVen Batten 已提交
72 73 74
		const menuElement = document.createElement('div');
		addClass(menuElement, 'monaco-menu');
		menuElement.setAttribute('role', 'presentation');
E
Erich Gamma 已提交
75

S
SteVen Batten 已提交
76
		super(menuElement, {
B
Benjamin Pasero 已提交
77
			orientation: ActionsOrientation.VERTICAL,
78
			actionViewItemProvider: action => this.doGetActionViewItem(action, options, parentData),
E
Erich Gamma 已提交
79
			context: options.context,
A
Alex Dima 已提交
80
			actionRunner: options.actionRunner,
81
			ariaLabel: options.ariaLabel,
S
SteVen Batten 已提交
82
			triggerKeys: { keys: [KeyCode.Enter, ...(isMacintosh || isLinux ? [KeyCode.Space] : [])], keyDown: true }
E
Erich Gamma 已提交
83 84
		});

S
SteVen Batten 已提交
85 86
		this.menuElement = menuElement;

S
SteVen Batten 已提交
87 88
		this.actionsList.setAttribute('role', 'menu');

89
		this.actionsList.tabIndex = 0;
S
SteVen Batten 已提交
90

91
		this.menuDisposables = this._register(new DisposableStore());
S
SteVen Batten 已提交
92

S
SteVen Batten 已提交
93 94 95 96 97
		addDisposableListener(menuElement, EventType.KEY_DOWN, (e) => {
			const event = new StandardKeyboardEvent(e);

			// Stop tab navigation of menus
			if (event.equals(KeyCode.Tab)) {
S
SteVen Batten 已提交
98
				e.preventDefault();
S
SteVen Batten 已提交
99 100 101
			}
		});

S
SteVen Batten 已提交
102
		if (options.enableMnemonics) {
103
			this.menuDisposables.add(addDisposableListener(menuElement, EventType.KEY_DOWN, (e) => {
S
SteVen Batten 已提交
104
				const key = e.key.toLocaleLowerCase();
S
SteVen Batten 已提交
105 106
				if (this.mnemonics.has(key)) {
					EventHelper.stop(e, true);
107
					const actions = this.mnemonics.get(key)!;
S
SteVen Batten 已提交
108 109

					if (actions.length === 1) {
S
SteVen Batten 已提交
110
						if (actions[0] instanceof SubmenuMenuActionViewItem && actions[0].container) {
S
SteVen Batten 已提交
111 112 113
							this.focusItemByElement(actions[0].container);
						}

114
						actions[0].onClick(e);
S
SteVen Batten 已提交
115 116 117 118
					}

					if (actions.length > 1) {
						const action = actions.shift();
S
SteVen Batten 已提交
119
						if (action && action.container) {
120 121 122
							this.focusItemByElement(action.container);
							actions.push(action);
						}
S
SteVen Batten 已提交
123 124 125 126 127 128 129

						this.mnemonics.set(key, actions);
					}
				}
			}));
		}

S
SteVen Batten 已提交
130 131 132 133 134
		if (isLinux) {
			this._register(addDisposableListener(menuElement, EventType.KEY_DOWN, e => {
				const event = new StandardKeyboardEvent(e);

				if (event.equals(KeyCode.Home) || event.equals(KeyCode.PageUp)) {
135
					this.focusedItem = this.viewItems.length - 1;
S
SteVen Batten 已提交
136 137 138 139 140 141 142 143 144 145
					this.focusNext();
					EventHelper.stop(e, true);
				} else if (event.equals(KeyCode.End) || event.equals(KeyCode.PageDown)) {
					this.focusedItem = 0;
					this.focusPrevious();
					EventHelper.stop(e, true);
				}
			}));
		}

146
		this._register(addDisposableListener(this.domNode, EventType.MOUSE_OUT, e => {
147
			let relatedTarget = e.relatedTarget as HTMLElement;
S
SteVen Batten 已提交
148 149 150 151 152
			if (!isAncestor(relatedTarget, this.domNode)) {
				this.focusedItem = undefined;
				this.updateFocus();
				e.stopPropagation();
			}
153
		}));
S
SteVen Batten 已提交
154

155
		this._register(addDisposableListener(this.actionsList, EventType.MOUSE_OVER, e => {
S
SteVen Batten 已提交
156 157 158 159 160
			let target = e.target as HTMLElement;
			if (!target || !isAncestor(target, this.actionsList) || target === this.actionsList) {
				return;
			}

161
			while (target.parentElement !== this.actionsList && target.parentElement !== null) {
S
SteVen Batten 已提交
162 163 164 165 166 167 168 169 170 171 172
				target = target.parentElement;
			}

			if (hasClass(target, 'action-item')) {
				const lastFocusedItem = this.focusedItem;
				this.setFocusedItem(target);

				if (lastFocusedItem !== this.focusedItem) {
					this.updateFocus();
				}
			}
173
		}));
S
SteVen Batten 已提交
174 175 176 177 178

		let parentData: ISubMenuData = {
			parent: this
		};

179
		this.mnemonics = new Map<string, Array<BaseMenuActionViewItem>>();
S
SteVen Batten 已提交
180

S
SteVen Batten 已提交
181 182 183 184 185 186 187 188 189 190 191
		// Scroll Logic
		this.scrollableElement = this._register(new DomScrollableElement(menuElement, {
			alwaysConsumeMouseWheel: true,
			horizontal: ScrollbarVisibility.Hidden,
			vertical: ScrollbarVisibility.Visible,
			verticalScrollbarSize: 7,
			handleMouseWheel: true,
			useShadows: true
		}));

		const scrollElement = this.scrollableElement.getDomNode();
192
		scrollElement.style.position = '';
S
SteVen Batten 已提交
193

194 195 196 197 198
		this._register(addDisposableListener(scrollElement, EventType.MOUSE_UP, e => {
			// Absorb clicks in menu dead space https://github.com/Microsoft/vscode/issues/63575
			// We do this on the scroll element so the scroll bar doesn't dismiss the menu either
			e.preventDefault();
		}));
S
SteVen Batten 已提交
199

200
		menuElement.style.maxHeight = `${Math.max(10, window.innerHeight - container.getBoundingClientRect().top - 30)}px`;
S
SteVen Batten 已提交
201

202
		this.push(actions, { icon: true, label: true, isMenu: true });
S
SteVen Batten 已提交
203 204 205 206

		container.appendChild(this.scrollableElement.getDomNode());
		this.scrollableElement.scanDomNode();

B
Benjamin Pasero 已提交
207
		this.viewItems.filter(item => !(item instanceof MenuSeparatorActionViewItem)).forEach((item, index, array) => {
M
Matt Bierner 已提交
208
			(item as BaseMenuActionViewItem).updatePositionInSet(index + 1, array.length);
S
SteVen Batten 已提交
209
		});
S
SteVen Batten 已提交
210 211
	}

212 213 214
	style(style: IMenuStyles): void {
		const container = this.getContainer();

215 216
		const fgColor = style.foregroundColor ? `${style.foregroundColor}` : '';
		const bgColor = style.backgroundColor ? `${style.backgroundColor}` : '';
217
		const border = style.borderColor ? `1px solid ${style.borderColor}` : '';
218
		const shadow = style.shadowColor ? `0 2px 4px ${style.shadowColor}` : '';
219 220 221 222 223 224

		container.style.border = border;
		this.domNode.style.color = fgColor;
		this.domNode.style.backgroundColor = bgColor;
		container.style.boxShadow = shadow;

225 226 227
		if (this.viewItems) {
			this.viewItems.forEach(item => {
				if (item instanceof BaseMenuActionViewItem || item instanceof MenuSeparatorActionViewItem) {
228 229 230 231
					item.style(style);
				}
			});
		}
232 233
	}

S
SteVen Batten 已提交
234 235 236 237
	getContainer(): HTMLElement {
		return this.scrollableElement.getDomNode();
	}

238 239
	get onScroll(): Event<ScrollEvent> {
		return this.scrollableElement.onScroll;
S
SteVen Batten 已提交
240 241 242 243 244 245
	}

	get scrollOffset(): number {
		return this.menuElement.scrollTop;
	}

S
SteVen Batten 已提交
246
	trigger(index: number): void {
247 248 249
		if (index <= this.viewItems.length && index >= 0) {
			const item = this.viewItems[index];
			if (item instanceof SubmenuMenuActionViewItem) {
S
SteVen Batten 已提交
250 251
				super.focus(index);
				item.open(true);
252
			} else if (item instanceof BaseMenuActionViewItem) {
S
SteVen Batten 已提交
253 254 255 256 257 258 259
				super.run(item._action, item._context);
			} else {
				return;
			}
		}
	}

S
SteVen Batten 已提交
260 261 262 263 264 265 266 267 268
	private focusItemByElement(element: HTMLElement) {
		const lastFocusedItem = this.focusedItem;
		this.setFocusedItem(element);

		if (lastFocusedItem !== this.focusedItem) {
			this.updateFocus();
		}
	}

S
SteVen Batten 已提交
269 270 271 272 273 274 275 276
	private setFocusedItem(element: HTMLElement): void {
		for (let i = 0; i < this.actionsList.children.length; i++) {
			let elem = this.actionsList.children[i];
			if (element === elem) {
				this.focusedItem = i;
				break;
			}
		}
E
Erich Gamma 已提交
277 278
	}

279 280 281 282 283 284 285 286 287 288 289 290 291
	protected updateFocus(fromRight?: boolean): void {
		super.updateFocus(fromRight, true);

		if (typeof this.focusedItem !== 'undefined') {
			// Workaround for #80047 caused by an issue in chromium
			// https://bugs.chromium.org/p/chromium/issues/detail?id=414283
			// When that's fixed, just call this.scrollableElement.scanDomNode()
			this.scrollableElement.setScrollPosition({
				scrollTop: Math.round(this.menuElement.scrollTop)
			});
		}
	}

292
	private doGetActionViewItem(action: IAction, options: IMenuOptions, parentData: ISubMenuData): BaseActionViewItem {
S
SteVen Batten 已提交
293
		if (action instanceof Separator) {
294
			return new MenuSeparatorActionViewItem(options.context, action, { icon: true });
S
SteVen Batten 已提交
295
		} else if (action instanceof SubmenuAction) {
296
			const menuActionViewItem = new SubmenuMenuActionViewItem(action, action.entries, parentData, options);
S
SteVen Batten 已提交
297 298

			if (options.enableMnemonics) {
299 300 301
				const mnemonic = menuActionViewItem.getMnemonic();
				if (mnemonic && menuActionViewItem.isEnabled()) {
					let actionViewItems: BaseMenuActionViewItem[] = [];
S
SteVen Batten 已提交
302
					if (this.mnemonics.has(mnemonic)) {
303
						actionViewItems = this.mnemonics.get(mnemonic)!;
S
SteVen Batten 已提交
304 305
					}

306
					actionViewItems.push(menuActionViewItem);
S
SteVen Batten 已提交
307

308
					this.mnemonics.set(mnemonic, actionViewItems);
S
SteVen Batten 已提交
309 310 311
				}
			}

312
			return menuActionViewItem;
S
SteVen Batten 已提交
313
		} else {
S
SteVen Batten 已提交
314
			const menuItemOptions: IMenuItemOptions = { enableMnemonics: options.enableMnemonics, useEventAsContext: options.useEventAsContext };
S
SteVen Batten 已提交
315 316 317
			if (options.getKeyBinding) {
				const keybinding = options.getKeyBinding(action);
				if (keybinding) {
318 319 320 321 322
					const keybindingLabel = keybinding.getLabel();

					if (keybindingLabel) {
						menuItemOptions.keybinding = keybindingLabel;
					}
S
SteVen Batten 已提交
323 324 325
				}
			}

326
			const menuActionViewItem = new BaseMenuActionViewItem(options.context, action, menuItemOptions);
S
SteVen Batten 已提交
327 328

			if (options.enableMnemonics) {
329 330 331
				const mnemonic = menuActionViewItem.getMnemonic();
				if (mnemonic && menuActionViewItem.isEnabled()) {
					let actionViewItems: BaseMenuActionViewItem[] = [];
S
SteVen Batten 已提交
332
					if (this.mnemonics.has(mnemonic)) {
333
						actionViewItems = this.mnemonics.get(mnemonic)!;
S
SteVen Batten 已提交
334 335
					}

336
					actionViewItems.push(menuActionViewItem);
S
SteVen Batten 已提交
337

338
					this.mnemonics.set(mnemonic, actionViewItems);
S
SteVen Batten 已提交
339 340 341
				}
			}

342
			return menuActionViewItem;
S
SteVen Batten 已提交
343 344
		}
	}
345 346
}

347
interface IMenuItemOptions extends IActionViewItemOptions {
S
SteVen Batten 已提交
348 349 350
	enableMnemonics?: boolean;
}

351
class BaseMenuActionViewItem extends BaseActionViewItem {
352

S
SteVen Batten 已提交
353
	public container: HTMLElement | undefined;
354

S
SteVen Batten 已提交
355
	protected options: IMenuItemOptions;
S
SteVen Batten 已提交
356
	protected item: HTMLElement | undefined;
357

S
SteVen Batten 已提交
358
	private runOnceToEnableMouseUp: RunOnceScheduler;
S
SteVen Batten 已提交
359 360 361
	private label: HTMLElement | undefined;
	private check: HTMLElement | undefined;
	private mnemonic: string | undefined;
362
	private cssClass: string;
S
SteVen Batten 已提交
363
	protected menuStyle: IMenuStyles | undefined;
364

B
Benjamin Pasero 已提交
365
	constructor(ctx: unknown, action: IAction, options: IMenuItemOptions = {}) {
366 367 368
		options.isMenu = true;
		super(action, action, options);

369 370 371 372
		this.options = options;
		this.options.icon = options.icon !== undefined ? options.icon : false;
		this.options.label = options.label !== undefined ? options.label : true;
		this.cssClass = '';
S
SteVen Batten 已提交
373 374 375 376 377

		// Set mnemonic
		if (this.options.label && options.enableMnemonics) {
			let label = this.getAction().label;
			if (label) {
S
SteVen Batten 已提交
378 379
				let matches = MENU_MNEMONIC_REGEX.exec(label);
				if (matches) {
S
SteVen Batten 已提交
380
					this.mnemonic = (!!matches[1] ? matches[1] : matches[3]).toLocaleLowerCase();
S
SteVen Batten 已提交
381 382 383
				}
			}
		}
S
SteVen Batten 已提交
384 385 386 387 388 389 390 391

		// Add mouse up listener later to avoid accidental clicks
		this.runOnceToEnableMouseUp = new RunOnceScheduler(() => {
			if (!this.element) {
				return;
			}

			this._register(addDisposableListener(this.element, EventType.MOUSE_UP, e => {
S
SteVen Batten 已提交
392 393 394
				// removed default prevention as it conflicts
				// with BaseActionViewItem #101537
				// add back if issues arise and link new issue
S
SteVen Batten 已提交
395 396 397
				EventHelper.stop(e, true);
				this.onClick(e);
			}));
S
SteVen Batten 已提交
398
		}, 100);
S
SteVen Batten 已提交
399 400

		this._register(this.runOnceToEnableMouseUp);
401
	}
402

S
SteVen Batten 已提交
403
	render(container: HTMLElement): void {
404
		super.render(container);
405

406 407 408 409
		if (!this.element) {
			return;
		}

S
SteVen Batten 已提交
410 411
		this.container = container;

412
		this.item = append(this.element, $('a.action-menu-item'));
413 414
		if (this._action.id === Separator.ID) {
			// A separator is a presentation item
415
			this.item.setAttribute('role', 'presentation');
416
		} else {
417
			this.item.setAttribute('role', 'menuitem');
418
			if (this.mnemonic) {
419
				this.item.setAttribute('aria-keyshortcuts', `${this.mnemonic}`);
420
			}
421 422
		}

I
IllusionMH 已提交
423
		this.check = append(this.item, $('span.menu-item-check' + menuSelectionIcon.cssSelector));
424 425 426
		this.check.setAttribute('role', 'none');

		this.label = append(this.item, $('span.action-label'));
427

428
		if (this.options.label && this.options.keybinding) {
429
			append(this.item, $('span.keybinding')).textContent = this.options.keybinding;
430 431
		}

S
SteVen Batten 已提交
432 433
		// Adds mouse up listener to actually run the action
		this.runOnceToEnableMouseUp.schedule();
S
SteVen Batten 已提交
434

435 436 437 438 439
		this.updateClass();
		this.updateLabel();
		this.updateTooltip();
		this.updateEnabled();
		this.updateChecked();
440 441
	}

442 443 444 445 446
	blur(): void {
		super.blur();
		this.applyStyle();
	}

S
SteVen Batten 已提交
447
	focus(): void {
448
		super.focus();
S
SteVen Batten 已提交
449 450 451 452 453

		if (this.item) {
			this.item.focus();
		}

454
		this.applyStyle();
455 456
	}

S
SteVen Batten 已提交
457
	updatePositionInSet(pos: number, setSize: number): void {
S
SteVen Batten 已提交
458 459 460 461
		if (this.item) {
			this.item.setAttribute('aria-posinset', `${pos}`);
			this.item.setAttribute('aria-setsize', `${setSize}`);
		}
S
SteVen Batten 已提交
462 463
	}

464
	updateLabel(): void {
465 466 467 468
		if (!this.label) {
			return;
		}

469
		if (this.options.label) {
470 471
			clearNode(this.label);

S
SteVen Batten 已提交
472
			let label = stripCodicons(this.getAction().label);
473
			if (label) {
S
SteVen Batten 已提交
474 475 476 477
				const cleanLabel = cleanMnemonic(label);
				if (!this.options.enableMnemonics) {
					label = cleanLabel;
				}
478

479
				this.label.setAttribute('aria-label', cleanLabel.replace(/&&/g, '&'));
S
SteVen Batten 已提交
480

S
SteVen Batten 已提交
481
				const matches = MENU_MNEMONIC_REGEX.exec(label);
482

S
SteVen Batten 已提交
483
				if (matches) {
S
SteVen Batten 已提交
484 485 486 487 488 489 490 491 492 493 494
					label = strings.escape(label);

					// This is global, reset it
					MENU_ESCAPED_MNEMONIC_REGEX.lastIndex = 0;
					let escMatch = MENU_ESCAPED_MNEMONIC_REGEX.exec(label);

					// We can't use negative lookbehind so if we match our negative and skip
					while (escMatch && escMatch[1]) {
						escMatch = MENU_ESCAPED_MNEMONIC_REGEX.exec(label);
					}

495 496
					const replaceDoubleEscapes = (str: string) => str.replace(/&amp;&amp;/g, '&amp;');

S
SteVen Batten 已提交
497
					if (escMatch) {
498 499 500 501 502 503 504
						this.label.append(
							strings.ltrim(replaceDoubleEscapes(label.substr(0, escMatch.index)), ' '),
							$('u', { 'aria-hidden': 'true' },
								escMatch[3]),
							strings.rtrim(replaceDoubleEscapes(label.substr(escMatch.index + escMatch[0].length)), ' '));
					} else {
						this.label.innerText = replaceDoubleEscapes(label).trim();
S
SteVen Batten 已提交
505 506
					}

S
SteVen Batten 已提交
507 508 509
					if (this.item) {
						this.item.setAttribute('aria-keyshortcuts', (!!matches[1] ? matches[1] : matches[3]).toLocaleLowerCase());
					}
510
				} else {
511
					this.label.innerText = label.replace(/&&/g, '&').trim();
S
SteVen Batten 已提交
512
				}
513
			}
514 515 516
		}
	}

517
	updateTooltip(): void {
518
		let title: string | null = null;
519 520 521 522 523 524 525 526 527 528 529 530

		if (this.getAction().tooltip) {
			title = this.getAction().tooltip;

		} else if (!this.options.label && this.getAction().label && this.options.icon) {
			title = this.getAction().label;

			if (this.options.keybinding) {
				title = nls.localize({ key: 'titleLabel', comment: ['action title', 'action keybinding'] }, "{0} ({1})", title, this.options.keybinding);
			}
		}

S
SteVen Batten 已提交
531
		if (title && this.item) {
532
			this.item.title = title;
533 534 535
		}
	}

536
	updateClass(): void {
S
SteVen Batten 已提交
537
		if (this.cssClass && this.item) {
538
			removeClasses(this.item, this.cssClass);
539
		}
S
SteVen Batten 已提交
540
		if (this.options.icon && this.label) {
541
			this.cssClass = this.getAction().class || '';
542
			addClass(this.label, 'icon');
543
			if (this.cssClass) {
544
				addClasses(this.label, this.cssClass);
545
			}
546
			this.updateEnabled();
S
SteVen Batten 已提交
547
		} else if (this.label) {
548
			removeClass(this.label, 'icon');
549 550 551
		}
	}

552
	updateEnabled(): void {
553
		if (this.getAction().enabled) {
554 555 556 557
			if (this.element) {
				removeClass(this.element, 'disabled');
			}

S
SteVen Batten 已提交
558 559 560 561
			if (this.item) {
				removeClass(this.item, 'disabled');
				this.item.tabIndex = 0;
			}
562
		} else {
563 564 565 566
			if (this.element) {
				addClass(this.element, 'disabled');
			}

S
SteVen Batten 已提交
567 568 569 570
			if (this.item) {
				addClass(this.item, 'disabled');
				removeTabIndexAndUpdateFocus(this.item);
			}
571 572 573
		}
	}

574
	updateChecked(): void {
S
SteVen Batten 已提交
575 576 577 578
		if (!this.item) {
			return;
		}

579
		if (this.getAction().checked) {
580 581 582
			addClass(this.item, 'checked');
			this.item.setAttribute('role', 'menuitemcheckbox');
			this.item.setAttribute('aria-checked', 'true');
583
		} else {
584 585 586
			removeClass(this.item, 'checked');
			this.item.setAttribute('role', 'menuitem');
			this.item.setAttribute('aria-checked', 'false');
587 588
		}
	}
S
SteVen Batten 已提交
589

S
SteVen Batten 已提交
590
	getMnemonic(): string | undefined {
S
SteVen Batten 已提交
591 592
		return this.mnemonic;
	}
593 594

	protected applyStyle(): void {
S
SteVen Batten 已提交
595 596 597 598
		if (!this.menuStyle) {
			return;
		}

599
		const isSelected = this.element && hasClass(this.element, 'focused');
600
		const fgColor = isSelected && this.menuStyle.selectionForegroundColor ? this.menuStyle.selectionForegroundColor : this.menuStyle.foregroundColor;
S
SteVen Batten 已提交
601
		const bgColor = isSelected && this.menuStyle.selectionBackgroundColor ? this.menuStyle.selectionBackgroundColor : undefined;
602
		const border = isSelected && this.menuStyle.selectionBorderColor ? `thin solid ${this.menuStyle.selectionBorderColor}` : '';
603

S
SteVen Batten 已提交
604
		if (this.item) {
M
Matt Bierner 已提交
605 606
			this.item.style.color = fgColor ? fgColor.toString() : '';
			this.item.style.backgroundColor = bgColor ? bgColor.toString() : '';
S
SteVen Batten 已提交
607 608 609
		}

		if (this.check) {
M
Matt Bierner 已提交
610
			this.check.style.color = fgColor ? fgColor.toString() : '';
S
SteVen Batten 已提交
611 612 613 614 615
		}

		if (this.container) {
			this.container.style.border = border;
		}
616 617 618 619 620 621
	}

	style(style: IMenuStyles): void {
		this.menuStyle = style;
		this.applyStyle();
	}
622 623
}

624
class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
S
SteVen Batten 已提交
625
	private mysubmenu: Menu | null = null;
626
	private submenuContainer: HTMLElement | undefined;
S
SteVen Batten 已提交
627
	private submenuIndicator: HTMLElement | undefined;
628
	private readonly submenuDisposables = this._register(new DisposableStore());
S
SteVen Batten 已提交
629
	private mouseOver: boolean = false;
S
SteVen Batten 已提交
630
	private showScheduler: RunOnceScheduler;
631
	private hideScheduler: RunOnceScheduler;
S
SteVen Batten 已提交
632
	private expandDirection: Direction;
633 634 635

	constructor(
		action: IAction,
636
		private submenuActions: ReadonlyArray<IAction>,
637 638 639
		private parentData: ISubMenuData,
		private submenuOptions?: IMenuOptions
	) {
S
SteVen Batten 已提交
640
		super(action, action, submenuOptions);
641

S
SteVen Batten 已提交
642 643
		this.expandDirection = submenuOptions && submenuOptions.expandDirection !== undefined ? submenuOptions.expandDirection : Direction.Right;

S
SteVen Batten 已提交
644 645 646 647 648 649 650
		this.showScheduler = new RunOnceScheduler(() => {
			if (this.mouseOver) {
				this.cleanupExistingSubmenu(false);
				this.createSubmenu(false);
			}
		}, 250);

651
		this.hideScheduler = new RunOnceScheduler(() => {
652
			if (this.element && (!isAncestor(document.activeElement, this.element) && this.parentData.submenu === this.mysubmenu)) {
653
				this.parentData.parent.focus(false);
654 655 656
				this.cleanupExistingSubmenu(true);
			}
		}, 750);
657 658
	}

S
SteVen Batten 已提交
659
	render(container: HTMLElement): void {
660 661
		super.render(container);

662 663 664 665
		if (!this.element) {
			return;
		}

S
SteVen Batten 已提交
666 667 668
		if (this.item) {
			addClass(this.item, 'monaco-submenu-item');
			this.item.setAttribute('aria-haspopup', 'true');
669
			this.updateAriaExpanded('false');
670
			this.submenuIndicator = append(this.item, $('span.submenu-indicator' + menuSubmenuIcon.cssSelector));
S
SteVen Batten 已提交
671 672
			this.submenuIndicator.setAttribute('aria-hidden', 'true');
		}
673

674
		this._register(addDisposableListener(this.element, EventType.KEY_UP, e => {
675
			let event = new StandardKeyboardEvent(e);
676
			if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Enter)) {
677 678
				EventHelper.stop(e, true);

679
				this.createSubmenu(true);
680
			}
681
		}));
682

683
		this._register(addDisposableListener(this.element, EventType.KEY_DOWN, e => {
684
			let event = new StandardKeyboardEvent(e);
S
SteVen Batten 已提交
685 686 687 688 689

			if (document.activeElement === this.item) {
				if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Enter)) {
					EventHelper.stop(e, true);
				}
690
			}
691
		}));
692

693
		this._register(addDisposableListener(this.element, EventType.MOUSE_OVER, e => {
S
SteVen Batten 已提交
694 695 696 697 698
			if (!this.mouseOver) {
				this.mouseOver = true;

				this.showScheduler.schedule();
			}
699
		}));
S
SteVen Batten 已提交
700

701
		this._register(addDisposableListener(this.element, EventType.MOUSE_LEAVE, e => {
S
SteVen Batten 已提交
702
			this.mouseOver = false;
703
		}));
704

705
		this._register(addDisposableListener(this.element, EventType.FOCUS_OUT, e => {
706
			if (this.element && !isAncestor(document.activeElement, this.element)) {
707 708
				this.hideScheduler.schedule();
			}
709
		}));
S
SteVen Batten 已提交
710 711 712 713 714

		this._register(this.parentData.parent.onScroll(() => {
			this.parentData.parent.focus(false);
			this.cleanupExistingSubmenu(false);
		}));
715 716
	}

S
SteVen Batten 已提交
717 718 719 720 721
	open(selectFirst?: boolean): void {
		this.cleanupExistingSubmenu(false);
		this.createSubmenu(selectFirst);
	}

722
	onClick(e: EventLike): void {
723 724
		// stop clicking from trying to run an action
		EventHelper.stop(e, true);
725

S
SteVen Batten 已提交
726
		this.cleanupExistingSubmenu(false);
727
		this.createSubmenu(true);
728 729
	}

730
	private cleanupExistingSubmenu(force: boolean): void {
731 732
		if (this.parentData.submenu && (force || (this.parentData.submenu !== this.mysubmenu))) {
			this.parentData.submenu.dispose();
733
			this.parentData.submenu = undefined;
734
			this.updateAriaExpanded('false');
735
			if (this.submenuContainer) {
736
				this.submenuDisposables.clear();
737
				this.submenuContainer = undefined;
738
			}
739 740 741
		}
	}

742
	private createSubmenu(selectFirstItem = true): void {
743 744 745 746
		if (!this.element) {
			return;
		}

747
		if (!this.parentData.submenu) {
748
			this.updateAriaExpanded('true');
749 750
			this.submenuContainer = append(this.element, $('div.monaco-submenu'));
			addClasses(this.submenuContainer, 'menubar-menu-items-holder', 'context-view');
S
SteVen Batten 已提交
751

752 753 754 755 756 757
			// Set the top value of the menu container before construction
			// This allows the menu constructor to calculate the proper max height
			const computedStyles = getComputedStyle(this.parentData.parent.domNode);
			const paddingTop = parseFloat(computedStyles.paddingTop || '0') || 0;
			this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset - paddingTop}px`;

S
SteVen Batten 已提交
758 759 760 761 762 763 764 765
			this.parentData.submenu = new Menu(this.submenuContainer, this.submenuActions, this.submenuOptions);
			if (this.menuStyle) {
				this.parentData.submenu.style(this.menuStyle);
			}

			const boundingRect = this.element.getBoundingClientRect();
			const childBoundingRect = this.submenuContainer.getBoundingClientRect();

S
SteVen Batten 已提交
766 767 768 769 770 771 772 773 774 775 776
			if (this.expandDirection === Direction.Right) {
				if (window.innerWidth <= boundingRect.right + childBoundingRect.width) {
					this.submenuContainer.style.left = '10px';
					this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset + boundingRect.height}px`;
				} else {
					this.submenuContainer.style.left = `${this.element.offsetWidth}px`;
					this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset - paddingTop}px`;
				}
			} else if (this.expandDirection === Direction.Left) {
				this.submenuContainer.style.right = `${this.element.offsetWidth}px`;
				this.submenuContainer.style.left = 'auto';
S
SteVen Batten 已提交
777
				this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset - paddingTop}px`;
S
SteVen Batten 已提交
778
			}
779

780
			this.submenuDisposables.add(addDisposableListener(this.submenuContainer, EventType.KEY_UP, e => {
781
				let event = new StandardKeyboardEvent(e);
782 783 784 785
				if (event.equals(KeyCode.LeftArrow)) {
					EventHelper.stop(e, true);

					this.parentData.parent.focus();
786

787
					this.cleanupExistingSubmenu(true);
788
				}
789
			}));
790

791
			this.submenuDisposables.add(addDisposableListener(this.submenuContainer, EventType.KEY_DOWN, e => {
792
				let event = new StandardKeyboardEvent(e);
793 794 795
				if (event.equals(KeyCode.LeftArrow)) {
					EventHelper.stop(e, true);
				}
796
			}));
797

S
SteVen Batten 已提交
798

799
			this.submenuDisposables.add(this.parentData.submenu.onDidCancel(() => {
S
SteVen Batten 已提交
800
				this.parentData.parent.focus();
801

802
				this.cleanupExistingSubmenu(true);
803
			}));
S
SteVen Batten 已提交
804

805
			this.parentData.submenu.focus(selectFirstItem);
806 807

			this.mysubmenu = this.parentData.submenu;
808
		} else {
809
			this.parentData.submenu.focus(false);
810 811
		}
	}
812

813 814 815 816 817 818
	private updateAriaExpanded(value: string): void {
		if (this.item) {
			this.item?.setAttribute('aria-expanded', value);
		}
	}

819 820
	protected applyStyle(): void {
		super.applyStyle();
S
SteVen Batten 已提交
821 822 823 824 825

		if (!this.menuStyle) {
			return;
		}

826
		const isSelected = this.element && hasClass(this.element, 'focused');
827 828
		const fgColor = isSelected && this.menuStyle.selectionForegroundColor ? this.menuStyle.selectionForegroundColor : this.menuStyle.foregroundColor;

S
SteVen Batten 已提交
829
		if (this.submenuIndicator) {
830
			this.submenuIndicator.style.color = fgColor ? `${fgColor}` : '';
S
SteVen Batten 已提交
831
		}
832 833 834 835 836 837

		if (this.parentData.submenu) {
			this.parentData.submenu.style(this.menuStyle);
		}
	}

838
	dispose(): void {
839 840
		super.dispose();

841 842
		this.hideScheduler.dispose();

843 844 845 846
		if (this.mysubmenu) {
			this.mysubmenu.dispose();
			this.mysubmenu = null;
		}
847 848

		if (this.submenuContainer) {
849
			this.submenuContainer = undefined;
850
		}
851
	}
S
SteVen Batten 已提交
852
}
S
SteVen Batten 已提交
853

854
class MenuSeparatorActionViewItem extends ActionViewItem {
855
	style(style: IMenuStyles): void {
856
		if (this.label) {
857
			this.label.style.borderBottomColor = style.separatorColor ? `${style.separatorColor}` : '';
858
		}
859 860 861
	}
}

S
SteVen Batten 已提交
862 863 864 865 866 867 868 869
export function cleanMnemonic(label: string): string {
	const regex = MENU_MNEMONIC_REGEX;

	const matches = regex.exec(label);
	if (!matches) {
		return label;
	}

S
SteVen Batten 已提交
870
	const mnemonicInText = !matches[1];
S
SteVen Batten 已提交
871

S
SteVen Batten 已提交
872
	return label.replace(regex, mnemonicInText ? '$2$3' : '').trim();
S
SteVen Batten 已提交
873
}