From 23ea30bd9f90d0659838e9bf014a6b4c2ccf299d Mon Sep 17 00:00:00 2001 From: Remi Schnekenburger Date: Wed, 7 Aug 2024 14:49:41 +0200 Subject: [PATCH 1/4] Support the menu contribution point "testing/profiles/context" fixes #14013 contributed on behalf of STMicroelectronics Signed-off-by: Remi Schnekenburger --- .../src/main/browser/menus/menus-contribution-handler.ts | 4 +++- .../src/main/browser/menus/plugin-menu-command-adapter.ts | 1 + .../src/main/browser/menus/vscode-theia-menu-mappings.ts | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts index 9410875ce1f1c..2c8b9ad5006bb 100644 --- a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts @@ -25,13 +25,14 @@ import { ScmWidget } from '@theia/scm/lib/browser/scm-widget'; import { QuickCommandService } from '@theia/core/lib/browser'; import { CodeEditorWidgetUtil, codeToTheiaMappings, ContributionPoint, - PLUGIN_EDITOR_TITLE_MENU, PLUGIN_EDITOR_TITLE_RUN_MENU, PLUGIN_SCM_TITLE_MENU, PLUGIN_VIEW_TITLE_MENU + PLUGIN_EDITOR_TITLE_MENU, PLUGIN_EDITOR_TITLE_RUN_MENU, PLUGIN_SCM_TITLE_MENU, PLUGIN_TEST_VIEW_TITLE_MENU, PLUGIN_VIEW_TITLE_MENU } from './vscode-theia-menu-mappings'; import { PluginMenuCommandAdapter, ReferenceCountingSet } from './plugin-menu-command-adapter'; import { ContextKeyExpr } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { PluginSharedStyle } from '../plugin-shared-style'; import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables'; +import { TestTreeWidget } from '@theia/test/lib/browser/view/test-tree-widget'; @injectable() export class MenusContributionPointHandler { @@ -62,6 +63,7 @@ export class MenusContributionPointHandler { }); this.tabBarToolbar.registerMenuDelegate(PLUGIN_SCM_TITLE_MENU, widget => widget instanceof ScmWidget); this.tabBarToolbar.registerMenuDelegate(PLUGIN_VIEW_TITLE_MENU, widget => !this.codeEditorWidgetUtil.is(widget)); + this.tabBarToolbar.registerMenuDelegate(PLUGIN_TEST_VIEW_TITLE_MENU, widget => widget instanceof TestTreeWidget); this.tabBarToolbar.registerItem({ id: 'plugin-menu-contribution-title-contribution', command: '_never_', onDidChange: this.onDidChangeTitleContributionEmitter.event }); this.contextKeyService.onDidChange(event => { if (event.affects(this.titleContributionContextKeys)) { diff --git a/packages/plugin-ext/src/main/browser/menus/plugin-menu-command-adapter.ts b/packages/plugin-ext/src/main/browser/menus/plugin-menu-command-adapter.ts index cc8be6f6fe2b2..ec91bc9ef7c95 100644 --- a/packages/plugin-ext/src/main/browser/menus/plugin-menu-command-adapter.ts +++ b/packages/plugin-ext/src/main/browser/menus/plugin-menu-command-adapter.ts @@ -108,6 +108,7 @@ export class PluginMenuCommandAdapter implements MenuCommandAdapter { ['scm/resourceState/context', toScmArgs], ['scm/title', () => [this.toScmArg(this.scmService.selectedRepository)]], ['testing/message/context', toTestMessageArgs], + ['testing/profiles/context', noArgs], ['scm/change/title', (...args) => this.toScmChangeArgs(...args)], ['timeline/item/context', (...args) => this.toTimelineArgs(...args)], ['view/item/context', (...args) => this.toTreeArgs(...args)], diff --git a/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts b/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts index 6bbc91f01bfa0..c9eb290085f71 100644 --- a/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts +++ b/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts @@ -40,6 +40,7 @@ export const PLUGIN_EDITOR_TITLE_MENU = ['plugin_editor/title']; export const PLUGIN_EDITOR_TITLE_RUN_MENU = ['plugin_editor/title/run']; export const PLUGIN_SCM_TITLE_MENU = ['plugin_scm/title']; export const PLUGIN_VIEW_TITLE_MENU = ['plugin_view/title']; +export const PLUGIN_TEST_VIEW_TITLE_MENU = ['plugin_test/title']; export const implementedVSCodeContributionPoints = [ 'comments/comment/context', @@ -62,6 +63,7 @@ export const implementedVSCodeContributionPoints = [ 'timeline/item/context', 'testing/item/context', 'testing/message/context', + 'testing/profiles/context', 'view/item/context', 'view/title', 'webview/context', @@ -93,6 +95,7 @@ export const codeToTheiaMappings = new Map([ ['scm/title', [PLUGIN_SCM_TITLE_MENU]], ['testing/item/context', [TEST_VIEW_CONTEXT_MENU]], ['testing/message/context', [TEST_RUNS_CONTEXT_MENU]], + ['testing/profiles/context', [PLUGIN_TEST_VIEW_TITLE_MENU]], ['timeline/item/context', [TIMELINE_ITEM_CONTEXT_MENU]], ['view/item/context', [VIEW_ITEM_CONTEXT_MENU]], ['view/title', [PLUGIN_VIEW_TITLE_MENU]], From 93c3d57a3db447a2e653c7331fddcd204b6d701e Mon Sep 17 00:00:00 2001 From: Remi Schnekenburger Date: Thu, 8 Aug 2024 17:05:49 +0200 Subject: [PATCH 2/4] Support the testing/profiles/context additions in the '...' additional menu rather than inline --- .../src/main/browser/menus/menus-contribution-handler.ts | 9 +++++++-- .../src/main/browser/menus/vscode-theia-menu-mappings.ts | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts index 2c8b9ad5006bb..604c5e637e9b3 100644 --- a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts @@ -24,7 +24,7 @@ import { DeployedPlugin, IconUrl, Menu } from '../../../common'; import { ScmWidget } from '@theia/scm/lib/browser/scm-widget'; import { QuickCommandService } from '@theia/core/lib/browser'; import { - CodeEditorWidgetUtil, codeToTheiaMappings, ContributionPoint, + CodeEditorWidgetUtil, codeToTheiaMappings, codeToTheiaGroupProviders, ContributionPoint, PLUGIN_EDITOR_TITLE_MENU, PLUGIN_EDITOR_TITLE_RUN_MENU, PLUGIN_SCM_TITLE_MENU, PLUGIN_TEST_VIEW_TITLE_MENU, PLUGIN_VIEW_TITLE_MENU } from './vscode-theia-menu-mappings'; import { PluginMenuCommandAdapter, ReferenceCountingSet } from './plugin-menu-command-adapter'; @@ -76,6 +76,10 @@ export class MenusContributionPointHandler { return codeToTheiaMappings.get(contributionPoint); } + private getMatchingGroup(contributionPoint: ContributionPoint, item: Menu): string | undefined { + return codeToTheiaGroupProviders.get(contributionPoint)?.(item) ?? item.group; + } + handle(plugin: DeployedPlugin): Disposable { const allMenus = plugin.contributes?.menus; if (!allMenus) { @@ -99,7 +103,8 @@ export class MenusContributionPointHandler { } else { this.checkTitleContribution(contributionPoint, item, toDispose); const targets = this.getMatchingMenu(contributionPoint as ContributionPoint) ?? [contributionPoint]; - const { group, order } = this.parseGroup(item.group); + const matchingGroup = this.getMatchingGroup(contributionPoint as ContributionPoint, item); + const { group, order } = this.parseGroup(matchingGroup); const { submenu, command } = item; if (submenu && command) { console.warn( diff --git a/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts b/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts index c9eb290085f71..6a9a6533914aa 100644 --- a/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts +++ b/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts @@ -35,6 +35,7 @@ import { EDITOR_LINENUMBER_CONTEXT_MENU } from '@theia/editor/lib/browser/editor import { TEST_VIEW_CONTEXT_MENU } from '@theia/test/lib/browser/view/test-view-contribution'; import { TEST_RUNS_CONTEXT_MENU } from '@theia/test/lib/browser/view/test-run-view-contribution'; import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution'; +import { Menu } from '../../../common'; export const PLUGIN_EDITOR_TITLE_MENU = ['plugin_editor/title']; export const PLUGIN_EDITOR_TITLE_RUN_MENU = ['plugin_editor/title/run']; @@ -106,6 +107,10 @@ export const codeToTheiaMappings = new Map([ ]); +export const codeToTheiaGroupProviders = new Map string>([ + ['testing/profiles/context', () => 'configure'] +]); + type CodeEditorWidget = EditorWidget | WebviewWidget; @injectable() export class CodeEditorWidgetUtil { From 887b522b1c0aefd7588723114c5412b4f9194b1d Mon Sep 17 00:00:00 2001 From: Remi Schnekenburger Date: Thu, 8 Aug 2024 16:56:03 +0200 Subject: [PATCH 3/4] added support for context keys overlay to fully support testin/profiles/context contributions --- .../src/browser/menu/browser-menu-plugin.ts | 6 ++- .../tab-bar-toolbar-menu-adapters.ts | 1 + .../tab-bar-toolbar-registry.ts | 14 +++++-- .../shell/tab-bar-toolbar/tab-bar-toolbar.tsx | 2 + .../core/src/common/menu/action-menu-node.ts | 2 + packages/core/src/common/menu/menu-types.ts | 8 +++- .../menus/menus-contribution-handler.ts | 37 +++++++++++++++---- .../menus/vscode-theia-menu-mappings.ts | 4 ++ 8 files changed, 61 insertions(+), 13 deletions(-) diff --git a/packages/core/src/browser/menu/browser-menu-plugin.ts b/packages/core/src/browser/menu/browser-menu-plugin.ts index d1ceac06b8220..83e6f1d615010 100644 --- a/packages/core/src/browser/menu/browser-menu-plugin.ts +++ b/packages/core/src/browser/menu/browser-menu-plugin.ts @@ -316,7 +316,11 @@ export class DynamicMenuWidget extends MenuWidget { } } else if (menu.command) { const node = menu.altNode && this.services.context.altPressed ? menu.altNode : (menu as MenuNode & CommandMenuNode); - if (commands.isVisible(node.command) && this.undefinedOrMatch(this.options.contextKeyService ?? this.services.contextKeyService, node.when, this.options.context)) { + let contextMatcher: ContextMatcher = this.options.contextKeyService || this.services.contextKeyService; + if (node.contextKeyOverlays) { + contextMatcher = this.services.contextKeyService.createOverlay(node.contextKeyOverlays.map(item => [item.key, item.value])); + } + if (commands.isVisible(node.command) && this.undefinedOrMatch(contextMatcher, node.when, this.options.context)) { parentItems.push({ command: node.command, type: 'command' diff --git a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-menu-adapters.ts b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-menu-adapters.ts index 1af64ee75bc75..06e82c72f2b11 100644 --- a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-menu-adapters.ts +++ b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-menu-adapters.ts @@ -27,5 +27,6 @@ export class ToolbarMenuNodeWrapper implements TabBarToolbarItem { get tooltip(): string | undefined { return this.menuNode.label; } get when(): string | undefined { return this.menuNode.when; } get text(): string | undefined { return (this.group === NAVIGATION || this.group === undefined) ? undefined : this.menuNode.label; } + get contextKeyOverlays(): { key: string, value: string }[] | undefined { return this.menuNode.contextKeyOverlays; } } diff --git a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.ts b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.ts index e3946be66d197..61358241c3bd8 100644 --- a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.ts +++ b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.ts @@ -18,7 +18,7 @@ import debounce = require('lodash.debounce'); import { inject, injectable, named } from 'inversify'; // eslint-disable-next-line max-len import { CommandMenuNode, CommandRegistry, CompoundMenuNode, ContributionProvider, Disposable, DisposableCollection, Emitter, Event, MenuModelRegistry, MenuNode, MenuPath } from '../../../common'; -import { ContextKeyService } from '../../context-key-service'; +import { ContextKeyService, ContextMatcher } from '../../context-key-service'; import { FrontendApplicationContribution } from '../../frontend-application-contribution'; import { Widget } from '../../widgets'; import { AnyToolbarItem, ConditionalToolbarItem, MenuDelegate, MenuToolbarItem, ReactTabBarToolbarItem, TabBarToolbarItem } from './tab-bar-toolbar-types'; @@ -112,10 +112,18 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution { const menu = this.menuRegistry.getMenu(delegate.menuPath); const children = CompoundMenuNode.getFlatChildren(menu.children); for (const child of children) { - if (!child.when || this.contextKeyService.match(child.when, widget.node)) { + let contextMatcher: ContextMatcher = this.contextKeyService; + if (child.contextKeyOverlays) { + contextMatcher = this.contextKeyService.createOverlay(child.contextKeyOverlays.map(item => [item.key, item.value])); + } + if (!child.when || contextMatcher.match(child.when, widget.node)) { if (child.children) { for (const grandchild of child.children) { - if (!grandchild.when || this.contextKeyService.match(grandchild.when, widget.node)) { + let grandChildContextMatcher = contextMatcher; + if (grandchild.contextKeyOverlays) { + grandChildContextMatcher = this.contextKeyService.createOverlay(grandchild.contextKeyOverlays.map(item => [item.key, item.value])); + } + if (!grandchild.when || grandChildContextMatcher.match(grandchild.when, widget.node)) { if (CommandMenuNode.is(grandchild)) { result.push(new ToolbarMenuNodeWrapper(grandchild, child.id, delegate.menuPath)); } else if (CompoundMenuNode.is(grandchild)) { diff --git a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar.tsx b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar.tsx index eda4d12ad9956..b572430f5400c 100644 --- a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar.tsx +++ b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar.tsx @@ -24,6 +24,7 @@ import { ACTION_ITEM, codicon, ReactWidget, Widget } from '../../widgets'; import { TabBarToolbarRegistry } from './tab-bar-toolbar-registry'; import { AnyToolbarItem, ReactTabBarToolbarItem, TabBarDelegator, TabBarToolbarItem, TAB_BAR_TOOLBAR_CONTEXT_MENU, MenuToolbarItem } from './tab-bar-toolbar-types'; import { KeybindingRegistry } from '../..//keybinding'; +import { ToolbarMenuNodeWrapper } from './tab-bar-toolbar-menu-adapters'; /** * Factory for instantiating tab-bar toolbars. @@ -297,6 +298,7 @@ export class TabBarToolbar extends ReactWidget { commandId: item.command, when: item.when, order: item.order, + contextKeyOverlays: item instanceof ToolbarMenuNodeWrapper ? item.contextKeyOverlays : undefined })); } } diff --git a/packages/core/src/common/menu/action-menu-node.ts b/packages/core/src/common/menu/action-menu-node.ts index 2da168d8c0da3..10a93c38daa0b 100644 --- a/packages/core/src/common/menu/action-menu-node.ts +++ b/packages/core/src/common/menu/action-menu-node.ts @@ -39,6 +39,8 @@ export class ActionMenuNode implements MenuNode, CommandMenuNode, Partial { +export interface MenuAction extends MenuNodeRenderingData, Pick, Pick { + /** * The command to execute. */ diff --git a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts index 604c5e637e9b3..54723145771c5 100644 --- a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts @@ -24,7 +24,7 @@ import { DeployedPlugin, IconUrl, Menu } from '../../../common'; import { ScmWidget } from '@theia/scm/lib/browser/scm-widget'; import { QuickCommandService } from '@theia/core/lib/browser'; import { - CodeEditorWidgetUtil, codeToTheiaMappings, codeToTheiaGroupProviders, ContributionPoint, + CodeEditorWidgetUtil, codeToTheiaMappings, codeToTheiaContextKeyOverlays, codeToTheiaGroupProviders, ContributionPoint, PLUGIN_EDITOR_TITLE_MENU, PLUGIN_EDITOR_TITLE_RUN_MENU, PLUGIN_SCM_TITLE_MENU, PLUGIN_TEST_VIEW_TITLE_MENU, PLUGIN_VIEW_TITLE_MENU } from './vscode-theia-menu-mappings'; import { PluginMenuCommandAdapter, ReferenceCountingSet } from './plugin-menu-command-adapter'; @@ -80,6 +80,13 @@ export class MenusContributionPointHandler { return codeToTheiaGroupProviders.get(contributionPoint)?.(item) ?? item.group; } + private getContextKeyOverlay(contributionPoint: ContributionPoint): { key: string; values: string[]; } | undefined { + if (codeToTheiaContextKeyOverlays.has(contributionPoint)) { + return codeToTheiaContextKeyOverlays.get(contributionPoint); + } + return undefined; + } + handle(plugin: DeployedPlugin): Disposable { const allMenus = plugin.contributes?.menus; if (!allMenus) { @@ -114,13 +121,27 @@ export class MenusContributionPointHandler { if (command) { toDispose.push(this.commandAdapter.addCommand(command)); targets.forEach(target => { - const node = new ActionMenuNode({ - commandId: command, - when: item.when, - order, - }, this.commands); - const parent = this.menuRegistry.getMenuNode(target, group); - toDispose.push(parent.addNode(node)); + const overlay = this.getContextKeyOverlay(contributionPoint as ContributionPoint); + if (overlay) { + overlay.values.forEach(value => { + const node = new ActionMenuNode({ + commandId: command, + when: item.when, + order, + contextKeyOverlays: [{ key: overlay.key, value: value }] + }, this.commands); + const parent = this.menuRegistry.getMenuNode(target, group); + toDispose.push(parent.addNode(node)); + }); + } else { + const node = new ActionMenuNode({ + commandId: command, + when: item.when, + order + }, this.commands); + const parent = this.menuRegistry.getMenuNode(target, group); + toDispose.push(parent.addNode(node)); + } }); } else if (submenu) { targets.forEach(target => toDispose.push(this.menuRegistry.linkSubmenu(target, submenu!, { order, when: item.when }, group))); diff --git a/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts b/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts index 6a9a6533914aa..0f15c435a10f2 100644 --- a/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts +++ b/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts @@ -111,6 +111,10 @@ export const codeToTheiaGroupProviders = new Map string> ['testing/profiles/context', () => 'configure'] ]); +export const codeToTheiaContextKeyOverlays = new Map([ + ['testing/profiles/context', { key: 'testing.profile.context.group', values: ['run', 'debug', 'coverage'] }], +]); + type CodeEditorWidget = EditorWidget | WebviewWidget; @injectable() export class CodeEditorWidgetUtil { From 46eebffc6260d5fe3239bee3670d992bc8e30136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=A4der?= Date: Wed, 21 Aug 2024 10:09:33 +0200 Subject: [PATCH 4/4] Refactored toolbar item support to - support multiple items with the same menu path, but different context - allow for items with both commands and menus - fixed menu rendering with native drop-downs - simplified typing around toolbar items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Mäder --- .../menu/sample-electron-menu-module.ts | 5 +- .../core/src/browser/context-key-service.ts | 6 +- .../src/browser/menu/browser-menu-plugin.ts | 6 +- .../tab-bar-toolbar-menu-adapters.ts | 5 +- .../tab-bar-toolbar-registry.ts | 65 ++---- .../tab-bar-toolbar/tab-bar-toolbar-types.ts | 186 ++++++------------ .../shell/tab-bar-toolbar/tab-bar-toolbar.tsx | 67 ++++--- packages/core/src/browser/style/tabs.css | 37 ++-- .../core/src/browser/style/view-container.css | 7 - packages/core/src/browser/view-container.ts | 4 +- .../core/src/common/menu/action-menu-node.ts | 2 - packages/core/src/common/menu/menu-types.ts | 7 +- .../menu/electron-main-menu-factory.ts | 13 +- .../src/electron-main/electron-api-main.ts | 15 +- ...debug-frontend-application-contribution.ts | 4 +- packages/git/src/browser/git-contribution.ts | 2 +- .../src/browser/monaco-context-key-service.ts | 4 +- .../src/browser/navigator-contribution.ts | 4 +- .../menus/menus-contribution-handler.ts | 49 ++--- .../menus/vscode-theia-menu-mappings.ts | 12 +- .../browser/view/test-view-contribution.ts | 9 + .../toolbar/src/browser/toolbar-controller.ts | 3 +- .../toolbar/src/browser/toolbar-interfaces.ts | 4 +- packages/toolbar/src/browser/toolbar.tsx | 10 +- 24 files changed, 204 insertions(+), 322 deletions(-) diff --git a/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts b/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts index d9f7e657660a7..d8e3e75183e2a 100644 --- a/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts +++ b/examples/api-samples/src/electron-browser/menu/sample-electron-menu-module.ts @@ -29,12 +29,13 @@ class SampleElectronMainMenuFactory extends ElectronMainMenuFactory { protected override fillMenuTemplate(parentItems: MenuDto[], menu: MenuNode, args: unknown[] = [], - options: ElectronMenuOptions + options: ElectronMenuOptions, + skipRoot: boolean ): MenuDto[] { if (menu instanceof PlaceholderMenuNode) { parentItems.push({ label: menu.label, enabled: false, visible: true }); } else { - super.fillMenuTemplate(parentItems, menu, args, options); + super.fillMenuTemplate(parentItems, menu, args, options, skipRoot); } return parentItems; } diff --git a/packages/core/src/browser/context-key-service.ts b/packages/core/src/browser/context-key-service.ts index 0bb30ad313df4..c250c18999948 100644 --- a/packages/core/src/browser/context-key-service.ts +++ b/packages/core/src/browser/context-key-service.ts @@ -16,6 +16,7 @@ import { injectable } from 'inversify'; import { Emitter, Event } from '../common/event'; +import { Disposable } from '../common'; export type ContextKeyValue = null | undefined | boolean | number | string | Array @@ -83,11 +84,10 @@ export interface ContextKeyService extends ContextMatcher { setContext(key: string, value: unknown): void; } -export type ScopedValueStore = Omit; +export type ScopedValueStore = Omit & Disposable; @injectable() export class ContextKeyServiceDummyImpl implements ContextKeyService { - protected readonly onDidChangeEmitter = new Emitter(); readonly onDidChange = this.onDidChangeEmitter.event; protected fireDidChange(event: ContextKeyChangeEvent): void { @@ -122,7 +122,7 @@ export class ContextKeyServiceDummyImpl implements ContextKeyService { /** * Details should implemented by an extension, e.g. by the monaco extension. */ - createScoped(target: HTMLElement): ContextKeyService { + createScoped(target: HTMLElement): ScopedValueStore { return this; } diff --git a/packages/core/src/browser/menu/browser-menu-plugin.ts b/packages/core/src/browser/menu/browser-menu-plugin.ts index 83e6f1d615010..d1ceac06b8220 100644 --- a/packages/core/src/browser/menu/browser-menu-plugin.ts +++ b/packages/core/src/browser/menu/browser-menu-plugin.ts @@ -316,11 +316,7 @@ export class DynamicMenuWidget extends MenuWidget { } } else if (menu.command) { const node = menu.altNode && this.services.context.altPressed ? menu.altNode : (menu as MenuNode & CommandMenuNode); - let contextMatcher: ContextMatcher = this.options.contextKeyService || this.services.contextKeyService; - if (node.contextKeyOverlays) { - contextMatcher = this.services.contextKeyService.createOverlay(node.contextKeyOverlays.map(item => [item.key, item.value])); - } - if (commands.isVisible(node.command) && this.undefinedOrMatch(contextMatcher, node.when, this.options.context)) { + if (commands.isVisible(node.command) && this.undefinedOrMatch(this.options.contextKeyService ?? this.services.contextKeyService, node.when, this.options.context)) { parentItems.push({ command: node.command, type: 'command' diff --git a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-menu-adapters.ts b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-menu-adapters.ts index 06e82c72f2b11..76edc12e0a1f9 100644 --- a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-menu-adapters.ts +++ b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-menu-adapters.ts @@ -15,11 +15,11 @@ // ***************************************************************************** import { MenuNode, MenuPath } from '../../../common'; -import { NAVIGATION, TabBarToolbarItem } from './tab-bar-toolbar-types'; +import { NAVIGATION, RenderedToolbarItem } from './tab-bar-toolbar-types'; export const TOOLBAR_WRAPPER_ID_SUFFIX = '-as-tabbar-toolbar-item'; -export class ToolbarMenuNodeWrapper implements TabBarToolbarItem { +export class ToolbarMenuNodeWrapper implements RenderedToolbarItem { constructor(protected readonly menuNode: MenuNode, readonly group?: string, readonly menuPath?: MenuPath) { } get id(): string { return this.menuNode.id + TOOLBAR_WRAPPER_ID_SUFFIX; } get command(): string { return this.menuNode.command ?? ''; }; @@ -27,6 +27,5 @@ export class ToolbarMenuNodeWrapper implements TabBarToolbarItem { get tooltip(): string | undefined { return this.menuNode.label; } get when(): string | undefined { return this.menuNode.when; } get text(): string | undefined { return (this.group === NAVIGATION || this.group === undefined) ? undefined : this.menuNode.label; } - get contextKeyOverlays(): { key: string, value: string }[] | undefined { return this.menuNode.contextKeyOverlays; } } diff --git a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.ts b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.ts index 61358241c3bd8..5851830895b23 100644 --- a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.ts +++ b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-registry.ts @@ -17,11 +17,11 @@ import debounce = require('lodash.debounce'); import { inject, injectable, named } from 'inversify'; // eslint-disable-next-line max-len -import { CommandMenuNode, CommandRegistry, CompoundMenuNode, ContributionProvider, Disposable, DisposableCollection, Emitter, Event, MenuModelRegistry, MenuNode, MenuPath } from '../../../common'; -import { ContextKeyService, ContextMatcher } from '../../context-key-service'; +import { CommandRegistry, ContributionProvider, Disposable, DisposableCollection, Emitter, Event, MenuModelRegistry, MenuNode, MenuPath } from '../../../common'; +import { ContextKeyService } from '../../context-key-service'; import { FrontendApplicationContribution } from '../../frontend-application-contribution'; import { Widget } from '../../widgets'; -import { AnyToolbarItem, ConditionalToolbarItem, MenuDelegate, MenuToolbarItem, ReactTabBarToolbarItem, TabBarToolbarItem } from './tab-bar-toolbar-types'; +import { MenuDelegate, ReactTabBarToolbarItem, RenderedToolbarItem, TabBarToolbarItem } from './tab-bar-toolbar-types'; import { ToolbarMenuNodeWrapper } from './tab-bar-toolbar-menu-adapters'; /** @@ -75,7 +75,7 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution { * * @param item the item to register. */ - registerItem(item: TabBarToolbarItem | ReactTabBarToolbarItem): Disposable { + registerItem(item: RenderedToolbarItem | ReactTabBarToolbarItem): Disposable { const { id } = item; if (this.items.has(id)) { throw new Error(`A toolbar item is already registered with the '${id}' ID.`); @@ -110,32 +110,17 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution { for (const delegate of this.menuDelegates.values()) { if (delegate.isVisible(widget)) { const menu = this.menuRegistry.getMenu(delegate.menuPath); - const children = CompoundMenuNode.getFlatChildren(menu.children); - for (const child of children) { - let contextMatcher: ContextMatcher = this.contextKeyService; - if (child.contextKeyOverlays) { - contextMatcher = this.contextKeyService.createOverlay(child.contextKeyOverlays.map(item => [item.key, item.value])); - } - if (!child.when || contextMatcher.match(child.when, widget.node)) { + for (const child of menu.children) { + if (!child.when || this.contextKeyService.match(child.when, widget.node)) { if (child.children) { for (const grandchild of child.children) { - let grandChildContextMatcher = contextMatcher; - if (grandchild.contextKeyOverlays) { - grandChildContextMatcher = this.contextKeyService.createOverlay(grandchild.contextKeyOverlays.map(item => [item.key, item.value])); - } - if (!grandchild.when || grandChildContextMatcher.match(grandchild.when, widget.node)) { - if (CommandMenuNode.is(grandchild)) { - result.push(new ToolbarMenuNodeWrapper(grandchild, child.id, delegate.menuPath)); - } else if (CompoundMenuNode.is(grandchild)) { - let menuPath; - if (menuPath = this.menuRegistry.getPath(grandchild)) { - result.push(new ToolbarMenuNodeWrapper(grandchild, child.id, menuPath)); - } - } + if (!grandchild.when || this.contextKeyService.match(grandchild.when, widget.node)) { + const menuPath = this.menuRegistry.getPath(grandchild); + result.push(new ToolbarMenuNodeWrapper(grandchild, child.id, menuPath)); } } } else if (child.command) { - result.push(new ToolbarMenuNodeWrapper(child, '', delegate.menuPath)); + result.push(new ToolbarMenuNodeWrapper(child, '')); } } } @@ -153,15 +138,17 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution { * @returns `false` if the `item` should be suppressed, otherwise `true` */ protected isItemVisible(item: TabBarToolbarItem | ReactTabBarToolbarItem, widget: Widget): boolean { - if (TabBarToolbarItem.is(item) && item.command && !this.isTabBarToolbarItemVisible(item, widget)) { + if (!this.isConditionalItemVisible(item, widget)) { return false; } - if (MenuToolbarItem.is(item) && !this.isMenuToolbarItemVisible(item, widget)) { + + if (item.command && !this.commandRegistry.isVisible(item.command, widget)) { return false; } - if (AnyToolbarItem.isConditional(item) && !this.isConditionalItemVisible(item, widget)) { + if (item.menuPath && !this.isNonEmptyMenu(item, widget)) { return false; } + // The item is not vetoed. Accept it return true; } @@ -174,7 +161,7 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution { * @param widget the widget that is updating the toolbar * @returns `false` if the `item` should be suppressed, otherwise `true` */ - protected isConditionalItemVisible(item: ConditionalToolbarItem, widget: Widget): boolean { + protected isConditionalItemVisible(item: TabBarToolbarItem, widget: Widget): boolean { if (item.isVisible && !item.isVisible(widget)) { return false; } @@ -184,19 +171,6 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution { return true; } - /** - * Query whether a tab-bar toolbar `item` that has a command should be shown in the toolbar. - * This implementation returns `false` if the `item`'s command is not visible in the - * `widget` according to the command registry. - * - * @param item a tab-bar toolbar item that has a non-empty `command` - * @param widget the widget that is updating the toolbar - * @returns `false` if the `item` should be suppressed, otherwise `true` - */ - protected isTabBarToolbarItemVisible(item: TabBarToolbarItem, widget: Widget): boolean { - return this.commandRegistry.isVisible(item.command, widget); - } - /** * Query whether a menu toolbar `item` should be shown in the toolbar. * This implementation returns `false` if the `item` does not have any actual menu to show. @@ -205,7 +179,10 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution { * @param widget the widget that is updating the toolbar * @returns `false` if the `item` should be suppressed, otherwise `true` */ - protected isMenuToolbarItemVisible(item: MenuToolbarItem, widget: Widget): boolean { + isNonEmptyMenu(item: TabBarToolbarItem, widget: Widget | undefined): boolean { + if (!item.menuPath) { + return false; + } const menu = this.menuRegistry.getMenu(item.menuPath); const isVisible: (node: MenuNode) => boolean = node => node.children?.length @@ -228,7 +205,7 @@ export class TabBarToolbarRegistry implements FrontendApplicationContribution { } } - registerMenuDelegate(menuPath: MenuPath, when?: string | ((widget: Widget) => boolean)): Disposable { + registerMenuDelegate(menuPath: MenuPath, when?: ((widget: Widget) => boolean)): Disposable { const id = this.toElementId(menuPath); if (!this.menuDelegates.has(id)) { const isVisible: MenuDelegate['isVisible'] = !when diff --git a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-types.ts b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-types.ts index 00ad879b4d761..e59f9f63c2384 100644 --- a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-types.ts +++ b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar-types.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import * as React from 'react'; -import { ArrayUtils, Event, isFunction, isObject, isString, MenuPath } from '../../../common'; +import { ArrayUtils, Event, isFunction, isObject, MenuPath } from '../../../common'; import { Widget } from '../../widgets'; /** Items whose group is exactly 'navigation' will be rendered inline. */ @@ -32,58 +32,21 @@ export namespace TabBarDelegator { } } -interface RegisteredToolbarItem { +export type TabBarToolbarItem = RenderedToolbarItem | ReactTabBarToolbarItem; + +/** + * Representation of an item in the tab + */ +export interface TabBarToolbarItemBase { /** * The unique ID of the toolbar item. */ id: string; -} - -interface RenderedToolbarItem { - /** - * Optional icon for the item. - */ - icon?: string | (() => string); - - /** - * Optional text of the item. - * - * Strings in the format `$(iconIdentifier~animationType) will be treated as icon references. - * If the iconIdentifier begins with fa-, Font Awesome icons will be used; otherwise it will be treated as Codicon name. - * - * You can find Codicon classnames here: https://microsoft.github.io/vscode-codicons/dist/codicon.html - * You can find Font Awesome classnames here: http://fontawesome.io/icons/ - * The type of animation can be either `spin` or `pulse`. - */ - text?: string; - - /** - * Optional tooltip for the item. - */ - tooltip?: string; -} - -interface SelfRenderingToolbarItem { - render(widget?: Widget): React.ReactNode; -} - -interface ExecutableToolbarItem { /** * The command to execute when the item is selected. */ - command: string; -} - -export interface MenuToolbarItem { - /** - * A menu path with which this item is associated. - * If accompanied by a command, this data will be passed to the {@link MenuCommandExecutor}. - * If no command is present, this menu will be opened. - */ - menuPath: MenuPath; -} + command?: string; -export interface ConditionalToolbarItem { /** * https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts */ @@ -98,61 +61,67 @@ export interface ConditionalToolbarItem { * Note: currently, each item of the container toolbar will be re-rendered if any of the items have changed. */ onDidChange?: Event; -} -interface InlineToolbarItemMetadata { /** * Priority among the items. Can be negative. The smaller the number the left-most the item will be placed in the toolbar. It is `0` by default. */ priority?: number; - group: 'navigation' | undefined; -} - -interface MenuToolbarItemMetadata { + group?: string; /** - * Optional group for the item. Default `navigation`. - * `navigation` group will be inlined, while all the others will appear in the `...` dropdown. - * A group in format `submenu_group_1/submenu 1/.../submenu_group_n/ submenu n/item_group` means that the item will be located in a submenu(s) of the `...` dropdown. - * The submenu's title is named by the submenu section name, e.g. `group//subgroup`. + * A menu path with which this item is associated. + * If accompanied by a command, this data will be passed to the {@link MenuCommandExecutor}. + * If no command is present, this menu will be opened. */ - group: string; + menuPath?: MenuPath; + contextKeyOverlays?: Record; /** * Optional ordering string for placing the item within its group */ order?: string; } -/** - * Representation of an item in the tab - */ -export interface TabBarToolbarItem extends RegisteredToolbarItem, - ExecutableToolbarItem, - RenderedToolbarItem, - Omit, - Pick, - Partial, - Partial { } +export interface RenderedToolbarItem extends TabBarToolbarItemBase { + /** + * Optional icon for the item. + */ + icon?: string | (() => string); + + /** + * Optional text of the item. + * + * Strings in the format `$(iconIdentifier~animationType) will be treated as icon references. + * If the iconIdentifier begins with fa-, Font Awesome icons will be used; otherwise it will be treated as Codicon name. + * + * You can find Codicon classnames here: https://microsoft.github.io/vscode-codicons/dist/codicon.html + * You can find Font Awesome classnames here: http://fontawesome.io/icons/ + * The type of animation can be either `spin` or `pulse`. + */ + text?: string; + + /** + * Optional tooltip for the item. + */ + tooltip?: string; +} /** * Tab-bar toolbar item backed by a `React.ReactNode`. * Unlike the `TabBarToolbarItem`, this item is not connected to the command service. */ -export interface ReactTabBarToolbarItem extends RegisteredToolbarItem, - SelfRenderingToolbarItem, - ConditionalToolbarItem, - Pick, - Pick, 'group'> { } - -export interface AnyToolbarItem extends RegisteredToolbarItem, - Partial, - Partial, - Partial, - Partial, - Partial, - Pick, - Partial { } - -export interface MenuDelegate extends MenuToolbarItem, Required> { } +export interface ReactTabBarToolbarItem extends TabBarToolbarItemBase { + render(widget?: Widget): React.ReactNode; +} + +export namespace ReactTabBarToolbarItem { + export function is(item: TabBarToolbarItem): item is ReactTabBarToolbarItem { + return isObject(item) && typeof item.render === 'function'; + } +} + +export interface MenuDelegate { + menuPath: MenuPath; + isVisible(widget?: Widget): boolean; +} export namespace TabBarToolbarItem { @@ -160,48 +129,17 @@ export namespace TabBarToolbarItem { * Compares the items by `priority` in ascending. Undefined priorities will be treated as `0`. */ export const PRIORITY_COMPARATOR = (left: TabBarToolbarItem, right: TabBarToolbarItem) => { - const leftGroup = left.group ?? NAVIGATION; - const rightGroup = right.group ?? NAVIGATION; - if (leftGroup === NAVIGATION && rightGroup !== NAVIGATION) { return ArrayUtils.Sort.LeftBeforeRight; } - if (rightGroup === NAVIGATION && leftGroup !== NAVIGATION) { return ArrayUtils.Sort.RightBeforeLeft; } - if (leftGroup !== rightGroup) { return leftGroup.localeCompare(rightGroup); } + const leftGroup: string = left.group ?? NAVIGATION; + const rightGroup: string = right.group ?? NAVIGATION; + if (leftGroup === NAVIGATION && rightGroup !== NAVIGATION) { + return ArrayUtils.Sort.LeftBeforeRight; + } + if (rightGroup === NAVIGATION && leftGroup !== NAVIGATION) { + return ArrayUtils.Sort.RightBeforeLeft; + } + if (leftGroup !== rightGroup) { + return leftGroup.localeCompare(rightGroup); + } return (left.priority || 0) - (right.priority || 0); }; - - export function is(arg: unknown): arg is TabBarToolbarItem { - return isObject(arg) && isString(arg.command); - } - -} - -export namespace MenuToolbarItem { - /** - * Type guard for a toolbar item that actually is a menu item, amongst - * the other kinds of item that it may also be. - * - * @param item a toolbar item - * @returns whether the `item` is a menu item - */ - export function is(item: T): item is T & MenuToolbarItem { - return Array.isArray(item.menuPath); - } - - export function getMenuPath(item: AnyToolbarItem): MenuPath | undefined { - return Array.isArray(item.menuPath) ? item.menuPath : undefined; - } -} - -export namespace AnyToolbarItem { - /** - * Type guard for a toolbar item that actually manifests any of the - * features of a conditional toolbar item. - * - * @param item a toolbar item - * @returns whether the `item` is a conditional item - */ - export function isConditional(item: T): item is T & ConditionalToolbarItem { - return 'isVisible' in item && typeof item.isVisible === 'function' - || 'onDidChange' in item && typeof item.onDidChange === 'function' - || 'when' in item && typeof item.when === 'string'; - } } diff --git a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar.tsx b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar.tsx index b572430f5400c..6d4f21b3d3264 100644 --- a/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar.tsx +++ b/packages/core/src/browser/shell/tab-bar-toolbar/tab-bar-toolbar.tsx @@ -16,15 +16,14 @@ import { inject, injectable, postConstruct } from 'inversify'; import * as React from 'react'; -import { ContextKeyService } from '../../context-key-service'; +import { ContextKeyService, ContextMatcher } from '../../context-key-service'; import { CommandRegistry, Disposable, DisposableCollection, MenuCommandExecutor, MenuModelRegistry, MenuPath, nls } from '../../../common'; import { Anchor, ContextMenuAccess, ContextMenuRenderer } from '../../context-menu-renderer'; import { LabelIcon, LabelParser } from '../../label-parser'; import { ACTION_ITEM, codicon, ReactWidget, Widget } from '../../widgets'; import { TabBarToolbarRegistry } from './tab-bar-toolbar-registry'; -import { AnyToolbarItem, ReactTabBarToolbarItem, TabBarDelegator, TabBarToolbarItem, TAB_BAR_TOOLBAR_CONTEXT_MENU, MenuToolbarItem } from './tab-bar-toolbar-types'; +import { ReactTabBarToolbarItem, TabBarDelegator, TabBarToolbarItem, TAB_BAR_TOOLBAR_CONTEXT_MENU, RenderedToolbarItem } from './tab-bar-toolbar-types'; import { KeybindingRegistry } from '../..//keybinding'; -import { ToolbarMenuNodeWrapper } from './tab-bar-toolbar-menu-adapters'; /** * Factory for instantiating tab-bar toolbars. @@ -88,7 +87,7 @@ export class TabBarToolbar extends ReactWidget { const contextKeys = new Set(); for (const item of items.sort(TabBarToolbarItem.PRIORITY_COMPARATOR).reverse()) { - if ('command' in item) { + if (item.command) { this.commands.getAllHandlers(item.command).forEach(handler => { if (handler.onDidChangeEnabled) { this.toDisposeOnUpdateItems.push(handler.onDidChangeEnabled(() => this.maybeUpdate())); @@ -155,9 +154,13 @@ export class TabBarToolbar extends ReactWidget { this.keybindingContextKeys.clear(); return {this.renderMore()} - {[...this.inline.values()].map(item => TabBarToolbarItem.is(item) - ? (MenuToolbarItem.is(item) && !item.command ? this.renderMenuItem(item) : this.renderItem(item)) - : item.render(this.current))} + {[...this.inline.values()].map(item => { + if (ReactTabBarToolbarItem.is(item)) { + return item.render(this.current); + } else { + return (item.menuPath && this.toolbarRegistry.isNonEmptyMenu(item, this.current) ? this.renderMenuItem(item) : this.renderItem(item)); + } + })} ; } @@ -181,7 +184,7 @@ export class TabBarToolbar extends ReactWidget { return result; } - protected renderItem(item: AnyToolbarItem): React.ReactNode { + protected renderItem(item: RenderedToolbarItem): React.ReactNode { let innerText = ''; const classNames = []; const command = item.command ? this.commands.getCommand(item.command) : undefined; @@ -223,13 +226,13 @@ export class TabBarToolbar extends ReactWidget { onMouseUp={this.onMouseUpEvent} onMouseOut={this.onMouseUpEvent} >
this.executeCommand(e, item)} title={tooltip}>{innerText}
; } - protected isEnabled(item: AnyToolbarItem): boolean { + protected isEnabled(item: TabBarToolbarItem): boolean { if (!!item.command) { return this.commandIsEnabled(item.command) && this.evaluateWhenClause(item.when); } else { @@ -237,7 +240,7 @@ export class TabBarToolbar extends ReactWidget { } } - protected getToolbarItemClassNames(item: AnyToolbarItem): string[] { + protected getToolbarItemClassNames(item: TabBarToolbarItem): string[] { const classNames = [TabBarToolbar.Styles.TAB_BAR_TOOLBAR_ITEM]; if (item.command) { if (this.isEnabled(item)) { @@ -280,7 +283,7 @@ export class TabBarToolbar extends ReactWidget { if (subpath) { toDisposeOnHide.push(this.menus.linkSubmenu(TAB_BAR_TOOLBAR_CONTEXT_MENU, subpath)); } else { - for (const item of this.more.values() as IterableIterator) { + for (const item of this.more.values()) { if (item.menuPath && !item.command) { toDisposeOnHide.push(this.menus.linkSubmenu(TAB_BAR_TOOLBAR_CONTEXT_MENU, item.menuPath, undefined, item.group)); } else if (item.command) { @@ -294,11 +297,10 @@ export class TabBarToolbar extends ReactWidget { } } toDisposeOnHide.push(this.menus.registerMenuAction([...TAB_BAR_TOOLBAR_CONTEXT_MENU, ...item.group!.split('/')], { - label: item.tooltip, + label: (item as RenderedToolbarItem).tooltip, commandId: item.command, when: item.when, - order: item.order, - contextKeyOverlays: item instanceof ToolbarMenuNodeWrapper ? item.contextKeyOverlays : undefined + order: item.order })); } } @@ -320,14 +322,26 @@ export class TabBarToolbar extends ReactWidget { * @param item a toolbar item that is a menu item * @returns the rendered toolbar item */ - protected renderMenuItem(item: TabBarToolbarItem & MenuToolbarItem): React.ReactNode { - const icon = typeof item.icon === 'function' ? item.icon() : item.icon ?? 'ellipsis'; + protected renderMenuItem(item: RenderedToolbarItem): React.ReactNode { + const command = item.command ? this.commands.getCommand(item.command) : undefined; + const icon = (typeof item.icon === 'function' && item.icon()) || item.icon as string || (command && command.iconClass) || 'ellipsis'; + + let contextMatcher: ContextMatcher = this.contextKeyService; + if (item.contextKeyOverlays) { + contextMatcher = this.contextKeyService.createOverlay(Object.keys(item.contextKeyOverlays).map(key => [key, item.contextKeyOverlays![key]])); + } + return
-
-
+ > +
this.executeCommand(e, item)} + /> +
this.showPopupMenu(item.menuPath!, event, contextMatcher)}> +
+
+
; } @@ -338,11 +352,11 @@ export class TabBarToolbar extends ReactWidget { * @param menuPath the path of the registered menu to show * @param event the mouse event triggering the menu */ - protected showPopupMenu = (menuPath: MenuPath, event: React.MouseEvent) => { + protected showPopupMenu = (menuPath: MenuPath, event: React.MouseEvent, contextMatcher: ContextMatcher) => { event.stopPropagation(); event.preventDefault(); const anchor = this.toAnchor(event); - this.renderPopupMenu(menuPath, anchor); + this.renderPopupMenu(menuPath, anchor, contextMatcher); }; /** @@ -352,7 +366,7 @@ export class TabBarToolbar extends ReactWidget { * @param anchor a description of where to render the menu * @returns platform-specific access to the rendered context menu */ - protected renderPopupMenu(menuPath: MenuPath, anchor: Anchor): ContextMenuAccess { + protected renderPopupMenu(menuPath: MenuPath, anchor: Anchor, contextMatcher: ContextMatcher): ContextMenuAccess { const toDisposeOnHide = new DisposableCollection(); this.addClass('menu-open'); toDisposeOnHide.push(Disposable.create(() => this.removeClass('menu-open'))); @@ -362,6 +376,7 @@ export class TabBarToolbar extends ReactWidget { args: [this.current], anchor, context: this.current?.node, + contextKeyService: contextMatcher, onHide: () => toDisposeOnHide.dispose() }); } @@ -382,12 +397,10 @@ export class TabBarToolbar extends ReactWidget { return whenClause ? this.contextKeyService.match(whenClause, this.current?.node) : true; } - protected executeCommand = (e: React.MouseEvent) => { + protected executeCommand(e: React.MouseEvent, item: TabBarToolbarItem): void { e.preventDefault(); e.stopPropagation(); - const item: AnyToolbarItem | undefined = this.inline.get(e.currentTarget.id); - if (!item || !this.isEnabled(item)) { return; } diff --git a/packages/core/src/browser/style/tabs.css b/packages/core/src/browser/style/tabs.css index 18db27f235de4..d81927dca9e90 100644 --- a/packages/core/src/browser/style/tabs.css +++ b/packages/core/src/browser/style/tabs.css @@ -464,11 +464,16 @@ } .p-TabBar-toolbar .item { - display: flex; - align-items: center; - margin-left: 4px; /* `padding` + `margin-right` from the container toolbar */ opacity: var(--theia-mod-disabled-opacity); cursor: default; + display: flex; + flex-direction: row; + column-gap: 0px; + align-items: centery; +} + +.p-TabBar-toolbar .item>div { + height: 100%; } .p-TabBar-toolbar .item.enabled { @@ -476,10 +481,6 @@ cursor: pointer; } -.p-TabBar-toolbar .item.enabled .action-label::before { - display: flex; -} - .p-TabBar-toolbar :not(.item.enabled) .action-label { background: transparent; cursor: default; @@ -491,8 +492,8 @@ } .p-TabBar-toolbar .item > div { - height: 18px; - width: 18px; + line-height: calc(var(--theia-icon-size)+2px); + height: calc(var(--theia-icon-size)+2px); background-repeat: no-repeat; line-height: 18px; } @@ -522,25 +523,13 @@ background: var(--theia-icon-close) no-repeat; } -/** Configure layout of a toolbar item that shows a pop-up menu. */ -.p-TabBar-toolbar .item.menu { - display: grid; -} - -/** The elements of the item that shows a pop-up menu are stack atop one other. */ -.p-TabBar-toolbar .item.menu > div { - grid-area: 1 / 1; -} - /** * The chevron for the pop-up menu indication is shrunk and * stuffed in the bottom-right corner. */ -.p-TabBar-toolbar .item.menu > .chevron { - scale: 50%; - align-self: end; - justify-self: end; - translate: 5px 3px; +.p-TabBar-toolbar .item.menu .chevron { + font-size: 8px; + vertical-align: bottom; } #theia-main-content-panel diff --git a/packages/core/src/browser/style/view-container.css b/packages/core/src/browser/style/view-container.css index f3e6ce00bc5d2..59a9f9b5bd7e6 100644 --- a/packages/core/src/browser/style/view-container.css +++ b/packages/core/src/browser/style/view-container.css @@ -168,13 +168,6 @@ padding-right: calc(var(--theia-ui-padding) * 2 / 3); } -.theia-view-container-part-title .item > div { - height: var(--theia-icon-size); - width: var(--theia-icon-size); - background-size: var(--theia-icon-size); - line-height: var(--theia-icon-size); -} - .theia-view-container-part-title { display: none; } diff --git a/packages/core/src/browser/view-container.ts b/packages/core/src/browser/view-container.ts index b45fe54169901..ef39d86d12980 100644 --- a/packages/core/src/browser/view-container.ts +++ b/packages/core/src/browser/view-container.ts @@ -29,7 +29,7 @@ import { MAIN_AREA_ID, BOTTOM_AREA_ID } from './shell/theia-dock-panel'; import { FrontendApplicationStateService } from './frontend-application-state'; import { ContextMenuRenderer, Anchor } from './context-menu-renderer'; import { parseCssMagnitude } from './browser'; -import { TabBarToolbarRegistry, TabBarToolbarFactory, TabBarToolbar, TabBarDelegator, TabBarToolbarItem } from './shell/tab-bar-toolbar'; +import { TabBarToolbarRegistry, TabBarToolbarFactory, TabBarToolbar, TabBarDelegator, RenderedToolbarItem } from './shell/tab-bar-toolbar'; import { isEmpty, isObject, nls } from '../common'; import { WidgetManager } from './widget-manager'; import { Key } from './keys'; @@ -324,7 +324,7 @@ export class ViewContainer extends BaseWidget implements StatefulWidget, Applica return 'view'; } - protected registerToolbarItem(commandId: string, options?: Partial>): void { + protected registerToolbarItem(commandId: string, options?: Partial>): void { const newId = `${this.id}-tabbar-toolbar-${commandId}`; const existingHandler = this.commandRegistry.getAllHandlers(commandId)[0]; const existingCommand = this.commandRegistry.getCommand(commandId); diff --git a/packages/core/src/common/menu/action-menu-node.ts b/packages/core/src/common/menu/action-menu-node.ts index 10a93c38daa0b..2da168d8c0da3 100644 --- a/packages/core/src/common/menu/action-menu-node.ts +++ b/packages/core/src/common/menu/action-menu-node.ts @@ -39,8 +39,6 @@ export class ActionMenuNode implements MenuNode, CommandMenuNode, Partial, Pick { +export interface MenuAction extends MenuNodeRenderingData, Pick { /** * The command to execute. diff --git a/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts b/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts index 68a305c29b094..4f0b97a51a113 100644 --- a/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts +++ b/packages/core/src/electron-browser/menu/electron-main-menu-factory.ts @@ -117,7 +117,7 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { const maxWidget = document.getElementsByClassName(MAXIMIZED_CLASS); if (preference === 'visible' || (preference === 'classic' && maxWidget.length === 0)) { const menuModel = this.menuProvider.getMenu(MAIN_MENU_BAR); - this._menu = this.fillMenuTemplate([], menuModel, [], { honorDisabled: false, rootMenuPath: MAIN_MENU_BAR }); + this._menu = this.fillMenuTemplate([], menuModel, [], { honorDisabled: false, rootMenuPath: MAIN_MENU_BAR }, false); if (isOSX) { this._menu.unshift(this.createOSXMenu()); } @@ -130,13 +130,14 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { createElectronContextMenu(menuPath: MenuPath, args?: any[], context?: HTMLElement, contextKeyService?: ContextMatcher, skipSingleRootNode?: boolean): MenuDto[] { const menuModel = skipSingleRootNode ? this.menuProvider.removeSingleRootNode(this.menuProvider.getMenu(menuPath), menuPath) : this.menuProvider.getMenu(menuPath); - return this.fillMenuTemplate([], menuModel, args, { showDisabled: true, context, rootMenuPath: menuPath, contextKeyService }); + return this.fillMenuTemplate([], menuModel, args, { showDisabled: true, context, rootMenuPath: menuPath, contextKeyService }, true); } protected fillMenuTemplate(parentItems: MenuDto[], menu: MenuNode, args: unknown[] = [], - options: ElectronMenuOptions + options: ElectronMenuOptions, + skipRoot: boolean ): MenuDto[] { const showDisabled = options?.showDisabled !== false; const honorDisabled = options?.honorDisabled !== false; @@ -148,13 +149,13 @@ export class ElectronMainMenuFactory extends BrowserMainMenuFactory { } const children = CompoundMenuNode.getFlatChildren(menu.children); const myItems: MenuDto[] = []; - children.forEach(child => this.fillMenuTemplate(myItems, child, args, options)); + children.forEach(child => this.fillMenuTemplate(myItems, child, args, options, false)); if (myItems.length === 0) { return parentItems; } - if (role === CompoundMenuNodeRole.Submenu) { + if (!skipRoot && role === CompoundMenuNodeRole.Submenu) { parentItems.push({ label: menu.label, submenu: myItems }); - } else if (role === CompoundMenuNodeRole.Group && menu.id !== 'inline') { + } else { if (parentItems.length && parentItems[parentItems.length - 1].type !== 'separator') { parentItems.push({ type: 'separator' }); } diff --git a/packages/core/src/electron-main/electron-api-main.ts b/packages/core/src/electron-main/electron-api-main.ts index d5966536dd470..b6a600e4d3a6d 100644 --- a/packages/core/src/electron-main/electron-api-main.ts +++ b/packages/core/src/electron-main/electron-api-main.ts @@ -241,9 +241,20 @@ export class TheiaMainApi implements ElectronMainApplicationContribution { }); } + private isASCI(accelerator: string | undefined): boolean { + if (typeof accelerator !== 'string') { + return false; + } + for (let i = 0; i < accelerator.length; i++) { + if (accelerator.charCodeAt(i) > 127) { + return false; + } + } + return true; + } + fromMenuDto(sender: WebContents, menuId: number, menuDto: InternalMenuDto[]): MenuItemConstructorOptions[] { return menuDto.map(dto => { - const result: MenuItemConstructorOptions = { id: dto.id, label: dto.label, @@ -252,7 +263,7 @@ export class TheiaMainApi implements ElectronMainApplicationContribution { enabled: dto.enabled, visible: dto.visible, role: dto.role, - accelerator: dto.accelerator + accelerator: this.isASCI(dto.accelerator) ? dto.accelerator : undefined }; if (dto.submenu) { result.submenu = this.fromMenuDto(sender, menuId, dto.submenu); diff --git a/packages/debug/src/browser/debug-frontend-application-contribution.ts b/packages/debug/src/browser/debug-frontend-application-contribution.ts index 5e833a8a39f29..51401d39a53ec 100644 --- a/packages/debug/src/browser/debug-frontend-application-contribution.ts +++ b/packages/debug/src/browser/debug-frontend-application-contribution.ts @@ -42,7 +42,7 @@ import { DebugConsoleContribution } from './console/debug-console-contribution'; import { DebugService } from '../common/debug-service'; import { DebugSchemaUpdater } from './debug-schema-updater'; import { DebugPreferences } from './debug-preferences'; -import { TabBarToolbarContribution, TabBarToolbarRegistry, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { TabBarToolbarContribution, TabBarToolbarRegistry, RenderedToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { DebugWatchWidget } from './view/debug-watch-widget'; import { DebugWatchExpression } from './view/debug-watch-expression'; import { DebugWatchManager } from './debug-watch-manager'; @@ -1079,7 +1079,7 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi registerToolbarItems(toolbar: TabBarToolbarRegistry): void { const onDidChangeToggleBreakpointsEnabled = new Emitter(); - const toggleBreakpointsEnabled: Mutable = { + const toggleBreakpointsEnabled: Mutable = { id: DebugCommands.TOGGLE_BREAKPOINTS_ENABLED.id, command: DebugCommands.TOGGLE_BREAKPOINTS_ENABLED.id, icon: codicon('activate-breakpoints'), diff --git a/packages/git/src/browser/git-contribution.ts b/packages/git/src/browser/git-contribution.ts index de36bd4d7788a..84bd48ee7bb95 100644 --- a/packages/git/src/browser/git-contribution.ts +++ b/packages/git/src/browser/git-contribution.ts @@ -654,7 +654,7 @@ export class GitContribution implements CommandContribution, MenuContribution, T tooltip: GIT_COMMANDS.INIT_REPOSITORY.label }); - const registerItem = (item: Mutable) => { + const registerItem = (item: Mutable) => { const commandId = item.command; const id = '__git.tabbar.toolbar.' + commandId; const command = this.commands.getCommand(commandId); diff --git a/packages/monaco/src/browser/monaco-context-key-service.ts b/packages/monaco/src/browser/monaco-context-key-service.ts index 3de29a0826c4c..38640ba236c00 100644 --- a/packages/monaco/src/browser/monaco-context-key-service.ts +++ b/packages/monaco/src/browser/monaco-context-key-service.ts @@ -109,9 +109,9 @@ export class MonacoContextKeyService implements TheiaContextKeyService { createScoped(target: HTMLElement): ScopedValueStore { const scoped = this.contextKeyService.createScoped(target); if (scoped instanceof AbstractContextKeyService) { - return scoped as AbstractContextKeyService & { createScoped(): ScopedValueStore }; + return scoped as unknown as ScopedValueStore; } - return this; + throw new Error('Could not created scoped value store'); } createOverlay(overlay: Iterable<[string, unknown]>): ContextMatcher { diff --git a/packages/navigator/src/browser/navigator-contribution.ts b/packages/navigator/src/browser/navigator-contribution.ts index 3a41805dc16b6..956436fbafe2a 100644 --- a/packages/navigator/src/browser/navigator-contribution.ts +++ b/packages/navigator/src/browser/navigator-contribution.ts @@ -57,8 +57,8 @@ import { FileNavigatorFilter } from './navigator-filter'; import { WorkspaceNode } from './navigator-tree'; import { NavigatorContextKeyService } from './navigator-context-key-service'; import { + RenderedToolbarItem, TabBarToolbarContribution, - TabBarToolbarItem, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { FileSystemCommands } from '@theia/filesystem/lib/browser/filesystem-frontend-contribution'; @@ -588,7 +588,7 @@ export class FileNavigatorContribution extends AbstractViewContribution) => { + public registerMoreToolbarItem = (item: Mutable & { command: string }) => { const commandId = item.command; const id = 'navigator.tabbar.toolbar.' + commandId; const command = this.commandRegistry.getCommand(commandId); diff --git a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts index 54723145771c5..ae3823e2a7c01 100644 --- a/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts +++ b/packages/plugin-ext/src/main/browser/menus/menus-contribution-handler.ts @@ -24,15 +24,14 @@ import { DeployedPlugin, IconUrl, Menu } from '../../../common'; import { ScmWidget } from '@theia/scm/lib/browser/scm-widget'; import { QuickCommandService } from '@theia/core/lib/browser'; import { - CodeEditorWidgetUtil, codeToTheiaMappings, codeToTheiaContextKeyOverlays, codeToTheiaGroupProviders, ContributionPoint, - PLUGIN_EDITOR_TITLE_MENU, PLUGIN_EDITOR_TITLE_RUN_MENU, PLUGIN_SCM_TITLE_MENU, PLUGIN_TEST_VIEW_TITLE_MENU, PLUGIN_VIEW_TITLE_MENU + CodeEditorWidgetUtil, codeToTheiaMappings, ContributionPoint, + PLUGIN_EDITOR_TITLE_MENU, PLUGIN_EDITOR_TITLE_RUN_MENU, PLUGIN_SCM_TITLE_MENU, PLUGIN_VIEW_TITLE_MENU } from './vscode-theia-menu-mappings'; import { PluginMenuCommandAdapter, ReferenceCountingSet } from './plugin-menu-command-adapter'; import { ContextKeyExpr } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey'; import { ContextKeyService } from '@theia/core/lib/browser/context-key-service'; import { PluginSharedStyle } from '../plugin-shared-style'; import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables'; -import { TestTreeWidget } from '@theia/test/lib/browser/view/test-tree-widget'; @injectable() export class MenusContributionPointHandler { @@ -63,7 +62,6 @@ export class MenusContributionPointHandler { }); this.tabBarToolbar.registerMenuDelegate(PLUGIN_SCM_TITLE_MENU, widget => widget instanceof ScmWidget); this.tabBarToolbar.registerMenuDelegate(PLUGIN_VIEW_TITLE_MENU, widget => !this.codeEditorWidgetUtil.is(widget)); - this.tabBarToolbar.registerMenuDelegate(PLUGIN_TEST_VIEW_TITLE_MENU, widget => widget instanceof TestTreeWidget); this.tabBarToolbar.registerItem({ id: 'plugin-menu-contribution-title-contribution', command: '_never_', onDidChange: this.onDidChangeTitleContributionEmitter.event }); this.contextKeyService.onDidChange(event => { if (event.affects(this.titleContributionContextKeys)) { @@ -76,17 +74,6 @@ export class MenusContributionPointHandler { return codeToTheiaMappings.get(contributionPoint); } - private getMatchingGroup(contributionPoint: ContributionPoint, item: Menu): string | undefined { - return codeToTheiaGroupProviders.get(contributionPoint)?.(item) ?? item.group; - } - - private getContextKeyOverlay(contributionPoint: ContributionPoint): { key: string; values: string[]; } | undefined { - if (codeToTheiaContextKeyOverlays.has(contributionPoint)) { - return codeToTheiaContextKeyOverlays.get(contributionPoint); - } - return undefined; - } - handle(plugin: DeployedPlugin): Disposable { const allMenus = plugin.contributes?.menus; if (!allMenus) { @@ -110,8 +97,7 @@ export class MenusContributionPointHandler { } else { this.checkTitleContribution(contributionPoint, item, toDispose); const targets = this.getMatchingMenu(contributionPoint as ContributionPoint) ?? [contributionPoint]; - const matchingGroup = this.getMatchingGroup(contributionPoint as ContributionPoint, item); - const { group, order } = this.parseGroup(matchingGroup); + const { group, order } = this.parseGroup(item.group); const { submenu, command } = item; if (submenu && command) { console.warn( @@ -121,27 +107,14 @@ export class MenusContributionPointHandler { if (command) { toDispose.push(this.commandAdapter.addCommand(command)); targets.forEach(target => { - const overlay = this.getContextKeyOverlay(contributionPoint as ContributionPoint); - if (overlay) { - overlay.values.forEach(value => { - const node = new ActionMenuNode({ - commandId: command, - when: item.when, - order, - contextKeyOverlays: [{ key: overlay.key, value: value }] - }, this.commands); - const parent = this.menuRegistry.getMenuNode(target, group); - toDispose.push(parent.addNode(node)); - }); - } else { - const node = new ActionMenuNode({ - commandId: command, - when: item.when, - order - }, this.commands); - const parent = this.menuRegistry.getMenuNode(target, group); - toDispose.push(parent.addNode(node)); - } + + const node = new ActionMenuNode({ + commandId: command, + when: item.when, + order + }, this.commands); + const parent = this.menuRegistry.getMenuNode(target, group); + toDispose.push(parent.addNode(node)); }); } else if (submenu) { targets.forEach(target => toDispose.push(this.menuRegistry.linkSubmenu(target, submenu!, { order, when: item.when }, group))); diff --git a/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts b/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts index 0f15c435a10f2..3a519199040f2 100644 --- a/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts +++ b/packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts @@ -32,16 +32,14 @@ import { COMMENT_CONTEXT, COMMENT_THREAD_CONTEXT, COMMENT_TITLE } from '../comme import { VIEW_ITEM_CONTEXT_MENU } from '../view/tree-view-widget'; import { WEBVIEW_CONTEXT_MENU, WebviewWidget } from '../webview/webview'; import { EDITOR_LINENUMBER_CONTEXT_MENU } from '@theia/editor/lib/browser/editor-linenumber-contribution'; -import { TEST_VIEW_CONTEXT_MENU } from '@theia/test/lib/browser/view/test-view-contribution'; +import { PLUGIN_TEST_VIEW_TITLE_MENU, TEST_VIEW_CONTEXT_MENU } from '@theia/test/lib/browser/view/test-view-contribution'; import { TEST_RUNS_CONTEXT_MENU } from '@theia/test/lib/browser/view/test-run-view-contribution'; import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution'; -import { Menu } from '../../../common'; export const PLUGIN_EDITOR_TITLE_MENU = ['plugin_editor/title']; export const PLUGIN_EDITOR_TITLE_RUN_MENU = ['plugin_editor/title/run']; export const PLUGIN_SCM_TITLE_MENU = ['plugin_scm/title']; export const PLUGIN_VIEW_TITLE_MENU = ['plugin_view/title']; -export const PLUGIN_TEST_VIEW_TITLE_MENU = ['plugin_test/title']; export const implementedVSCodeContributionPoints = [ 'comments/comment/context', @@ -107,14 +105,6 @@ export const codeToTheiaMappings = new Map([ ]); -export const codeToTheiaGroupProviders = new Map string>([ - ['testing/profiles/context', () => 'configure'] -]); - -export const codeToTheiaContextKeyOverlays = new Map([ - ['testing/profiles/context', { key: 'testing.profile.context.group', values: ['run', 'debug', 'coverage'] }], -]); - type CodeEditorWidget = EditorWidget | WebviewWidget; @injectable() export class CodeEditorWidgetUtil { diff --git a/packages/test/src/browser/view/test-view-contribution.ts b/packages/test/src/browser/view/test-view-contribution.ts index 7b672083dab52..37f123be2590d 100644 --- a/packages/test/src/browser/view/test-view-contribution.ts +++ b/packages/test/src/browser/view/test-view-contribution.ts @@ -26,6 +26,7 @@ import { NavigationLocationService } from '@theia/editor/lib/browser/navigation/ import { NavigationLocation } from '@theia/editor/lib/browser/navigation/navigation-location'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { FileNavigatorCommands } from '@theia/navigator/lib/browser/file-navigator-commands'; +export const PLUGIN_TEST_VIEW_TITLE_MENU = ['plugin_test', 'title']; export namespace TestViewCommands { /** @@ -293,12 +294,20 @@ export class TestViewContribution extends AbstractViewContribution this.executeCommand(e, item)} onDragOver={this.handleOnDragEnter} onDragLeave={this.handleOnDragLeave} onContextMenu={this.handleContextMenu} @@ -279,7 +279,7 @@ export class ToolbarImpl extends TabBarToolbar { } protected override renderItem( - item: TabBarToolbarItem, + item: RenderedToolbarItem, ): React.ReactNode { const classNames = []; if (item.text) { @@ -290,7 +290,7 @@ export class ToolbarImpl extends TabBarToolbar { } } } - const command = this.commands.getCommand(item.command); + const command = this.commands.getCommand(item.command!); const iconClass = (typeof item.icon === 'function' && item.icon()) || item.icon || command?.iconClass; if (iconClass) { classNames.push(iconClass);