eee3f60d9426c72f89468cf893e76f058b0664fac1db76255e8d90308a494521eaee8d88ca52db216a848bd4dd4508b58f75ee0d0c20a8eb6ce1f06f11e507 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import { isFirefox } from '../../browser.js';
  6. import { EventType as TouchEventType, Gesture } from '../../touch.js';
  7. import { $, addDisposableListener, append, clearNode, createStyleSheet, Dimension, EventHelper, EventType, getActiveElement, isAncestor, isInShadowDOM } from '../../dom.js';
  8. import { StandardKeyboardEvent } from '../../keyboardEvent.js';
  9. import { StandardMouseEvent } from '../../mouseEvent.js';
  10. import { ActionBar } from '../actionbar/actionbar.js';
  11. import { ActionViewItem, BaseActionViewItem } from '../actionbar/actionViewItems.js';
  12. import { formatRule } from '../codicons/codiconStyles.js';
  13. import { layout } from '../contextview/contextview.js';
  14. import { DomScrollableElement } from '../scrollbar/scrollableElement.js';
  15. import { EmptySubmenuAction, Separator, SubmenuAction } from '../../../common/actions.js';
  16. import { RunOnceScheduler } from '../../../common/async.js';
  17. import { Codicon } from '../../../common/codicons.js';
  18. import { stripIcons } from '../../../common/iconLabels.js';
  19. import { DisposableStore } from '../../../common/lifecycle.js';
  20. import { isLinux, isMacintosh } from '../../../common/platform.js';
  21. import * as strings from '../../../common/strings.js';
  22. export const MENU_MNEMONIC_REGEX = /\(&([^\s&])\)|(^|[^&])&([^\s&])/;
  23. export const MENU_ESCAPED_MNEMONIC_REGEX = /(&)?(&)([^\s&])/g;
  24. export var Direction;
  25. (function (Direction) {
  26. Direction[Direction["Right"] = 0] = "Right";
  27. Direction[Direction["Left"] = 1] = "Left";
  28. })(Direction || (Direction = {}));
  29. export class Menu extends ActionBar {
  30. constructor(container, actions, options = {}) {
  31. container.classList.add('monaco-menu-container');
  32. container.setAttribute('role', 'presentation');
  33. const menuElement = document.createElement('div');
  34. menuElement.classList.add('monaco-menu');
  35. menuElement.setAttribute('role', 'presentation');
  36. super(menuElement, {
  37. orientation: 1 /* ActionsOrientation.VERTICAL */,
  38. actionViewItemProvider: action => this.doGetActionViewItem(action, options, parentData),
  39. context: options.context,
  40. actionRunner: options.actionRunner,
  41. ariaLabel: options.ariaLabel,
  42. ariaRole: 'menu',
  43. focusOnlyEnabledItems: true,
  44. triggerKeys: { keys: [3 /* KeyCode.Enter */, ...(isMacintosh || isLinux ? [10 /* KeyCode.Space */] : [])], keyDown: true }
  45. });
  46. this.menuElement = menuElement;
  47. this.actionsList.tabIndex = 0;
  48. this.menuDisposables = this._register(new DisposableStore());
  49. this.initializeOrUpdateStyleSheet(container, {});
  50. this._register(Gesture.addTarget(menuElement));
  51. addDisposableListener(menuElement, EventType.KEY_DOWN, (e) => {
  52. const event = new StandardKeyboardEvent(e);
  53. // Stop tab navigation of menus
  54. if (event.equals(2 /* KeyCode.Tab */)) {
  55. e.preventDefault();
  56. }
  57. });
  58. if (options.enableMnemonics) {
  59. this.menuDisposables.add(addDisposableListener(menuElement, EventType.KEY_DOWN, (e) => {
  60. const key = e.key.toLocaleLowerCase();
  61. if (this.mnemonics.has(key)) {
  62. EventHelper.stop(e, true);
  63. const actions = this.mnemonics.get(key);
  64. if (actions.length === 1) {
  65. if (actions[0] instanceof SubmenuMenuActionViewItem && actions[0].container) {
  66. this.focusItemByElement(actions[0].container);
  67. }
  68. actions[0].onClick(e);
  69. }
  70. if (actions.length > 1) {
  71. const action = actions.shift();
  72. if (action && action.container) {
  73. this.focusItemByElement(action.container);
  74. actions.push(action);
  75. }
  76. this.mnemonics.set(key, actions);
  77. }
  78. }
  79. }));
  80. }
  81. if (isLinux) {
  82. this._register(addDisposableListener(menuElement, EventType.KEY_DOWN, e => {
  83. const event = new StandardKeyboardEvent(e);
  84. if (event.equals(14 /* KeyCode.Home */) || event.equals(11 /* KeyCode.PageUp */)) {
  85. this.focusedItem = this.viewItems.length - 1;
  86. this.focusNext();
  87. EventHelper.stop(e, true);
  88. }
  89. else if (event.equals(13 /* KeyCode.End */) || event.equals(12 /* KeyCode.PageDown */)) {
  90. this.focusedItem = 0;
  91. this.focusPrevious();
  92. EventHelper.stop(e, true);
  93. }
  94. }));
  95. }
  96. this._register(addDisposableListener(this.domNode, EventType.MOUSE_OUT, e => {
  97. const relatedTarget = e.relatedTarget;
  98. if (!isAncestor(relatedTarget, this.domNode)) {
  99. this.focusedItem = undefined;
  100. this.updateFocus();
  101. e.stopPropagation();
  102. }
  103. }));
  104. this._register(addDisposableListener(this.actionsList, EventType.MOUSE_OVER, e => {
  105. let target = e.target;
  106. if (!target || !isAncestor(target, this.actionsList) || target === this.actionsList) {
  107. return;
  108. }
  109. while (target.parentElement !== this.actionsList && target.parentElement !== null) {
  110. target = target.parentElement;
  111. }
  112. if (target.classList.contains('action-item')) {
  113. const lastFocusedItem = this.focusedItem;
  114. this.setFocusedItem(target);
  115. if (lastFocusedItem !== this.focusedItem) {
  116. this.updateFocus();
  117. }
  118. }
  119. }));
  120. // Support touch on actions list to focus items (needed for submenus)
  121. this._register(Gesture.addTarget(this.actionsList));
  122. this._register(addDisposableListener(this.actionsList, TouchEventType.Tap, e => {
  123. let target = e.initialTarget;
  124. if (!target || !isAncestor(target, this.actionsList) || target === this.actionsList) {
  125. return;
  126. }
  127. while (target.parentElement !== this.actionsList && target.parentElement !== null) {
  128. target = target.parentElement;
  129. }
  130. if (target.classList.contains('action-item')) {
  131. const lastFocusedItem = this.focusedItem;
  132. this.setFocusedItem(target);
  133. if (lastFocusedItem !== this.focusedItem) {
  134. this.updateFocus();
  135. }
  136. }
  137. }));
  138. const parentData = {
  139. parent: this
  140. };
  141. this.mnemonics = new Map();
  142. // Scroll Logic
  143. this.scrollableElement = this._register(new DomScrollableElement(menuElement, {
  144. alwaysConsumeMouseWheel: true,
  145. horizontal: 2 /* ScrollbarVisibility.Hidden */,
  146. vertical: 3 /* ScrollbarVisibility.Visible */,
  147. verticalScrollbarSize: 7,
  148. handleMouseWheel: true,
  149. useShadows: true
  150. }));
  151. const scrollElement = this.scrollableElement.getDomNode();
  152. scrollElement.style.position = '';
  153. // Support scroll on menu drag
  154. this._register(addDisposableListener(menuElement, TouchEventType.Change, e => {
  155. EventHelper.stop(e, true);
  156. const scrollTop = this.scrollableElement.getScrollPosition().scrollTop;
  157. this.scrollableElement.setScrollPosition({ scrollTop: scrollTop - e.translationY });
  158. }));
  159. this._register(addDisposableListener(scrollElement, EventType.MOUSE_UP, e => {
  160. // Absorb clicks in menu dead space https://github.com/microsoft/vscode/issues/63575
  161. // We do this on the scroll element so the scroll bar doesn't dismiss the menu either
  162. e.preventDefault();
  163. }));
  164. menuElement.style.maxHeight = `${Math.max(10, window.innerHeight - container.getBoundingClientRect().top - 35)}px`;
  165. actions = actions.filter(a => {
  166. var _a;
  167. if ((_a = options.submenuIds) === null || _a === void 0 ? void 0 : _a.has(a.id)) {
  168. console.warn(`Found submenu cycle: ${a.id}`);
  169. return false;
  170. }
  171. return true;
  172. });
  173. this.push(actions, { icon: true, label: true, isMenu: true });
  174. container.appendChild(this.scrollableElement.getDomNode());
  175. this.scrollableElement.scanDomNode();
  176. this.viewItems.filter(item => !(item instanceof MenuSeparatorActionViewItem)).forEach((item, index, array) => {
  177. item.updatePositionInSet(index + 1, array.length);
  178. });
  179. }
  180. initializeOrUpdateStyleSheet(container, style) {
  181. if (!this.styleSheet) {
  182. if (isInShadowDOM(container)) {
  183. this.styleSheet = createStyleSheet(container);
  184. }
  185. else {
  186. if (!Menu.globalStyleSheet) {
  187. Menu.globalStyleSheet = createStyleSheet();
  188. }
  189. this.styleSheet = Menu.globalStyleSheet;
  190. }
  191. }
  192. this.styleSheet.textContent = getMenuWidgetCSS(style, isInShadowDOM(container));
  193. }
  194. style(style) {
  195. const container = this.getContainer();
  196. this.initializeOrUpdateStyleSheet(container, style);
  197. const fgColor = style.foregroundColor ? `${style.foregroundColor}` : '';
  198. const bgColor = style.backgroundColor ? `${style.backgroundColor}` : '';
  199. const border = style.borderColor ? `1px solid ${style.borderColor}` : '';
  200. const borderRadius = '5px';
  201. const shadow = style.shadowColor ? `0 2px 8px ${style.shadowColor}` : '';
  202. container.style.outline = border;
  203. container.style.borderRadius = borderRadius;
  204. container.style.color = fgColor;
  205. container.style.backgroundColor = bgColor;
  206. container.style.boxShadow = shadow;
  207. if (this.viewItems) {
  208. this.viewItems.forEach(item => {
  209. if (item instanceof BaseMenuActionViewItem || item instanceof MenuSeparatorActionViewItem) {
  210. item.style(style);
  211. }
  212. });
  213. }
  214. }
  215. getContainer() {
  216. return this.scrollableElement.getDomNode();
  217. }
  218. get onScroll() {
  219. return this.scrollableElement.onScroll;
  220. }
  221. focusItemByElement(element) {
  222. const lastFocusedItem = this.focusedItem;
  223. this.setFocusedItem(element);
  224. if (lastFocusedItem !== this.focusedItem) {
  225. this.updateFocus();
  226. }
  227. }
  228. setFocusedItem(element) {
  229. for (let i = 0; i < this.actionsList.children.length; i++) {
  230. const elem = this.actionsList.children[i];
  231. if (element === elem) {
  232. this.focusedItem = i;
  233. break;
  234. }
  235. }
  236. }
  237. updateFocus(fromRight) {
  238. super.updateFocus(fromRight, true, true);
  239. if (typeof this.focusedItem !== 'undefined') {
  240. // Workaround for #80047 caused by an issue in chromium
  241. // https://bugs.chromium.org/p/chromium/issues/detail?id=414283
  242. // When that's fixed, just call this.scrollableElement.scanDomNode()
  243. this.scrollableElement.setScrollPosition({
  244. scrollTop: Math.round(this.menuElement.scrollTop)
  245. });
  246. }
  247. }
  248. doGetActionViewItem(action, options, parentData) {
  249. if (action instanceof Separator) {
  250. return new MenuSeparatorActionViewItem(options.context, action, { icon: true });
  251. }
  252. else if (action instanceof SubmenuAction) {
  253. const menuActionViewItem = new SubmenuMenuActionViewItem(action, action.actions, parentData, Object.assign(Object.assign({}, options), { submenuIds: new Set([...(options.submenuIds || []), action.id]) }));
  254. if (options.enableMnemonics) {
  255. const mnemonic = menuActionViewItem.getMnemonic();
  256. if (mnemonic && menuActionViewItem.isEnabled()) {
  257. let actionViewItems = [];
  258. if (this.mnemonics.has(mnemonic)) {
  259. actionViewItems = this.mnemonics.get(mnemonic);
  260. }
  261. actionViewItems.push(menuActionViewItem);
  262. this.mnemonics.set(mnemonic, actionViewItems);
  263. }
  264. }
  265. return menuActionViewItem;
  266. }
  267. else {
  268. const menuItemOptions = { enableMnemonics: options.enableMnemonics, useEventAsContext: options.useEventAsContext };
  269. if (options.getKeyBinding) {
  270. const keybinding = options.getKeyBinding(action);
  271. if (keybinding) {
  272. const keybindingLabel = keybinding.getLabel();
  273. if (keybindingLabel) {
  274. menuItemOptions.keybinding = keybindingLabel;
  275. }
  276. }
  277. }
  278. const menuActionViewItem = new BaseMenuActionViewItem(options.context, action, menuItemOptions);
  279. if (options.enableMnemonics) {
  280. const mnemonic = menuActionViewItem.getMnemonic();
  281. if (mnemonic && menuActionViewItem.isEnabled()) {
  282. let actionViewItems = [];
  283. if (this.mnemonics.has(mnemonic)) {
  284. actionViewItems = this.mnemonics.get(mnemonic);
  285. }
  286. actionViewItems.push(menuActionViewItem);
  287. this.mnemonics.set(mnemonic, actionViewItems);
  288. }
  289. }
  290. return menuActionViewItem;
  291. }
  292. }
  293. }
  294. class BaseMenuActionViewItem extends BaseActionViewItem {
  295. constructor(ctx, action, options = {}) {
  296. options.isMenu = true;
  297. super(action, action, options);
  298. this.options = options;
  299. this.options.icon = options.icon !== undefined ? options.icon : false;
  300. this.options.label = options.label !== undefined ? options.label : true;
  301. this.cssClass = '';
  302. // Set mnemonic
  303. if (this.options.label && options.enableMnemonics) {
  304. const label = this.getAction().label;
  305. if (label) {
  306. const matches = MENU_MNEMONIC_REGEX.exec(label);
  307. if (matches) {
  308. this.mnemonic = (!!matches[1] ? matches[1] : matches[3]).toLocaleLowerCase();
  309. }
  310. }
  311. }
  312. // Add mouse up listener later to avoid accidental clicks
  313. this.runOnceToEnableMouseUp = new RunOnceScheduler(() => {
  314. if (!this.element) {
  315. return;
  316. }
  317. this._register(addDisposableListener(this.element, EventType.MOUSE_UP, e => {
  318. // removed default prevention as it conflicts
  319. // with BaseActionViewItem #101537
  320. // add back if issues arise and link new issue
  321. EventHelper.stop(e, true);
  322. // See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard
  323. // > Writing to the clipboard
  324. // > You can use the "cut" and "copy" commands without any special
  325. // permission if you are using them in a short-lived event handler
  326. // for a user action (for example, a click handler).
  327. // => to get the Copy and Paste context menu actions working on Firefox,
  328. // there should be no timeout here
  329. if (isFirefox) {
  330. const mouseEvent = new StandardMouseEvent(e);
  331. // Allowing right click to trigger the event causes the issue described below,
  332. // but since the solution below does not work in FF, we must disable right click
  333. if (mouseEvent.rightButton) {
  334. return;
  335. }
  336. this.onClick(e);
  337. }
  338. // In all other cases, set timeout to allow context menu cancellation to trigger
  339. // otherwise the action will destroy the menu and a second context menu
  340. // will still trigger for right click.
  341. else {
  342. setTimeout(() => {
  343. this.onClick(e);
  344. }, 0);
  345. }
  346. }));
  347. this._register(addDisposableListener(this.element, EventType.CONTEXT_MENU, e => {
  348. EventHelper.stop(e, true);
  349. }));
  350. }, 100);
  351. this._register(this.runOnceToEnableMouseUp);
  352. }
  353. render(container) {
  354. super.render(container);
  355. if (!this.element) {
  356. return;
  357. }
  358. this.container = container;
  359. this.item = append(this.element, $('a.action-menu-item'));
  360. if (this._action.id === Separator.ID) {
  361. // A separator is a presentation item
  362. this.item.setAttribute('role', 'presentation');
  363. }
  364. else {
  365. this.item.setAttribute('role', 'menuitem');
  366. if (this.mnemonic) {
  367. this.item.setAttribute('aria-keyshortcuts', `${this.mnemonic}`);
  368. }
  369. }
  370. this.check = append(this.item, $('span.menu-item-check' + Codicon.menuSelection.cssSelector));
  371. this.check.setAttribute('role', 'none');
  372. this.label = append(this.item, $('span.action-label'));
  373. if (this.options.label && this.options.keybinding) {
  374. append(this.item, $('span.keybinding')).textContent = this.options.keybinding;
  375. }
  376. // Adds mouse up listener to actually run the action
  377. this.runOnceToEnableMouseUp.schedule();
  378. this.updateClass();
  379. this.updateLabel();
  380. this.updateTooltip();
  381. this.updateEnabled();
  382. this.updateChecked();
  383. }
  384. blur() {
  385. super.blur();
  386. this.applyStyle();
  387. }
  388. focus() {
  389. super.focus();
  390. if (this.item) {
  391. this.item.focus();
  392. }
  393. this.applyStyle();
  394. }
  395. updatePositionInSet(pos, setSize) {
  396. if (this.item) {
  397. this.item.setAttribute('aria-posinset', `${pos}`);
  398. this.item.setAttribute('aria-setsize', `${setSize}`);
  399. }
  400. }
  401. updateLabel() {
  402. var _a;
  403. if (!this.label) {
  404. return;
  405. }
  406. if (this.options.label) {
  407. clearNode(this.label);
  408. let label = stripIcons(this.getAction().label);
  409. if (label) {
  410. const cleanLabel = cleanMnemonic(label);
  411. if (!this.options.enableMnemonics) {
  412. label = cleanLabel;
  413. }
  414. this.label.setAttribute('aria-label', cleanLabel.replace(/&&/g, '&'));
  415. const matches = MENU_MNEMONIC_REGEX.exec(label);
  416. if (matches) {
  417. label = strings.escape(label);
  418. // This is global, reset it
  419. MENU_ESCAPED_MNEMONIC_REGEX.lastIndex = 0;
  420. let escMatch = MENU_ESCAPED_MNEMONIC_REGEX.exec(label);
  421. // We can't use negative lookbehind so if we match our negative and skip
  422. while (escMatch && escMatch[1]) {
  423. escMatch = MENU_ESCAPED_MNEMONIC_REGEX.exec(label);
  424. }
  425. const replaceDoubleEscapes = (str) => str.replace(/&amp;&amp;/g, '&amp;');
  426. if (escMatch) {
  427. 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)), ' '));
  428. }
  429. else {
  430. this.label.innerText = replaceDoubleEscapes(label).trim();
  431. }
  432. (_a = this.item) === null || _a === void 0 ? void 0 : _a.setAttribute('aria-keyshortcuts', (!!matches[1] ? matches[1] : matches[3]).toLocaleLowerCase());
  433. }
  434. else {
  435. this.label.innerText = label.replace(/&&/g, '&').trim();
  436. }
  437. }
  438. }
  439. }
  440. updateTooltip() {
  441. // menus should function like native menus and they do not have tooltips
  442. }
  443. updateClass() {
  444. if (this.cssClass && this.item) {
  445. this.item.classList.remove(...this.cssClass.split(' '));
  446. }
  447. if (this.options.icon && this.label) {
  448. this.cssClass = this.getAction().class || '';
  449. this.label.classList.add('icon');
  450. if (this.cssClass) {
  451. this.label.classList.add(...this.cssClass.split(' '));
  452. }
  453. this.updateEnabled();
  454. }
  455. else if (this.label) {
  456. this.label.classList.remove('icon');
  457. }
  458. }
  459. updateEnabled() {
  460. if (this.getAction().enabled) {
  461. if (this.element) {
  462. this.element.classList.remove('disabled');
  463. this.element.removeAttribute('aria-disabled');
  464. }
  465. if (this.item) {
  466. this.item.classList.remove('disabled');
  467. this.item.removeAttribute('aria-disabled');
  468. this.item.tabIndex = 0;
  469. }
  470. }
  471. else {
  472. if (this.element) {
  473. this.element.classList.add('disabled');
  474. this.element.setAttribute('aria-disabled', 'true');
  475. }
  476. if (this.item) {
  477. this.item.classList.add('disabled');
  478. this.item.setAttribute('aria-disabled', 'true');
  479. }
  480. }
  481. }
  482. updateChecked() {
  483. if (!this.item) {
  484. return;
  485. }
  486. const checked = this.getAction().checked;
  487. this.item.classList.toggle('checked', !!checked);
  488. if (checked !== undefined) {
  489. this.item.setAttribute('role', 'menuitemcheckbox');
  490. this.item.setAttribute('aria-checked', checked ? 'true' : 'false');
  491. }
  492. else {
  493. this.item.setAttribute('role', 'menuitem');
  494. this.item.setAttribute('aria-checked', '');
  495. }
  496. }
  497. getMnemonic() {
  498. return this.mnemonic;
  499. }
  500. applyStyle() {
  501. if (!this.menuStyle) {
  502. return;
  503. }
  504. const isSelected = this.element && this.element.classList.contains('focused');
  505. const fgColor = isSelected && this.menuStyle.selectionForegroundColor ? this.menuStyle.selectionForegroundColor : this.menuStyle.foregroundColor;
  506. const bgColor = isSelected && this.menuStyle.selectionBackgroundColor ? this.menuStyle.selectionBackgroundColor : undefined;
  507. const outline = isSelected && this.menuStyle.selectionBorderColor ? `1px solid ${this.menuStyle.selectionBorderColor}` : '';
  508. const outlineOffset = isSelected && this.menuStyle.selectionBorderColor ? `-1px` : '';
  509. if (this.item) {
  510. this.item.style.color = fgColor ? fgColor.toString() : '';
  511. this.item.style.backgroundColor = bgColor ? bgColor.toString() : '';
  512. this.item.style.outline = outline;
  513. this.item.style.outlineOffset = outlineOffset;
  514. }
  515. if (this.check) {
  516. this.check.style.color = fgColor ? fgColor.toString() : '';
  517. }
  518. }
  519. style(style) {
  520. this.menuStyle = style;
  521. this.applyStyle();
  522. }
  523. }
  524. class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
  525. constructor(action, submenuActions, parentData, submenuOptions) {
  526. super(action, action, submenuOptions);
  527. this.submenuActions = submenuActions;
  528. this.parentData = parentData;
  529. this.submenuOptions = submenuOptions;
  530. this.mysubmenu = null;
  531. this.submenuDisposables = this._register(new DisposableStore());
  532. this.mouseOver = false;
  533. this.expandDirection = submenuOptions && submenuOptions.expandDirection !== undefined ? submenuOptions.expandDirection : Direction.Right;
  534. this.showScheduler = new RunOnceScheduler(() => {
  535. if (this.mouseOver) {
  536. this.cleanupExistingSubmenu(false);
  537. this.createSubmenu(false);
  538. }
  539. }, 250);
  540. this.hideScheduler = new RunOnceScheduler(() => {
  541. if (this.element && (!isAncestor(getActiveElement(), this.element) && this.parentData.submenu === this.mysubmenu)) {
  542. this.parentData.parent.focus(false);
  543. this.cleanupExistingSubmenu(true);
  544. }
  545. }, 750);
  546. }
  547. render(container) {
  548. super.render(container);
  549. if (!this.element) {
  550. return;
  551. }
  552. if (this.item) {
  553. this.item.classList.add('monaco-submenu-item');
  554. this.item.tabIndex = 0;
  555. this.item.setAttribute('aria-haspopup', 'true');
  556. this.updateAriaExpanded('false');
  557. this.submenuIndicator = append(this.item, $('span.submenu-indicator' + Codicon.menuSubmenu.cssSelector));
  558. this.submenuIndicator.setAttribute('aria-hidden', 'true');
  559. }
  560. this._register(addDisposableListener(this.element, EventType.KEY_UP, e => {
  561. const event = new StandardKeyboardEvent(e);
  562. if (event.equals(17 /* KeyCode.RightArrow */) || event.equals(3 /* KeyCode.Enter */)) {
  563. EventHelper.stop(e, true);
  564. this.createSubmenu(true);
  565. }
  566. }));
  567. this._register(addDisposableListener(this.element, EventType.KEY_DOWN, e => {
  568. const event = new StandardKeyboardEvent(e);
  569. if (getActiveElement() === this.item) {
  570. if (event.equals(17 /* KeyCode.RightArrow */) || event.equals(3 /* KeyCode.Enter */)) {
  571. EventHelper.stop(e, true);
  572. }
  573. }
  574. }));
  575. this._register(addDisposableListener(this.element, EventType.MOUSE_OVER, e => {
  576. if (!this.mouseOver) {
  577. this.mouseOver = true;
  578. this.showScheduler.schedule();
  579. }
  580. }));
  581. this._register(addDisposableListener(this.element, EventType.MOUSE_LEAVE, e => {
  582. this.mouseOver = false;
  583. }));
  584. this._register(addDisposableListener(this.element, EventType.FOCUS_OUT, e => {
  585. if (this.element && !isAncestor(getActiveElement(), this.element)) {
  586. this.hideScheduler.schedule();
  587. }
  588. }));
  589. this._register(this.parentData.parent.onScroll(() => {
  590. if (this.parentData.submenu === this.mysubmenu) {
  591. this.parentData.parent.focus(false);
  592. this.cleanupExistingSubmenu(true);
  593. }
  594. }));
  595. }
  596. updateEnabled() {
  597. // override on submenu entry
  598. // native menus do not observe enablement on sumbenus
  599. // we mimic that behavior
  600. }
  601. onClick(e) {
  602. // stop clicking from trying to run an action
  603. EventHelper.stop(e, true);
  604. this.cleanupExistingSubmenu(false);
  605. this.createSubmenu(true);
  606. }
  607. cleanupExistingSubmenu(force) {
  608. if (this.parentData.submenu && (force || (this.parentData.submenu !== this.mysubmenu))) {
  609. // disposal may throw if the submenu has already been removed
  610. try {
  611. this.parentData.submenu.dispose();
  612. }
  613. catch (_a) { }
  614. this.parentData.submenu = undefined;
  615. this.updateAriaExpanded('false');
  616. if (this.submenuContainer) {
  617. this.submenuDisposables.clear();
  618. this.submenuContainer = undefined;
  619. }
  620. }
  621. }
  622. calculateSubmenuMenuLayout(windowDimensions, submenu, entry, expandDirection) {
  623. const ret = { top: 0, left: 0 };
  624. // Start with horizontal
  625. ret.left = layout(windowDimensions.width, submenu.width, { position: expandDirection === Direction.Right ? 0 /* LayoutAnchorPosition.Before */ : 1 /* LayoutAnchorPosition.After */, offset: entry.left, size: entry.width });
  626. // We don't have enough room to layout the menu fully, so we are overlapping the menu
  627. if (ret.left >= entry.left && ret.left < entry.left + entry.width) {
  628. if (entry.left + 10 + submenu.width <= windowDimensions.width) {
  629. ret.left = entry.left + 10;
  630. }
  631. entry.top += 10;
  632. entry.height = 0;
  633. }
  634. // Now that we have a horizontal position, try layout vertically
  635. ret.top = layout(windowDimensions.height, submenu.height, { position: 0 /* LayoutAnchorPosition.Before */, offset: entry.top, size: 0 });
  636. // We didn't have enough room below, but we did above, so we shift down to align the menu
  637. if (ret.top + submenu.height === entry.top && ret.top + entry.height + submenu.height <= windowDimensions.height) {
  638. ret.top += entry.height;
  639. }
  640. return ret;
  641. }
  642. createSubmenu(selectFirstItem = true) {
  643. if (!this.element) {
  644. return;
  645. }
  646. if (!this.parentData.submenu) {
  647. this.updateAriaExpanded('true');
  648. this.submenuContainer = append(this.element, $('div.monaco-submenu'));
  649. this.submenuContainer.classList.add('menubar-menu-items-holder', 'context-view');
  650. // Set the top value of the menu container before construction
  651. // This allows the menu constructor to calculate the proper max height
  652. const computedStyles = getComputedStyle(this.parentData.parent.domNode);
  653. const paddingTop = parseFloat(computedStyles.paddingTop || '0') || 0;
  654. // this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset - paddingTop}px`;
  655. this.submenuContainer.style.zIndex = '1';
  656. this.submenuContainer.style.position = 'fixed';
  657. this.submenuContainer.style.top = '0';
  658. this.submenuContainer.style.left = '0';
  659. this.parentData.submenu = new Menu(this.submenuContainer, this.submenuActions.length ? this.submenuActions : [new EmptySubmenuAction()], this.submenuOptions);
  660. if (this.menuStyle) {
  661. this.parentData.submenu.style(this.menuStyle);
  662. }
  663. // layout submenu
  664. const entryBox = this.element.getBoundingClientRect();
  665. const entryBoxUpdated = {
  666. top: entryBox.top - paddingTop,
  667. left: entryBox.left,
  668. height: entryBox.height + 2 * paddingTop,
  669. width: entryBox.width
  670. };
  671. const viewBox = this.submenuContainer.getBoundingClientRect();
  672. const { top, left } = this.calculateSubmenuMenuLayout(new Dimension(window.innerWidth, window.innerHeight), Dimension.lift(viewBox), entryBoxUpdated, this.expandDirection);
  673. // subtract offsets caused by transform parent
  674. this.submenuContainer.style.left = `${left - viewBox.left}px`;
  675. this.submenuContainer.style.top = `${top - viewBox.top}px`;
  676. this.submenuDisposables.add(addDisposableListener(this.submenuContainer, EventType.KEY_UP, e => {
  677. const event = new StandardKeyboardEvent(e);
  678. if (event.equals(15 /* KeyCode.LeftArrow */)) {
  679. EventHelper.stop(e, true);
  680. this.parentData.parent.focus();
  681. this.cleanupExistingSubmenu(true);
  682. }
  683. }));
  684. this.submenuDisposables.add(addDisposableListener(this.submenuContainer, EventType.KEY_DOWN, e => {
  685. const event = new StandardKeyboardEvent(e);
  686. if (event.equals(15 /* KeyCode.LeftArrow */)) {
  687. EventHelper.stop(e, true);
  688. }
  689. }));
  690. this.submenuDisposables.add(this.parentData.submenu.onDidCancel(() => {
  691. this.parentData.parent.focus();
  692. this.cleanupExistingSubmenu(true);
  693. }));
  694. this.parentData.submenu.focus(selectFirstItem);
  695. this.mysubmenu = this.parentData.submenu;
  696. }
  697. else {
  698. this.parentData.submenu.focus(false);
  699. }
  700. }
  701. updateAriaExpanded(value) {
  702. var _a;
  703. if (this.item) {
  704. (_a = this.item) === null || _a === void 0 ? void 0 : _a.setAttribute('aria-expanded', value);
  705. }
  706. }
  707. applyStyle() {
  708. var _a;
  709. super.applyStyle();
  710. if (!this.menuStyle) {
  711. return;
  712. }
  713. const isSelected = this.element && this.element.classList.contains('focused');
  714. const fgColor = isSelected && this.menuStyle.selectionForegroundColor ? this.menuStyle.selectionForegroundColor : this.menuStyle.foregroundColor;
  715. if (this.submenuIndicator) {
  716. this.submenuIndicator.style.color = fgColor ? `${fgColor}` : '';
  717. }
  718. (_a = this.parentData.submenu) === null || _a === void 0 ? void 0 : _a.style(this.menuStyle);
  719. }
  720. dispose() {
  721. super.dispose();
  722. this.hideScheduler.dispose();
  723. if (this.mysubmenu) {
  724. this.mysubmenu.dispose();
  725. this.mysubmenu = null;
  726. }
  727. if (this.submenuContainer) {
  728. this.submenuContainer = undefined;
  729. }
  730. }
  731. }
  732. class MenuSeparatorActionViewItem extends ActionViewItem {
  733. style(style) {
  734. if (this.label) {
  735. this.label.style.borderBottomColor = style.separatorColor ? `${style.separatorColor}` : '';
  736. }
  737. }
  738. }
  739. export function cleanMnemonic(label) {
  740. const regex = MENU_MNEMONIC_REGEX;
  741. const matches = regex.exec(label);
  742. if (!matches) {
  743. return label;
  744. }
  745. const mnemonicInText = !matches[1];
  746. return label.replace(regex, mnemonicInText ? '$2$3' : '').trim();
  747. }
  748. function getMenuWidgetCSS(style, isForShadowDom) {
  749. let result = /* css */ `
  750. .monaco-menu {
  751. font-size: 13px;
  752. border-radius: 5px;
  753. min-width: 160px;
  754. }
  755. ${formatRule(Codicon.menuSelection)}
  756. ${formatRule(Codicon.menuSubmenu)}
  757. .monaco-menu .monaco-action-bar {
  758. text-align: right;
  759. overflow: hidden;
  760. white-space: nowrap;
  761. }
  762. .monaco-menu .monaco-action-bar .actions-container {
  763. display: flex;
  764. margin: 0 auto;
  765. padding: 0;
  766. width: 100%;
  767. justify-content: flex-end;
  768. }
  769. .monaco-menu .monaco-action-bar.vertical .actions-container {
  770. display: inline-block;
  771. }
  772. .monaco-menu .monaco-action-bar.reverse .actions-container {
  773. flex-direction: row-reverse;
  774. }
  775. .monaco-menu .monaco-action-bar .action-item {
  776. cursor: pointer;
  777. display: inline-block;
  778. transition: transform 50ms ease;
  779. position: relative; /* DO NOT REMOVE - this is the key to preventing the ghosting icon bug in Chrome 42 */
  780. }
  781. .monaco-menu .monaco-action-bar .action-item.disabled {
  782. cursor: default;
  783. }
  784. .monaco-menu .monaco-action-bar.animated .action-item.active {
  785. transform: scale(1.272019649, 1.272019649); /* 1.272019649 = √φ */
  786. }
  787. .monaco-menu .monaco-action-bar .action-item .icon,
  788. .monaco-menu .monaco-action-bar .action-item .codicon {
  789. display: inline-block;
  790. }
  791. .monaco-menu .monaco-action-bar .action-item .codicon {
  792. display: flex;
  793. align-items: center;
  794. }
  795. .monaco-menu .monaco-action-bar .action-label {
  796. font-size: 11px;
  797. margin-right: 4px;
  798. }
  799. .monaco-menu .monaco-action-bar .action-item.disabled .action-label,
  800. .monaco-menu .monaco-action-bar .action-item.disabled .action-label:hover {
  801. color: var(--vscode-disabledForeground);
  802. }
  803. /* Vertical actions */
  804. .monaco-menu .monaco-action-bar.vertical {
  805. text-align: left;
  806. }
  807. .monaco-menu .monaco-action-bar.vertical .action-item {
  808. display: block;
  809. }
  810. .monaco-menu .monaco-action-bar.vertical .action-label.separator {
  811. display: block;
  812. border-bottom: 1px solid var(--vscode-menu-separatorBackground);
  813. padding-top: 1px;
  814. padding: 30px;
  815. }
  816. .monaco-menu .secondary-actions .monaco-action-bar .action-label {
  817. margin-left: 6px;
  818. }
  819. /* Action Items */
  820. .monaco-menu .monaco-action-bar .action-item.select-container {
  821. overflow: hidden; /* somehow the dropdown overflows its container, we prevent it here to not push */
  822. flex: 1;
  823. max-width: 170px;
  824. min-width: 60px;
  825. display: flex;
  826. align-items: center;
  827. justify-content: center;
  828. margin-right: 10px;
  829. }
  830. .monaco-menu .monaco-action-bar.vertical {
  831. margin-left: 0;
  832. overflow: visible;
  833. }
  834. .monaco-menu .monaco-action-bar.vertical .actions-container {
  835. display: block;
  836. }
  837. .monaco-menu .monaco-action-bar.vertical .action-item {
  838. padding: 0;
  839. transform: none;
  840. display: flex;
  841. }
  842. .monaco-menu .monaco-action-bar.vertical .action-item.active {
  843. transform: none;
  844. }
  845. .monaco-menu .monaco-action-bar.vertical .action-menu-item {
  846. flex: 1 1 auto;
  847. display: flex;
  848. height: 2em;
  849. align-items: center;
  850. position: relative;
  851. }
  852. .monaco-menu .monaco-action-bar.vertical .action-menu-item:hover .keybinding,
  853. .monaco-menu .monaco-action-bar.vertical .action-menu-item:focus .keybinding {
  854. opacity: unset;
  855. }
  856. .monaco-menu .monaco-action-bar.vertical .action-label {
  857. flex: 1 1 auto;
  858. text-decoration: none;
  859. padding: 0 1em;
  860. background: none;
  861. font-size: 12px;
  862. line-height: 1;
  863. }
  864. .monaco-menu .monaco-action-bar.vertical .keybinding,
  865. .monaco-menu .monaco-action-bar.vertical .submenu-indicator {
  866. display: inline-block;
  867. flex: 2 1 auto;
  868. padding: 0 1em;
  869. text-align: right;
  870. font-size: 12px;
  871. line-height: 1;
  872. }
  873. .monaco-menu .monaco-action-bar.vertical .submenu-indicator {
  874. height: 100%;
  875. }
  876. .monaco-menu .monaco-action-bar.vertical .submenu-indicator.codicon {
  877. font-size: 16px !important;
  878. display: flex;
  879. align-items: center;
  880. }
  881. .monaco-menu .monaco-action-bar.vertical .submenu-indicator.codicon::before {
  882. margin-left: auto;
  883. margin-right: -20px;
  884. }
  885. .monaco-menu .monaco-action-bar.vertical .action-item.disabled .keybinding,
  886. .monaco-menu .monaco-action-bar.vertical .action-item.disabled .submenu-indicator {
  887. opacity: 0.4;
  888. }
  889. .monaco-menu .monaco-action-bar.vertical .action-label:not(.separator) {
  890. display: inline-block;
  891. box-sizing: border-box;
  892. margin: 0;
  893. }
  894. .monaco-menu .monaco-action-bar.vertical .action-item {
  895. position: static;
  896. overflow: visible;
  897. }
  898. .monaco-menu .monaco-action-bar.vertical .action-item .monaco-submenu {
  899. position: absolute;
  900. }
  901. .monaco-menu .monaco-action-bar.vertical .action-label.separator {
  902. width: 100%;
  903. height: 0px !important;
  904. opacity: 1;
  905. }
  906. .monaco-menu .monaco-action-bar.vertical .action-label.separator.text {
  907. padding: 0.7em 1em 0.1em 1em;
  908. font-weight: bold;
  909. opacity: 1;
  910. }
  911. .monaco-menu .monaco-action-bar.vertical .action-label:hover {
  912. color: inherit;
  913. }
  914. .monaco-menu .monaco-action-bar.vertical .menu-item-check {
  915. position: absolute;
  916. visibility: hidden;
  917. width: 1em;
  918. height: 100%;
  919. }
  920. .monaco-menu .monaco-action-bar.vertical .action-menu-item.checked .menu-item-check {
  921. visibility: visible;
  922. display: flex;
  923. align-items: center;
  924. justify-content: center;
  925. }
  926. /* Context Menu */
  927. .context-view.monaco-menu-container {
  928. outline: 0;
  929. border: none;
  930. animation: fadeIn 0.083s linear;
  931. -webkit-app-region: no-drag;
  932. }
  933. .context-view.monaco-menu-container :focus,
  934. .context-view.monaco-menu-container .monaco-action-bar.vertical:focus,
  935. .context-view.monaco-menu-container .monaco-action-bar.vertical :focus {
  936. outline: 0;
  937. }
  938. .hc-black .context-view.monaco-menu-container,
  939. .hc-light .context-view.monaco-menu-container,
  940. :host-context(.hc-black) .context-view.monaco-menu-container,
  941. :host-context(.hc-light) .context-view.monaco-menu-container {
  942. box-shadow: none;
  943. }
  944. .hc-black .monaco-menu .monaco-action-bar.vertical .action-item.focused,
  945. .hc-light .monaco-menu .monaco-action-bar.vertical .action-item.focused,
  946. :host-context(.hc-black) .monaco-menu .monaco-action-bar.vertical .action-item.focused,
  947. :host-context(.hc-light) .monaco-menu .monaco-action-bar.vertical .action-item.focused {
  948. background: none;
  949. }
  950. /* Vertical Action Bar Styles */
  951. .monaco-menu .monaco-action-bar.vertical {
  952. padding: .6em 0;
  953. }
  954. .monaco-menu .monaco-action-bar.vertical .action-menu-item {
  955. height: 2em;
  956. }
  957. .monaco-menu .monaco-action-bar.vertical .action-label:not(.separator),
  958. .monaco-menu .monaco-action-bar.vertical .keybinding {
  959. font-size: inherit;
  960. padding: 0 2em;
  961. }
  962. .monaco-menu .monaco-action-bar.vertical .menu-item-check {
  963. font-size: inherit;
  964. width: 2em;
  965. }
  966. .monaco-menu .monaco-action-bar.vertical .action-label.separator {
  967. font-size: inherit;
  968. margin: 5px 0 !important;
  969. padding: 0;
  970. border-radius: 0;
  971. }
  972. .linux .monaco-menu .monaco-action-bar.vertical .action-label.separator,
  973. :host-context(.linux) .monaco-menu .monaco-action-bar.vertical .action-label.separator {
  974. margin-left: 0;
  975. margin-right: 0;
  976. }
  977. .monaco-menu .monaco-action-bar.vertical .submenu-indicator {
  978. font-size: 60%;
  979. padding: 0 1.8em;
  980. }
  981. .linux .monaco-menu .monaco-action-bar.vertical .submenu-indicator {
  982. :host-context(.linux) .monaco-menu .monaco-action-bar.vertical .submenu-indicator {
  983. height: 100%;
  984. mask-size: 10px 10px;
  985. -webkit-mask-size: 10px 10px;
  986. }
  987. .monaco-menu .action-item {
  988. cursor: default;
  989. }`;
  990. if (isForShadowDom) {
  991. // Only define scrollbar styles when used inside shadow dom,
  992. // otherwise leave their styling to the global workbench styling.
  993. result += `
  994. /* Arrows */
  995. .monaco-scrollable-element > .scrollbar > .scra {
  996. cursor: pointer;
  997. font-size: 11px !important;
  998. }
  999. .monaco-scrollable-element > .visible {
  1000. opacity: 1;
  1001. /* Background rule added for IE9 - to allow clicks on dom node */
  1002. background:rgba(0,0,0,0);
  1003. transition: opacity 100ms linear;
  1004. }
  1005. .monaco-scrollable-element > .invisible {
  1006. opacity: 0;
  1007. pointer-events: none;
  1008. }
  1009. .monaco-scrollable-element > .invisible.fade {
  1010. transition: opacity 800ms linear;
  1011. }
  1012. /* Scrollable Content Inset Shadow */
  1013. .monaco-scrollable-element > .shadow {
  1014. position: absolute;
  1015. display: none;
  1016. }
  1017. .monaco-scrollable-element > .shadow.top {
  1018. display: block;
  1019. top: 0;
  1020. left: 3px;
  1021. height: 3px;
  1022. width: 100%;
  1023. }
  1024. .monaco-scrollable-element > .shadow.left {
  1025. display: block;
  1026. top: 3px;
  1027. left: 0;
  1028. height: 100%;
  1029. width: 3px;
  1030. }
  1031. .monaco-scrollable-element > .shadow.top-left-corner {
  1032. display: block;
  1033. top: 0;
  1034. left: 0;
  1035. height: 3px;
  1036. width: 3px;
  1037. }
  1038. `;
  1039. // Scrollbars
  1040. const scrollbarShadowColor = style.scrollbarShadow;
  1041. if (scrollbarShadowColor) {
  1042. result += `
  1043. .monaco-scrollable-element > .shadow.top {
  1044. box-shadow: ${scrollbarShadowColor} 0 6px 6px -6px inset;
  1045. }
  1046. .monaco-scrollable-element > .shadow.left {
  1047. box-shadow: ${scrollbarShadowColor} 6px 0 6px -6px inset;
  1048. }
  1049. .monaco-scrollable-element > .shadow.top.left {
  1050. box-shadow: ${scrollbarShadowColor} 6px 6px 6px -6px inset;
  1051. }
  1052. `;
  1053. }
  1054. const scrollbarSliderBackgroundColor = style.scrollbarSliderBackground;
  1055. if (scrollbarSliderBackgroundColor) {
  1056. result += `
  1057. .monaco-scrollable-element > .scrollbar > .slider {
  1058. background: ${scrollbarSliderBackgroundColor};
  1059. }
  1060. `;
  1061. }
  1062. const scrollbarSliderHoverBackgroundColor = style.scrollbarSliderHoverBackground;
  1063. if (scrollbarSliderHoverBackgroundColor) {
  1064. result += `
  1065. .monaco-scrollable-element > .scrollbar > .slider:hover {
  1066. background: ${scrollbarSliderHoverBackgroundColor};
  1067. }
  1068. `;
  1069. }
  1070. const scrollbarSliderActiveBackgroundColor = style.scrollbarSliderActiveBackground;
  1071. if (scrollbarSliderActiveBackgroundColor) {
  1072. result += `
  1073. .monaco-scrollable-element > .scrollbar > .slider.active {
  1074. background: ${scrollbarSliderActiveBackgroundColor};
  1075. }
  1076. `;
  1077. }
  1078. }
  1079. return result;
  1080. }