@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
451 lines • 19.7 kB
JavaScript
"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 WITH Classpath-exception-2.0
// *****************************************************************************
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
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;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MenuCommandRegistry = exports.BrowserMenuBarContribution = exports.DynamicMenuWidget = exports.MenuServices = exports.DynamicMenuBarWidget = exports.BrowserMainMenuFactory = exports.MenuBarWidget = void 0;
const inversify_1 = require("inversify");
const widgets_1 = require("@phosphor/widgets");
const commands_1 = require("@phosphor/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");
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, this.corePreferences.get('window.menuBarVisibility', 'classic'));
});
const preferenceListener = this.corePreferences.onPreferenceChanged(preference => {
if (preference.preferenceName === 'window.menuBarVisibility') {
this.showMenuBar(menuBar, preference.newValue);
}
});
const keybindingListener = this.keybindingRegistry.onKeybindingsChanged(() => {
const preference = this.corePreferences['window.menuBarVisibility'];
this.showMenuBar(menuBar, preference);
});
menuBar.disposed.connect(() => {
preferenceListener.dispose();
keybindingListener.dispose();
});
return menuBar;
}
showMenuBar(menuBar, preference) {
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) {
const menuModel = 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,
};
}
};
__decorate([
(0, inversify_1.inject)(context_key_service_1.ContextKeyService),
__metadata("design:type", Object)
], BrowserMainMenuFactory.prototype, "contextKeyService", void 0);
__decorate([
(0, inversify_1.inject)(context_menu_context_1.ContextMenuContext),
__metadata("design:type", context_menu_context_1.ContextMenuContext)
], BrowserMainMenuFactory.prototype, "context", void 0);
__decorate([
(0, inversify_1.inject)(common_1.CommandRegistry),
__metadata("design:type", common_1.CommandRegistry)
], BrowserMainMenuFactory.prototype, "commandRegistry", void 0);
__decorate([
(0, inversify_1.inject)(common_1.MenuCommandExecutor),
__metadata("design:type", Object)
], BrowserMainMenuFactory.prototype, "menuCommandExecutor", void 0);
__decorate([
(0, inversify_1.inject)(core_preferences_1.CorePreferences),
__metadata("design:type", Object)
], BrowserMainMenuFactory.prototype, "corePreferences", void 0);
__decorate([
(0, inversify_1.inject)(keybinding_1.KeybindingRegistry),
__metadata("design:type", keybinding_1.KeybindingRegistry)
], BrowserMainMenuFactory.prototype, "keybindingRegistry", void 0);
__decorate([
(0, inversify_1.inject)(common_1.MenuModelRegistry),
__metadata("design:type", common_1.MenuModelRegistry)
], BrowserMainMenuFactory.prototype, "menuProvider", void 0);
BrowserMainMenuFactory = __decorate([
(0, inversify_1.injectable)()
], BrowserMainMenuFactory);
exports.BrowserMainMenuFactory = BrowserMainMenuFactory;
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;
if (activeElement instanceof HTMLElement) {
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];
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 '${label}' 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 '${label}' 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);
}
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();
}
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 /* Group */ : common_1.CompoundMenuNode.getRole(menu);
if (role === 0 /* Submenu */) {
const submenu = this.services.menuWidgetFactory.createMenuWidget(menu, this.options);
if (submenu.items.length > 0) {
parentItems.push({ type: 'submenu', submenu });
}
}
else if (role === 1 /* 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) {
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) {
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;
}
};
__decorate([
(0, inversify_1.inject)(shell_1.ApplicationShell),
__metadata("design:type", shell_1.ApplicationShell)
], BrowserMenuBarContribution.prototype, "shell", void 0);
__decorate([
(0, inversify_1.inject)(preference_service_1.PreferenceService),
__metadata("design:type", Object)
], BrowserMenuBarContribution.prototype, "preferenceService", void 0);
BrowserMenuBarContribution = __decorate([
(0, inversify_1.injectable)(),
__param(0, (0, inversify_1.inject)(BrowserMainMenuFactory)),
__metadata("design:paramtypes", [BrowserMainMenuFactory])
], BrowserMenuBarContribution);
exports.BrowserMenuBarContribution = BrowserMenuBarContribution;
/**
* Stores Theia-specific action menu nodes instead of PhosphorJS 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) {
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,
icon: menu.icon,
isEnabled: () => enabled,
isVisible: () => visible,
isToggled: () => toggled
});
const bindings = keybindingRegistry.getKeybindingsForCommand(id);
// Only consider the first keybinding.
if (bindings.length) {
const binding = bindings[0];
const keys = keybindingRegistry.acceleratorFor(binding, ' ', true);
this.addKeyBinding({
command: id,
keys,
selector: '.p-Widget' // We have the PhosphorJS dependency anyway.
});
}
return common_1.Disposable.create(() => unregisterCommand.dispose());
}
}
exports.MenuCommandRegistry = MenuCommandRegistry;
//# sourceMappingURL=browser-menu-plugin.js.map