menuService.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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. var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
  6. var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
  7. if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
  8. else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
  9. return c > 3 && r && Object.defineProperty(target, key, r), r;
  10. };
  11. var __param = (this && this.__param) || function (paramIndex, decorator) {
  12. return function (target, key) { decorator(target, key, paramIndex); }
  13. };
  14. import { RunOnceScheduler } from '../../../base/common/async.js';
  15. import { Emitter } from '../../../base/common/event.js';
  16. import { DisposableStore } from '../../../base/common/lifecycle.js';
  17. import { IMenuService, isIMenuItem, MenuItemAction, MenuRegistry, SubmenuItemAction } from './actions.js';
  18. import { ICommandService } from '../../commands/common/commands.js';
  19. import { IContextKeyService } from '../../contextkey/common/contextkey.js';
  20. let MenuService = class MenuService {
  21. constructor(_commandService) {
  22. this._commandService = _commandService;
  23. //
  24. }
  25. /**
  26. * Create a new menu for the given menu identifier. A menu sends events when it's entries
  27. * have changed (placement, enablement, checked-state). By default it does send events for
  28. * sub menu entries. That is more expensive and must be explicitly enabled with the
  29. * `emitEventsForSubmenuChanges` flag.
  30. */
  31. createMenu(id, contextKeyService, options) {
  32. return new Menu(id, Object.assign({ emitEventsForSubmenuChanges: false, eventDebounceDelay: 50 }, options), this._commandService, contextKeyService, this);
  33. }
  34. };
  35. MenuService = __decorate([
  36. __param(0, ICommandService)
  37. ], MenuService);
  38. export { MenuService };
  39. let Menu = class Menu {
  40. constructor(_id, _options, _commandService, _contextKeyService, _menuService) {
  41. this._id = _id;
  42. this._options = _options;
  43. this._commandService = _commandService;
  44. this._contextKeyService = _contextKeyService;
  45. this._menuService = _menuService;
  46. this._disposables = new DisposableStore();
  47. this._menuGroups = [];
  48. this._contextKeys = new Set();
  49. this._build();
  50. // Rebuild this menu whenever the menu registry reports an event for this MenuId.
  51. // This usually happen while code and extensions are loaded and affects the over
  52. // structure of the menu
  53. const rebuildMenuSoon = new RunOnceScheduler(() => {
  54. this._build();
  55. this._onDidChange.fire(this);
  56. }, _options.eventDebounceDelay);
  57. this._disposables.add(rebuildMenuSoon);
  58. this._disposables.add(MenuRegistry.onDidChangeMenu(e => {
  59. if (e.has(_id)) {
  60. rebuildMenuSoon.schedule();
  61. }
  62. }));
  63. // When context keys change we need to check if the menu also has changed. However,
  64. // we only do that when someone listens on this menu because (1) context key events are
  65. // firing often and (2) menu are often leaked
  66. const contextKeyListener = this._disposables.add(new DisposableStore());
  67. const startContextKeyListener = () => {
  68. const fireChangeSoon = new RunOnceScheduler(() => this._onDidChange.fire(this), _options.eventDebounceDelay);
  69. contextKeyListener.add(fireChangeSoon);
  70. contextKeyListener.add(_contextKeyService.onDidChangeContext(e => {
  71. if (e.affectsSome(this._contextKeys)) {
  72. fireChangeSoon.schedule();
  73. }
  74. }));
  75. };
  76. this._onDidChange = new Emitter({
  77. // start/stop context key listener
  78. onFirstListenerAdd: startContextKeyListener,
  79. onLastListenerRemove: contextKeyListener.clear.bind(contextKeyListener)
  80. });
  81. this.onDidChange = this._onDidChange.event;
  82. }
  83. dispose() {
  84. this._disposables.dispose();
  85. this._onDidChange.dispose();
  86. }
  87. _build() {
  88. // reset
  89. this._menuGroups.length = 0;
  90. this._contextKeys.clear();
  91. const menuItems = MenuRegistry.getMenuItems(this._id);
  92. let group;
  93. menuItems.sort(Menu._compareMenuItems);
  94. for (const item of menuItems) {
  95. // group by groupId
  96. const groupName = item.group || '';
  97. if (!group || group[0] !== groupName) {
  98. group = [groupName, []];
  99. this._menuGroups.push(group);
  100. }
  101. group[1].push(item);
  102. // keep keys for eventing
  103. this._collectContextKeys(item);
  104. }
  105. }
  106. _collectContextKeys(item) {
  107. Menu._fillInKbExprKeys(item.when, this._contextKeys);
  108. if (isIMenuItem(item)) {
  109. // keep precondition keys for event if applicable
  110. if (item.command.precondition) {
  111. Menu._fillInKbExprKeys(item.command.precondition, this._contextKeys);
  112. }
  113. // keep toggled keys for event if applicable
  114. if (item.command.toggled) {
  115. const toggledExpression = item.command.toggled.condition || item.command.toggled;
  116. Menu._fillInKbExprKeys(toggledExpression, this._contextKeys);
  117. }
  118. }
  119. else if (this._options.emitEventsForSubmenuChanges) {
  120. // recursively collect context keys from submenus so that this
  121. // menu fires events when context key changes affect submenus
  122. MenuRegistry.getMenuItems(item.submenu).forEach(this._collectContextKeys, this);
  123. }
  124. }
  125. getActions(options) {
  126. const result = [];
  127. for (let group of this._menuGroups) {
  128. const [id, items] = group;
  129. const activeActions = [];
  130. for (const item of items) {
  131. if (this._contextKeyService.contextMatchesRules(item.when)) {
  132. const action = isIMenuItem(item)
  133. ? new MenuItemAction(item.command, item.alt, options, this._contextKeyService, this._commandService)
  134. : new SubmenuItemAction(item, this._menuService, this._contextKeyService, options);
  135. activeActions.push(action);
  136. }
  137. }
  138. if (activeActions.length > 0) {
  139. result.push([id, activeActions]);
  140. }
  141. }
  142. return result;
  143. }
  144. static _fillInKbExprKeys(exp, set) {
  145. if (exp) {
  146. for (let key of exp.keys()) {
  147. set.add(key);
  148. }
  149. }
  150. }
  151. static _compareMenuItems(a, b) {
  152. let aGroup = a.group;
  153. let bGroup = b.group;
  154. if (aGroup !== bGroup) {
  155. // Falsy groups come last
  156. if (!aGroup) {
  157. return 1;
  158. }
  159. else if (!bGroup) {
  160. return -1;
  161. }
  162. // 'navigation' group comes first
  163. if (aGroup === 'navigation') {
  164. return -1;
  165. }
  166. else if (bGroup === 'navigation') {
  167. return 1;
  168. }
  169. // lexical sort for groups
  170. let value = aGroup.localeCompare(bGroup);
  171. if (value !== 0) {
  172. return value;
  173. }
  174. }
  175. // sort on priority - default is 0
  176. let aPrio = a.order || 0;
  177. let bPrio = b.order || 0;
  178. if (aPrio < bPrio) {
  179. return -1;
  180. }
  181. else if (aPrio > bPrio) {
  182. return 1;
  183. }
  184. // sort on titles
  185. return Menu._compareTitles(isIMenuItem(a) ? a.command.title : a.title, isIMenuItem(b) ? b.command.title : b.title);
  186. }
  187. static _compareTitles(a, b) {
  188. const aStr = typeof a === 'string' ? a : a.original;
  189. const bStr = typeof b === 'string' ? b : b.original;
  190. return aStr.localeCompare(bStr);
  191. }
  192. };
  193. Menu = __decorate([
  194. __param(2, ICommandService),
  195. __param(3, IContextKeyService),
  196. __param(4, IMenuService)
  197. ], Menu);