UNPKG

19.7 kBJavaScriptView Raw
1"use strict";
2// *****************************************************************************
3// Copyright (C) 2017 TypeFox and others.
4//
5// This program and the accompanying materials are made available under the
6// terms of the Eclipse Public License v. 2.0 which is available at
7// http://www.eclipse.org/legal/epl-2.0.
8//
9// This Source Code may also be made available under the following Secondary
10// Licenses when the conditions for such availability set forth in the Eclipse
11// Public License v. 2.0 are satisfied: GNU General Public License, version 2
12// with the GNU Classpath Exception which is available at
13// https://www.gnu.org/software/classpath/license.html.
14//
15// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
16// *****************************************************************************
17var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
18 var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
19 if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
20 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;
21 return c > 3 && r && Object.defineProperty(target, key, r), r;
22};
23var __metadata = (this && this.__metadata) || function (k, v) {
24 if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
25};
26var __param = (this && this.__param) || function (paramIndex, decorator) {
27 return function (target, key) { decorator(target, key, paramIndex); }
28};
29Object.defineProperty(exports, "__esModule", { value: true });
30exports.MenuCommandRegistry = exports.BrowserMenuBarContribution = exports.DynamicMenuWidget = exports.MenuServices = exports.DynamicMenuBarWidget = exports.BrowserMainMenuFactory = exports.MenuBarWidget = void 0;
31const inversify_1 = require("inversify");
32const widgets_1 = require("@phosphor/widgets");
33const commands_1 = require("@phosphor/commands");
34const common_1 = require("../../common");
35const keybinding_1 = require("../keybinding");
36const context_key_service_1 = require("../context-key-service");
37const context_menu_context_1 = require("./context-menu-context");
38const widgets_2 = require("../widgets");
39const shell_1 = require("../shell");
40const core_preferences_1 = require("../core-preferences");
41const preference_service_1 = require("../preferences/preference-service");
42class MenuBarWidget extends widgets_1.MenuBar {
43}
44exports.MenuBarWidget = MenuBarWidget;
45;
46let BrowserMainMenuFactory = class BrowserMainMenuFactory {
47 createMenuBar() {
48 const menuBar = new DynamicMenuBarWidget();
49 menuBar.id = 'theia:menubar';
50 this.corePreferences.ready.then(() => {
51 this.showMenuBar(menuBar, this.corePreferences.get('window.menuBarVisibility', 'classic'));
52 });
53 const preferenceListener = this.corePreferences.onPreferenceChanged(preference => {
54 if (preference.preferenceName === 'window.menuBarVisibility') {
55 this.showMenuBar(menuBar, preference.newValue);
56 }
57 });
58 const keybindingListener = this.keybindingRegistry.onKeybindingsChanged(() => {
59 const preference = this.corePreferences['window.menuBarVisibility'];
60 this.showMenuBar(menuBar, preference);
61 });
62 menuBar.disposed.connect(() => {
63 preferenceListener.dispose();
64 keybindingListener.dispose();
65 });
66 return menuBar;
67 }
68 showMenuBar(menuBar, preference) {
69 if (preference && ['classic', 'visible'].includes(preference)) {
70 menuBar.clearMenus();
71 this.fillMenuBar(menuBar);
72 }
73 else {
74 menuBar.clearMenus();
75 }
76 }
77 fillMenuBar(menuBar) {
78 const menuModel = this.menuProvider.getMenu(common_1.MAIN_MENU_BAR);
79 const menuCommandRegistry = this.createMenuCommandRegistry(menuModel);
80 for (const menu of menuModel.children) {
81 if (common_1.CompoundMenuNode.is(menu)) {
82 const menuWidget = this.createMenuWidget(menu, { commands: menuCommandRegistry, rootMenuPath: common_1.MAIN_MENU_BAR });
83 menuBar.addMenu(menuWidget);
84 }
85 }
86 }
87 createContextMenu(path, args, context, contextKeyService) {
88 const menuModel = this.menuProvider.getMenu(path);
89 const menuCommandRegistry = this.createMenuCommandRegistry(menuModel, args).snapshot(path);
90 const contextMenu = this.createMenuWidget(menuModel, { commands: menuCommandRegistry, context, rootMenuPath: path, contextKeyService });
91 return contextMenu;
92 }
93 createMenuWidget(menu, options) {
94 return new DynamicMenuWidget(menu, options, this.services);
95 }
96 createMenuCommandRegistry(menu, args = []) {
97 const menuCommandRegistry = new MenuCommandRegistry(this.services);
98 this.registerMenu(menuCommandRegistry, menu, args);
99 return menuCommandRegistry;
100 }
101 registerMenu(menuCommandRegistry, menu, args) {
102 if (common_1.CompoundMenuNode.is(menu)) {
103 menu.children.forEach(child => this.registerMenu(menuCommandRegistry, child, args));
104 }
105 else if (common_1.CommandMenuNode.is(menu)) {
106 menuCommandRegistry.registerActionMenu(menu, args);
107 if (common_1.CommandMenuNode.hasAltHandler(menu)) {
108 menuCommandRegistry.registerActionMenu(menu.altNode, args);
109 }
110 }
111 }
112 get services() {
113 return {
114 context: this.context,
115 contextKeyService: this.contextKeyService,
116 commandRegistry: this.commandRegistry,
117 keybindingRegistry: this.keybindingRegistry,
118 menuWidgetFactory: this,
119 commandExecutor: this.menuCommandExecutor,
120 };
121 }
122};
123__decorate([
124 (0, inversify_1.inject)(context_key_service_1.ContextKeyService),
125 __metadata("design:type", Object)
126], BrowserMainMenuFactory.prototype, "contextKeyService", void 0);
127__decorate([
128 (0, inversify_1.inject)(context_menu_context_1.ContextMenuContext),
129 __metadata("design:type", context_menu_context_1.ContextMenuContext)
130], BrowserMainMenuFactory.prototype, "context", void 0);
131__decorate([
132 (0, inversify_1.inject)(common_1.CommandRegistry),
133 __metadata("design:type", common_1.CommandRegistry)
134], BrowserMainMenuFactory.prototype, "commandRegistry", void 0);
135__decorate([
136 (0, inversify_1.inject)(common_1.MenuCommandExecutor),
137 __metadata("design:type", Object)
138], BrowserMainMenuFactory.prototype, "menuCommandExecutor", void 0);
139__decorate([
140 (0, inversify_1.inject)(core_preferences_1.CorePreferences),
141 __metadata("design:type", Object)
142], BrowserMainMenuFactory.prototype, "corePreferences", void 0);
143__decorate([
144 (0, inversify_1.inject)(keybinding_1.KeybindingRegistry),
145 __metadata("design:type", keybinding_1.KeybindingRegistry)
146], BrowserMainMenuFactory.prototype, "keybindingRegistry", void 0);
147__decorate([
148 (0, inversify_1.inject)(common_1.MenuModelRegistry),
149 __metadata("design:type", common_1.MenuModelRegistry)
150], BrowserMainMenuFactory.prototype, "menuProvider", void 0);
151BrowserMainMenuFactory = __decorate([
152 (0, inversify_1.injectable)()
153], BrowserMainMenuFactory);
154exports.BrowserMainMenuFactory = BrowserMainMenuFactory;
155class DynamicMenuBarWidget extends MenuBarWidget {
156 constructor() {
157 super();
158 // HACK we need to hook in on private method _openChildMenu. Don't do this at home!
159 DynamicMenuBarWidget.prototype['_openChildMenu'] = () => {
160 if (this.activeMenu instanceof DynamicMenuWidget) {
161 // `childMenu` is `null` if we open the menu. For example, menu is not shown and you click on `Edit`.
162 // However, the `childMenu` is set, when `Edit` was already open and you move the mouse over `Select`.
163 // We want to save the focus object for the former case only.
164 if (!this.childMenu) {
165 const { activeElement } = document;
166 if (activeElement instanceof HTMLElement) {
167 this.previousFocusedElement = activeElement;
168 }
169 }
170 this.activeMenu.aboutToShow({ previousFocusedElement: this.previousFocusedElement });
171 }
172 super['_openChildMenu']();
173 };
174 }
175 async activateMenu(label, ...labels) {
176 const menu = this.menus.find(m => m.title.label === label);
177 if (!menu) {
178 throw new Error(`could not find '${label}' menu`);
179 }
180 this.activeMenu = menu;
181 this.openActiveMenu();
182 await (0, widgets_2.waitForRevealed)(menu);
183 const menuPath = [label];
184 let current = menu;
185 for (const itemLabel of labels) {
186 const item = current.items.find(i => i.label === itemLabel);
187 if (!item || !item.submenu) {
188 throw new Error(`could not find '${label}' submenu in ${menuPath.map(l => "'" + l + "'").join(' -> ')} menu`);
189 }
190 current.activeItem = item;
191 current.triggerActiveItem();
192 current = item.submenu;
193 await (0, widgets_2.waitForRevealed)(current);
194 }
195 return current;
196 }
197 async triggerMenuItem(label, ...labels) {
198 if (!labels.length) {
199 throw new Error('menu item label is not specified');
200 }
201 const menuPath = [label, ...labels.slice(0, labels.length - 1)];
202 const menu = await this.activateMenu(menuPath[0], ...menuPath.slice(1));
203 const item = menu.items.find(i => i.label === labels[labels.length - 1]);
204 if (!item) {
205 throw new Error(`could not find '${label}' item in ${menuPath.map(l => "'" + l + "'").join(' -> ')} menu`);
206 }
207 menu.activeItem = item;
208 menu.triggerActiveItem();
209 return item;
210 }
211}
212exports.DynamicMenuBarWidget = DynamicMenuBarWidget;
213class MenuServices {
214}
215exports.MenuServices = MenuServices;
216/**
217 * A menu widget that would recompute its items on update.
218 */
219class DynamicMenuWidget extends widgets_1.Menu {
220 constructor(menu, options, services) {
221 super(options);
222 this.menu = menu;
223 this.options = options;
224 this.services = services;
225 if (menu.label) {
226 this.title.label = menu.label;
227 }
228 if (menu.icon) {
229 this.title.iconClass = menu.icon;
230 }
231 this.updateSubMenus(this, this.menu, this.options.commands);
232 }
233 aboutToShow({ previousFocusedElement }) {
234 this.preserveFocusedElement(previousFocusedElement);
235 this.clearItems();
236 this.runWithPreservedFocusContext(() => {
237 this.options.commands.snapshot(this.options.rootMenuPath);
238 this.updateSubMenus(this, this.menu, this.options.commands);
239 });
240 }
241 open(x, y, options) {
242 const cb = () => {
243 this.restoreFocusedElement();
244 this.aboutToClose.disconnect(cb);
245 };
246 this.aboutToClose.connect(cb);
247 this.preserveFocusedElement();
248 super.open(x, y, options);
249 }
250 updateSubMenus(parent, menu, commands) {
251 var _a;
252 const items = this.buildSubMenus([], menu, commands);
253 while (((_a = items[items.length - 1]) === null || _a === void 0 ? void 0 : _a.type) === 'separator') {
254 items.pop();
255 }
256 for (const item of items) {
257 parent.addItem(item);
258 }
259 }
260 buildSubMenus(parentItems, menu, commands) {
261 var _a, _b;
262 if (common_1.CompoundMenuNode.is(menu)
263 && menu.children.length
264 && this.undefinedOrMatch((_a = this.options.contextKeyService) !== null && _a !== void 0 ? _a : this.services.contextKeyService, menu.when, this.options.context)) {
265 const role = menu === this.menu ? 1 /* Group */ : common_1.CompoundMenuNode.getRole(menu);
266 if (role === 0 /* Submenu */) {
267 const submenu = this.services.menuWidgetFactory.createMenuWidget(menu, this.options);
268 if (submenu.items.length > 0) {
269 parentItems.push({ type: 'submenu', submenu });
270 }
271 }
272 else if (role === 1 /* Group */ && menu.id !== 'inline') {
273 const children = common_1.CompoundMenuNode.getFlatChildren(menu.children);
274 const myItems = [];
275 children.forEach(child => this.buildSubMenus(myItems, child, commands));
276 if (myItems.length) {
277 if (parentItems.length && parentItems[parentItems.length - 1].type !== 'separator') {
278 parentItems.push({ type: 'separator' });
279 }
280 parentItems.push(...myItems);
281 parentItems.push({ type: 'separator' });
282 }
283 }
284 }
285 else if (menu.command) {
286 const node = menu.altNode && this.services.context.altPressed ? menu.altNode : menu;
287 if (commands.isVisible(node.command) && this.undefinedOrMatch((_b = this.options.contextKeyService) !== null && _b !== void 0 ? _b : this.services.contextKeyService, node.when, this.options.context)) {
288 parentItems.push({
289 command: node.command,
290 type: 'command'
291 });
292 }
293 }
294 return parentItems;
295 }
296 undefinedOrMatch(contextKeyService, expression, context) {
297 if (expression) {
298 return contextKeyService.match(expression, context);
299 }
300 return true;
301 }
302 preserveFocusedElement(previousFocusedElement = document.activeElement) {
303 if (!this.previousFocusedElement && previousFocusedElement instanceof HTMLElement) {
304 this.previousFocusedElement = previousFocusedElement;
305 return true;
306 }
307 return false;
308 }
309 restoreFocusedElement() {
310 if (this.previousFocusedElement) {
311 this.previousFocusedElement.focus({ preventScroll: true });
312 this.previousFocusedElement = undefined;
313 return true;
314 }
315 return false;
316 }
317 runWithPreservedFocusContext(what) {
318 let focusToRestore = undefined;
319 const { activeElement } = document;
320 if (this.previousFocusedElement && activeElement instanceof HTMLElement && this.previousFocusedElement !== activeElement) {
321 focusToRestore = activeElement;
322 this.previousFocusedElement.focus({ preventScroll: true });
323 }
324 try {
325 what();
326 }
327 finally {
328 if (focusToRestore) {
329 focusToRestore.focus({ preventScroll: true });
330 }
331 }
332 }
333}
334exports.DynamicMenuWidget = DynamicMenuWidget;
335let BrowserMenuBarContribution = class BrowserMenuBarContribution {
336 constructor(factory) {
337 this.factory = factory;
338 }
339 onStart(app) {
340 this.appendMenu(app.shell);
341 }
342 get menuBar() {
343 return this.shell.topPanel.widgets.find(w => w instanceof MenuBarWidget);
344 }
345 appendMenu(shell) {
346 const logo = this.createLogo();
347 shell.addWidget(logo, { area: 'top' });
348 const menu = this.factory.createMenuBar();
349 shell.addWidget(menu, { area: 'top' });
350 // Hiding the menu is only necessary in electron
351 // In the browser we hide the whole top panel
352 if (common_1.environment.electron.is()) {
353 this.preferenceService.ready.then(() => {
354 menu.setHidden(['compact', 'hidden'].includes(this.preferenceService.get('window.menuBarVisibility', '')));
355 });
356 this.preferenceService.onPreferenceChanged(change => {
357 if (change.preferenceName === 'window.menuBarVisibility') {
358 menu.setHidden(['compact', 'hidden'].includes(change.newValue));
359 }
360 });
361 }
362 }
363 createLogo() {
364 const logo = new widgets_1.Widget();
365 logo.id = 'theia:icon';
366 logo.addClass('theia-icon');
367 return logo;
368 }
369};
370__decorate([
371 (0, inversify_1.inject)(shell_1.ApplicationShell),
372 __metadata("design:type", shell_1.ApplicationShell)
373], BrowserMenuBarContribution.prototype, "shell", void 0);
374__decorate([
375 (0, inversify_1.inject)(preference_service_1.PreferenceService),
376 __metadata("design:type", Object)
377], BrowserMenuBarContribution.prototype, "preferenceService", void 0);
378BrowserMenuBarContribution = __decorate([
379 (0, inversify_1.injectable)(),
380 __param(0, (0, inversify_1.inject)(BrowserMainMenuFactory)),
381 __metadata("design:paramtypes", [BrowserMainMenuFactory])
382], BrowserMenuBarContribution);
383exports.BrowserMenuBarContribution = BrowserMenuBarContribution;
384/**
385 * Stores Theia-specific action menu nodes instead of PhosphorJS commands with their handlers.
386 */
387class MenuCommandRegistry extends commands_1.CommandRegistry {
388 constructor(services) {
389 super();
390 this.services = services;
391 this.actions = new Map();
392 this.toDispose = new common_1.DisposableCollection();
393 }
394 registerActionMenu(menu, args) {
395 const { commandRegistry } = this.services;
396 const command = commandRegistry.getCommand(menu.command);
397 if (!command) {
398 return;
399 }
400 const { id } = command;
401 if (this.actions.has(id)) {
402 return;
403 }
404 this.actions.set(id, [menu, args]);
405 }
406 snapshot(menuPath) {
407 this.toDispose.dispose();
408 for (const [menu, args] of this.actions.values()) {
409 this.toDispose.push(this.registerCommand(menu, args, menuPath));
410 }
411 return this;
412 }
413 registerCommand(menu, args, menuPath) {
414 const { commandRegistry, keybindingRegistry, commandExecutor } = this.services;
415 const command = commandRegistry.getCommand(menu.command);
416 if (!command) {
417 return common_1.Disposable.NULL;
418 }
419 const { id } = command;
420 if (this.hasCommand(id)) {
421 // several menu items can be registered for the same command in different contexts
422 return common_1.Disposable.NULL;
423 }
424 // We freeze the `isEnabled`, `isVisible`, and `isToggled` states so they won't change.
425 const enabled = commandExecutor.isEnabled(menuPath, id, ...args);
426 const visible = commandExecutor.isVisible(menuPath, id, ...args);
427 const toggled = commandExecutor.isToggled(menuPath, id, ...args);
428 const unregisterCommand = this.addCommand(id, {
429 execute: () => commandExecutor.executeCommand(menuPath, id, ...args),
430 label: menu.label,
431 icon: menu.icon,
432 isEnabled: () => enabled,
433 isVisible: () => visible,
434 isToggled: () => toggled
435 });
436 const bindings = keybindingRegistry.getKeybindingsForCommand(id);
437 // Only consider the first keybinding.
438 if (bindings.length) {
439 const binding = bindings[0];
440 const keys = keybindingRegistry.acceleratorFor(binding, ' ', true);
441 this.addKeyBinding({
442 command: id,
443 keys,
444 selector: '.p-Widget' // We have the PhosphorJS dependency anyway.
445 });
446 }
447 return common_1.Disposable.create(() => unregisterCommand.dispose());
448 }
449}
450exports.MenuCommandRegistry = MenuCommandRegistry;
451//# sourceMappingURL=browser-menu-plugin.js.map
\No newline at end of file