UNPKG

@theia/core

Version:

Theia is a cloud & desktop IDE framework implemented in TypeScript.

496 lines • 21.8 kB
"use strict"; // ***************************************************************************** // Copyright (C) 2017 TypeFox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** Object.defineProperty(exports, "__esModule", { value: true }); exports.MenuCommandRegistry = exports.BrowserMenuBarContribution = exports.DynamicMenuWidget = exports.MenuServices = exports.DynamicMenuBarWidget = exports.isMenuElement = exports.BrowserMainMenuFactory = exports.MenuBarWidget = void 0; const tslib_1 = require("tslib"); const inversify_1 = require("inversify"); const widgets_1 = require("@lumino/widgets"); const commands_1 = require("@lumino/commands"); const common_1 = require("../../common"); const keybinding_1 = require("../keybinding"); const context_key_service_1 = require("../context-key-service"); const context_menu_context_1 = require("./context-menu-context"); const widgets_2 = require("../widgets"); const shell_1 = require("../shell"); const core_preferences_1 = require("../core-preferences"); const preference_service_1 = require("../preferences/preference-service"); const domutils_1 = require("@lumino/domutils"); class MenuBarWidget extends widgets_1.MenuBar { } exports.MenuBarWidget = MenuBarWidget; ; let BrowserMainMenuFactory = class BrowserMainMenuFactory { createMenuBar() { const menuBar = new DynamicMenuBarWidget(); menuBar.id = 'theia:menubar'; this.corePreferences.ready.then(() => { this.showMenuBar(menuBar); }); const disposable = new common_1.DisposableCollection(this.corePreferences.onPreferenceChanged(change => { if (change.preferenceName === 'window.menuBarVisibility') { this.showMenuBar(menuBar, change.newValue); } }), this.keybindingRegistry.onKeybindingsChanged(() => { this.showMenuBar(menuBar); }), this.menuProvider.onDidChange(evt => { if (common_1.ArrayUtils.startsWith(evt.path, common_1.MAIN_MENU_BAR)) { this.showMenuBar(menuBar); } })); menuBar.disposed.connect(() => disposable.dispose()); return menuBar; } getMenuBarVisibility() { return this.corePreferences.get('window.menuBarVisibility', 'classic'); } showMenuBar(menuBar, preference = this.getMenuBarVisibility()) { if (preference && ['classic', 'visible'].includes(preference)) { menuBar.clearMenus(); this.fillMenuBar(menuBar); } else { menuBar.clearMenus(); } } fillMenuBar(menuBar) { const menuModel = this.menuProvider.getMenu(common_1.MAIN_MENU_BAR); const menuCommandRegistry = this.createMenuCommandRegistry(menuModel); for (const menu of menuModel.children) { if (common_1.CompoundMenuNode.is(menu)) { const menuWidget = this.createMenuWidget(menu, { commands: menuCommandRegistry, rootMenuPath: common_1.MAIN_MENU_BAR }); menuBar.addMenu(menuWidget); } } } createContextMenu(path, args, context, contextKeyService, skipSingleRootNode) { const menuModel = skipSingleRootNode ? this.menuProvider.removeSingleRootNode(this.menuProvider.getMenu(path), path) : this.menuProvider.getMenu(path); const menuCommandRegistry = this.createMenuCommandRegistry(menuModel, args).snapshot(path); const contextMenu = this.createMenuWidget(menuModel, { commands: menuCommandRegistry, context, rootMenuPath: path, contextKeyService }); return contextMenu; } createMenuWidget(menu, options) { return new DynamicMenuWidget(menu, options, this.services); } createMenuCommandRegistry(menu, args = []) { const menuCommandRegistry = new MenuCommandRegistry(this.services); this.registerMenu(menuCommandRegistry, menu, args); return menuCommandRegistry; } registerMenu(menuCommandRegistry, menu, args) { if (common_1.CompoundMenuNode.is(menu)) { menu.children.forEach(child => this.registerMenu(menuCommandRegistry, child, args)); } else if (common_1.CommandMenuNode.is(menu)) { menuCommandRegistry.registerActionMenu(menu, args); if (common_1.CommandMenuNode.hasAltHandler(menu)) { menuCommandRegistry.registerActionMenu(menu.altNode, args); } } } get services() { return { context: this.context, contextKeyService: this.contextKeyService, commandRegistry: this.commandRegistry, keybindingRegistry: this.keybindingRegistry, menuWidgetFactory: this, commandExecutor: this.menuCommandExecutor, }; } }; exports.BrowserMainMenuFactory = BrowserMainMenuFactory; tslib_1.__decorate([ (0, inversify_1.inject)(context_key_service_1.ContextKeyService), tslib_1.__metadata("design:type", Object) ], BrowserMainMenuFactory.prototype, "contextKeyService", void 0); tslib_1.__decorate([ (0, inversify_1.inject)(context_menu_context_1.ContextMenuContext), tslib_1.__metadata("design:type", context_menu_context_1.ContextMenuContext) ], BrowserMainMenuFactory.prototype, "context", void 0); tslib_1.__decorate([ (0, inversify_1.inject)(common_1.CommandRegistry), tslib_1.__metadata("design:type", common_1.CommandRegistry) ], BrowserMainMenuFactory.prototype, "commandRegistry", void 0); tslib_1.__decorate([ (0, inversify_1.inject)(common_1.MenuCommandExecutor), tslib_1.__metadata("design:type", Object) ], BrowserMainMenuFactory.prototype, "menuCommandExecutor", void 0); tslib_1.__decorate([ (0, inversify_1.inject)(core_preferences_1.CorePreferences), tslib_1.__metadata("design:type", Object) ], BrowserMainMenuFactory.prototype, "corePreferences", void 0); tslib_1.__decorate([ (0, inversify_1.inject)(keybinding_1.KeybindingRegistry), tslib_1.__metadata("design:type", keybinding_1.KeybindingRegistry) ], BrowserMainMenuFactory.prototype, "keybindingRegistry", void 0); tslib_1.__decorate([ (0, inversify_1.inject)(common_1.MenuModelRegistry), tslib_1.__metadata("design:type", common_1.MenuModelRegistry) ], BrowserMainMenuFactory.prototype, "menuProvider", void 0); exports.BrowserMainMenuFactory = BrowserMainMenuFactory = tslib_1.__decorate([ (0, inversify_1.injectable)() ], BrowserMainMenuFactory); function isMenuElement(element) { return !!element && element.className.includes('lm-Menu'); } exports.isMenuElement = isMenuElement; class DynamicMenuBarWidget extends MenuBarWidget { constructor() { super(); // HACK we need to hook in on private method _openChildMenu. Don't do this at home! DynamicMenuBarWidget.prototype['_openChildMenu'] = () => { if (this.activeMenu instanceof DynamicMenuWidget) { // `childMenu` is `null` if we open the menu. For example, menu is not shown and you click on `Edit`. // However, the `childMenu` is set, when `Edit` was already open and you move the mouse over `Select`. // We want to save the focus object for the former case only. if (!this.childMenu) { const { activeElement } = document; // we do not want to restore focus to menus if (activeElement instanceof HTMLElement && !isMenuElement(activeElement)) { this.previousFocusedElement = activeElement; } } this.activeMenu.aboutToShow({ previousFocusedElement: this.previousFocusedElement }); } super['_openChildMenu'](); }; } async activateMenu(label, ...labels) { const menu = this.menus.find(m => m.title.label === label); if (!menu) { throw new Error(`could not find '${label}' menu`); } this.activeMenu = menu; this.openActiveMenu(); await (0, widgets_2.waitForRevealed)(menu); const menuPath = [label, ...labels]; let current = menu; for (const itemLabel of labels) { const item = current.items.find(i => i.label === itemLabel); if (!item || !item.submenu) { throw new Error(`could not find '${itemLabel}' submenu in ${menuPath.map(l => "'" + l + "'").join(' -> ')} menu`); } current.activeItem = item; current.triggerActiveItem(); current = item.submenu; await (0, widgets_2.waitForRevealed)(current); } return current; } async triggerMenuItem(label, ...labels) { if (!labels.length) { throw new Error('menu item label is not specified'); } const menuPath = [label, ...labels.slice(0, labels.length - 1)]; const menu = await this.activateMenu(menuPath[0], ...menuPath.slice(1)); const item = menu.items.find(i => i.label === labels[labels.length - 1]); if (!item) { throw new Error(`could not find '${labels[labels.length - 1]}' item in ${menuPath.map(l => "'" + l + "'").join(' -> ')} menu`); } menu.activeItem = item; menu.triggerActiveItem(); return item; } } exports.DynamicMenuBarWidget = DynamicMenuBarWidget; class MenuServices { } exports.MenuServices = MenuServices; /** * A menu widget that would recompute its items on update. */ class DynamicMenuWidget extends widgets_1.Menu { constructor(menu, options, services) { super(options); this.menu = menu; this.options = options; this.services = services; if (menu.label) { this.title.label = menu.label; } if (menu.icon) { this.title.iconClass = menu.icon; } this.updateSubMenus(this, this.menu, this.options.commands); } onAfterAttach(msg) { super.onAfterAttach(msg); this.node.ownerDocument.addEventListener('pointerdown', this, true); } onBeforeDetach(msg) { this.node.ownerDocument.removeEventListener('pointerdown', this); super.onAfterDetach(msg); } handleEvent(event) { if (event.type === 'pointerdown') { this.handlePointerDown(event); } super.handleEvent(event); } handlePointerDown(event) { // this code is copied from the superclass because we cannot use the hit // test from the "Private" implementation namespace if (this['_parentMenu']) { return; } // The mouse button which is pressed is irrelevant. If the press // is not on a menu, the entire hierarchy is closed and the event // is allowed to propagate. This allows other code to act on the // event, such as focusing the clicked element. if (!this.hitTestMenus(this, event.clientX, event.clientY)) { this.close(); } } hitTestMenus(menu, x, y) { for (let temp = menu; temp; temp = temp.childMenu) { if (domutils_1.ElementExt.hitTest(temp.node, x, y)) { return true; } } return false; } aboutToShow({ previousFocusedElement }) { this.preserveFocusedElement(previousFocusedElement); this.clearItems(); this.runWithPreservedFocusContext(() => { this.options.commands.snapshot(this.options.rootMenuPath); this.updateSubMenus(this, this.menu, this.options.commands); }); } open(x, y, options) { const cb = () => { this.restoreFocusedElement(); this.aboutToClose.disconnect(cb); }; this.aboutToClose.connect(cb); this.preserveFocusedElement(); super.open(x, y, options); } updateSubMenus(parent, menu, commands) { var _a; const items = this.buildSubMenus([], menu, commands); while (((_a = items[items.length - 1]) === null || _a === void 0 ? void 0 : _a.type) === 'separator') { items.pop(); } // Add at least one entry to avoid empty menus. // This is needed as Lumino does all kind of checks whether a menu is empty and for example prevents activating it // This item will be cleared once the menu is opened via the next update as we don't have empty main menus // See https://github.com/jupyterlab/lumino/issues/729 if (items.length === 0) { items.push({ type: 'separator' }); } for (const item of items) { parent.addItem(item); } } buildSubMenus(parentItems, menu, commands) { var _a, _b; if (common_1.CompoundMenuNode.is(menu) && menu.children.length && this.undefinedOrMatch((_a = this.options.contextKeyService) !== null && _a !== void 0 ? _a : this.services.contextKeyService, menu.when, this.options.context)) { const role = menu === this.menu ? 1 /* CompoundMenuNodeRole.Group */ : common_1.CompoundMenuNode.getRole(menu); if (role === 0 /* CompoundMenuNodeRole.Submenu */) { const submenu = this.services.menuWidgetFactory.createMenuWidget(menu, this.options); if (submenu.items.length > 0) { parentItems.push({ type: 'submenu', submenu }); } } else if (role === 1 /* CompoundMenuNodeRole.Group */ && menu.id !== 'inline') { const children = common_1.CompoundMenuNode.getFlatChildren(menu.children); const myItems = []; children.forEach(child => this.buildSubMenus(myItems, child, commands)); if (myItems.length) { if (parentItems.length && parentItems[parentItems.length - 1].type !== 'separator') { parentItems.push({ type: 'separator' }); } parentItems.push(...myItems); parentItems.push({ type: 'separator' }); } } } else if (menu.command) { const node = menu.altNode && this.services.context.altPressed ? menu.altNode : menu; if (commands.isVisible(node.command) && this.undefinedOrMatch((_b = this.options.contextKeyService) !== null && _b !== void 0 ? _b : this.services.contextKeyService, node.when, this.options.context)) { parentItems.push({ command: node.command, type: 'command' }); } } return parentItems; } undefinedOrMatch(contextKeyService, expression, context) { if (expression) { return contextKeyService.match(expression, context); } return true; } preserveFocusedElement(previousFocusedElement = document.activeElement) { if (!this.previousFocusedElement && previousFocusedElement instanceof HTMLElement && !isMenuElement(previousFocusedElement)) { this.previousFocusedElement = previousFocusedElement; return true; } return false; } restoreFocusedElement() { if (this.previousFocusedElement) { this.previousFocusedElement.focus({ preventScroll: true }); this.previousFocusedElement = undefined; return true; } return false; } runWithPreservedFocusContext(what) { let focusToRestore = undefined; const { activeElement } = document; if (this.previousFocusedElement && activeElement instanceof HTMLElement && this.previousFocusedElement !== activeElement) { focusToRestore = activeElement; this.previousFocusedElement.focus({ preventScroll: true }); } try { what(); } finally { if (focusToRestore && !isMenuElement(focusToRestore)) { focusToRestore.focus({ preventScroll: true }); } } } } exports.DynamicMenuWidget = DynamicMenuWidget; let BrowserMenuBarContribution = class BrowserMenuBarContribution { constructor(factory) { this.factory = factory; } onStart(app) { this.appendMenu(app.shell); } get menuBar() { return this.shell.topPanel.widgets.find(w => w instanceof MenuBarWidget); } appendMenu(shell) { const logo = this.createLogo(); shell.addWidget(logo, { area: 'top' }); const menu = this.factory.createMenuBar(); shell.addWidget(menu, { area: 'top' }); // Hiding the menu is only necessary in electron // In the browser we hide the whole top panel if (common_1.environment.electron.is()) { this.preferenceService.ready.then(() => { menu.setHidden(['compact', 'hidden'].includes(this.preferenceService.get('window.menuBarVisibility', ''))); }); this.preferenceService.onPreferenceChanged(change => { if (change.preferenceName === 'window.menuBarVisibility') { menu.setHidden(['compact', 'hidden'].includes(change.newValue)); } }); } } createLogo() { const logo = new widgets_1.Widget(); logo.id = 'theia:icon'; logo.addClass('theia-icon'); return logo; } }; exports.BrowserMenuBarContribution = BrowserMenuBarContribution; tslib_1.__decorate([ (0, inversify_1.inject)(shell_1.ApplicationShell), tslib_1.__metadata("design:type", shell_1.ApplicationShell) ], BrowserMenuBarContribution.prototype, "shell", void 0); tslib_1.__decorate([ (0, inversify_1.inject)(preference_service_1.PreferenceService), tslib_1.__metadata("design:type", Object) ], BrowserMenuBarContribution.prototype, "preferenceService", void 0); exports.BrowserMenuBarContribution = BrowserMenuBarContribution = tslib_1.__decorate([ (0, inversify_1.injectable)(), tslib_1.__param(0, (0, inversify_1.inject)(BrowserMainMenuFactory)), tslib_1.__metadata("design:paramtypes", [BrowserMainMenuFactory]) ], BrowserMenuBarContribution); /** * Stores Theia-specific action menu nodes instead of Lumino commands with their handlers. */ class MenuCommandRegistry extends commands_1.CommandRegistry { constructor(services) { super(); this.services = services; this.actions = new Map(); this.toDispose = new common_1.DisposableCollection(); } registerActionMenu(menu, args) { const { commandRegistry } = this.services; const command = commandRegistry.getCommand(menu.command); if (!command) { return; } const { id } = command; if (this.actions.has(id)) { return; } this.actions.set(id, [menu, args]); } snapshot(menuPath) { this.toDispose.dispose(); for (const [menu, args] of this.actions.values()) { this.toDispose.push(this.registerCommand(menu, args, menuPath)); } return this; } registerCommand(menu, args, menuPath) { var _a; const { commandRegistry, keybindingRegistry, commandExecutor } = this.services; const command = commandRegistry.getCommand(menu.command); if (!command) { return common_1.Disposable.NULL; } const { id } = command; if (this.hasCommand(id)) { // several menu items can be registered for the same command in different contexts return common_1.Disposable.NULL; } // We freeze the `isEnabled`, `isVisible`, and `isToggled` states so they won't change. const enabled = commandExecutor.isEnabled(menuPath, id, ...args); const visible = commandExecutor.isVisible(menuPath, id, ...args); const toggled = commandExecutor.isToggled(menuPath, id, ...args); const unregisterCommand = this.addCommand(id, { execute: () => commandExecutor.executeCommand(menuPath, id, ...args), label: menu.label, iconClass: menu.icon, isEnabled: () => enabled, isVisible: () => visible, isToggled: () => toggled }); const bindings = keybindingRegistry.getKeybindingsForCommand(id); // Only consider the first active keybinding. if (bindings.length) { const binding = bindings.length > 1 ? (_a = bindings.find(b => !b.when || this.services.contextKeyService.match(b.when))) !== null && _a !== void 0 ? _a : bindings[0] : bindings[0]; const keys = keybindingRegistry.acceleratorFor(binding, ' ', true); this.addKeyBinding({ command: id, keys, selector: '.lm-Widget' // We have the Lumino dependency anyway. }); } return common_1.Disposable.create(() => unregisterCommand.dispose()); } } exports.MenuCommandRegistry = MenuCommandRegistry; //# sourceMappingURL=browser-menu-plugin.js.map