contextMenuHandler.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  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 { $, addDisposableListener, EventType, isHTMLElement } from '../../../base/browser/dom.js';
  6. import { StandardMouseEvent } from '../../../base/browser/mouseEvent.js';
  7. import { Menu } from '../../../base/browser/ui/menu/menu.js';
  8. import { ActionRunner } from '../../../base/common/actions.js';
  9. import { isPromiseCanceledError } from '../../../base/common/errors.js';
  10. import { combinedDisposable, DisposableStore } from '../../../base/common/lifecycle.js';
  11. import './contextMenuHandler.css';
  12. import { attachMenuStyler } from '../../theme/common/styler.js';
  13. export class ContextMenuHandler {
  14. constructor(contextViewService, telemetryService, notificationService, keybindingService, themeService) {
  15. this.contextViewService = contextViewService;
  16. this.telemetryService = telemetryService;
  17. this.notificationService = notificationService;
  18. this.keybindingService = keybindingService;
  19. this.themeService = themeService;
  20. this.focusToReturn = null;
  21. this.block = null;
  22. this.options = { blockMouse: true };
  23. }
  24. configure(options) {
  25. this.options = options;
  26. }
  27. showContextMenu(delegate) {
  28. const actions = delegate.getActions();
  29. if (!actions.length) {
  30. return; // Don't render an empty context menu
  31. }
  32. this.focusToReturn = document.activeElement;
  33. let menu;
  34. let shadowRootElement = isHTMLElement(delegate.domForShadowRoot) ? delegate.domForShadowRoot : undefined;
  35. this.contextViewService.showContextView({
  36. getAnchor: () => delegate.getAnchor(),
  37. canRelayout: false,
  38. anchorAlignment: delegate.anchorAlignment,
  39. anchorAxisAlignment: delegate.anchorAxisAlignment,
  40. render: (container) => {
  41. let className = delegate.getMenuClassName ? delegate.getMenuClassName() : '';
  42. if (className) {
  43. container.className += ' ' + className;
  44. }
  45. // Render invisible div to block mouse interaction in the rest of the UI
  46. if (this.options.blockMouse) {
  47. this.block = container.appendChild($('.context-view-block'));
  48. this.block.style.position = 'fixed';
  49. this.block.style.cursor = 'initial';
  50. this.block.style.left = '0';
  51. this.block.style.top = '0';
  52. this.block.style.width = '100%';
  53. this.block.style.height = '100%';
  54. this.block.style.zIndex = '-1';
  55. // TODO@Steven: this is never getting disposed
  56. addDisposableListener(this.block, EventType.MOUSE_DOWN, e => e.stopPropagation());
  57. }
  58. const menuDisposables = new DisposableStore();
  59. const actionRunner = delegate.actionRunner || new ActionRunner();
  60. actionRunner.onBeforeRun(this.onActionRun, this, menuDisposables);
  61. actionRunner.onDidRun(this.onDidActionRun, this, menuDisposables);
  62. menu = new Menu(container, actions, {
  63. actionViewItemProvider: delegate.getActionViewItem,
  64. context: delegate.getActionsContext ? delegate.getActionsContext() : null,
  65. actionRunner,
  66. getKeyBinding: delegate.getKeyBinding ? delegate.getKeyBinding : action => this.keybindingService.lookupKeybinding(action.id)
  67. });
  68. menuDisposables.add(attachMenuStyler(menu, this.themeService));
  69. menu.onDidCancel(() => this.contextViewService.hideContextView(true), null, menuDisposables);
  70. menu.onDidBlur(() => this.contextViewService.hideContextView(true), null, menuDisposables);
  71. menuDisposables.add(addDisposableListener(window, EventType.BLUR, () => this.contextViewService.hideContextView(true)));
  72. menuDisposables.add(addDisposableListener(window, EventType.MOUSE_DOWN, (e) => {
  73. if (e.defaultPrevented) {
  74. return;
  75. }
  76. let event = new StandardMouseEvent(e);
  77. let element = event.target;
  78. // Don't do anything as we are likely creating a context menu
  79. if (event.rightButton) {
  80. return;
  81. }
  82. while (element) {
  83. if (element === container) {
  84. return;
  85. }
  86. element = element.parentElement;
  87. }
  88. this.contextViewService.hideContextView(true);
  89. }));
  90. return combinedDisposable(menuDisposables, menu);
  91. },
  92. focus: () => {
  93. if (menu) {
  94. menu.focus(!!delegate.autoSelectFirstItem);
  95. }
  96. },
  97. onHide: (didCancel) => {
  98. if (delegate.onHide) {
  99. delegate.onHide(!!didCancel);
  100. }
  101. if (this.block) {
  102. this.block.remove();
  103. this.block = null;
  104. }
  105. if (this.focusToReturn) {
  106. this.focusToReturn.focus();
  107. }
  108. }
  109. }, shadowRootElement, !!shadowRootElement);
  110. }
  111. onActionRun(e) {
  112. this.telemetryService.publicLog2('workbenchActionExecuted', { id: e.action.id, from: 'contextMenu' });
  113. this.contextViewService.hideContextView(false);
  114. // Restore focus here
  115. if (this.focusToReturn) {
  116. this.focusToReturn.focus();
  117. }
  118. }
  119. onDidActionRun(e) {
  120. if (e.error && !isPromiseCanceledError(e.error)) {
  121. this.notificationService.error(e.error);
  122. }
  123. }
  124. }