diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e545eb1b1b48..219b55851a171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ - [Previous Changelogs](https://github.com/eclipse-theia/theia/tree/master/doc/changelogs/) +## v1.40.0 - + +- Show command shortcuts in toolbar item tooltips. #12660 (https://github.com/eclipse-theia/theia/pull/12660) - Contributed on behalf of STMicroelectronics + +[Breaking Changes:](#breaking_changes_1.40.0) + + ## v1.39.0 - 06/29/2023 - [application-manager] added support for backend bundling [#12412](https://github.com/eclipse-theia/theia/pull/12412) diff --git a/packages/core/src/browser/keybinding.ts b/packages/core/src/browser/keybinding.ts index 05e0c0af72ca4..a550ce746be3d 100644 --- a/packages/core/src/browser/keybinding.ts +++ b/packages/core/src/browser/keybinding.ts @@ -434,17 +434,18 @@ export class KeybindingRegistry { */ getKeybindingsForCommand(commandId: string): ScopedKeybinding[] { const result: ScopedKeybinding[] = []; - const disabledBindings: ScopedKeybinding[] = []; + const disabledBindings = new Set(); for (let scope = KeybindingScope.END - 1; scope >= KeybindingScope.DEFAULT; scope--) { this.keymaps[scope].forEach(binding => { if (binding.command?.startsWith('-')) { - disabledBindings.push(binding); - } - const command = this.commandRegistry.getCommand(binding.command); - if (command - && command.id === commandId - && !disabledBindings.some(disabled => common.Keybinding.equals(disabled, { ...binding, command: '-' + binding.command }, false, true))) { - result.push({ ...binding, scope }); + disabledBindings.add(JSON.stringify({ command: binding.command.substring(1), binding: binding.keybinding, context: binding.context, when: binding.when })); + } else { + const command = this.commandRegistry.getCommand(binding.command); + if (command + && command.id === commandId + && !disabledBindings.has(JSON.stringify({ command: binding.command, binding: binding.keybinding, context: binding.context, when: binding.when }))) { + result.push({ ...binding, scope }); + } } }); } @@ -491,11 +492,15 @@ export class KeybindingRegistry { * Only execute if it has no context (global context) or if we're in that context. */ protected isEnabled(binding: common.Keybinding, event: KeyboardEvent): boolean { + return this.isEnabledInScope(binding, event.target); + } + + isEnabledInScope(binding: common.Keybinding, target: HTMLElement | undefined): boolean { const context = binding.context && this.contexts[binding.context]; if (context && !context.isEnabled(binding)) { return false; } - if (binding.when && !this.whenContextService.match(binding.when, event.target)) { + if (binding.when && !this.whenContextService.match(binding.when, target)) { return false; } return true; 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 e5d2c5e3b9f1b..dac2d86a66425 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 @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { inject, injectable } from 'inversify'; +import { inject, injectable, postConstruct } from 'inversify'; import * as React from 'react'; import { ContextKeyService } from '../../context-key-service'; import { CommandRegistry, Disposable, DisposableCollection, MenuCommandExecutor, MenuModelRegistry, MenuPath, nls } from '../../../common'; @@ -23,6 +23,7 @@ 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 } from './tab-bar-toolbar-types'; +import { KeybindingRegistry } from '../..//keybinding'; /** * Factory for instantiating tab-bar toolbars. @@ -45,6 +46,8 @@ export class TabBarToolbar extends ReactWidget { protected contextKeyListener: Disposable | undefined; protected toDisposeOnUpdateItems: DisposableCollection = new DisposableCollection(); + protected keybindingContextKeys = new Set(); + @inject(CommandRegistry) protected readonly commands: CommandRegistry; @inject(LabelParser) protected readonly labelParser: LabelParser; @inject(MenuModelRegistry) protected readonly menus: MenuModelRegistry; @@ -52,6 +55,7 @@ export class TabBarToolbar extends ReactWidget { @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer; @inject(TabBarToolbarRegistry) protected readonly toolbarRegistry: TabBarToolbarRegistry; @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService; + @inject(KeybindingRegistry) protected readonly keybindings: KeybindingRegistry; constructor() { super(); @@ -59,6 +63,17 @@ export class TabBarToolbar extends ReactWidget { this.hide(); } + @postConstruct() + protected init(): void { + this.toDispose.push(this.keybindings.onKeybindingsChanged(() => this.update())); + + this.toDispose.push(this.contextKeyService.onDidChange(e => { + if (e.affects(this.keybindingContextKeys)) { + this.update(); + } + })); + } + updateItems(items: Array, current: Widget | undefined): void { this.toDisposeOnUpdateItems.dispose(); this.toDisposeOnUpdateItems = new DisposableCollection(); @@ -131,12 +146,33 @@ export class TabBarToolbar extends ReactWidget { } protected render(): React.ReactNode { + this.keybindingContextKeys.clear(); return {this.renderMore()} {[...this.inline.values()].map(item => TabBarToolbarItem.is(item) ? this.renderItem(item) : item.render(this.current))} ; } + protected resolveKeybindingForCommand(command: string | undefined): string { + let result = ''; + if (command) { + const bindings = this.keybindings.getKeybindingsForCommand(command); + let found = false; + if (bindings && bindings.length > 0) { + bindings.forEach(binding => { + if (binding.when) { + this.contextKeyService.parseKeys(binding.when)?.forEach(key => this.keybindingContextKeys.add(key)); + } + if (!found && this.keybindings.isEnabledInScope(binding, this.current?.node)) { + found = true; + result = ` (${this.keybindings.acceleratorFor(binding, '+')})`; + } + }); + } + } + return result; + } + protected renderItem(item: AnyToolbarItem): React.ReactNode { let innerText = ''; const classNames = []; @@ -156,7 +192,7 @@ export class TabBarToolbar extends ReactWidget { iconClass += ` ${ACTION_ITEM}`; classNames.push(iconClass); } - const tooltip = item.tooltip || (command && command.label); + const tooltip = `${item.tooltip || (command && command.label) || ''}${this.resolveKeybindingForCommand(command?.id)}`; const toolbarItemClassNames = this.getToolbarItemClassNames(item); return
this.update())); + this.toDispose.push(this.keybindings.onKeybindingsChanged(() => this.update())); this.treeDragType = `application/vnd.code.tree.${this.id.toLowerCase()}`; } @@ -731,6 +736,23 @@ export class TreeViewWidget extends TreeViewWelcomeWidget { return { viewId: this.id, itemId: treeNode.id }; } + protected resolveKeybindingForCommand(command: string | undefined): string { + let result = ''; + if (command) { + const bindings = this.keybindings.getKeybindingsForCommand(command); + let found = false; + if (bindings && bindings.length > 0) { + bindings.forEach(binding => { + if (!found && this.keybindings.isEnabledInScope(binding, this.node)) { + found = true; + result = ` (${this.keybindings.acceleratorFor(binding, '+')})`; + } + }); + } + } + return result; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any protected renderInlineCommand(actionMenuNode: ActionMenuNode, index: number, tabbable: boolean, args: any[]): React.ReactNode { if (!actionMenuNode.icon || !this.commands.isVisible(actionMenuNode.command, ...args) || !actionMenuNode.when || !this.contextKeys.match(actionMenuNode.when)) { @@ -738,7 +760,9 @@ export class TreeViewWidget extends TreeViewWelcomeWidget { } const className = [TREE_NODE_SEGMENT_CLASS, TREE_NODE_TAIL_CLASS, actionMenuNode.icon, ACTION_ITEM, 'theia-tree-view-inline-action'].join(' '); const tabIndex = tabbable ? 0 : undefined; - return
{ + const titleString = actionMenuNode.label + this.resolveKeybindingForCommand(actionMenuNode.command); + + return
{ e.stopPropagation(); this.commands.executeCommand(actionMenuNode.command, ...args); }} />; diff --git a/packages/toolbar/src/browser/toolbar.tsx b/packages/toolbar/src/browser/toolbar.tsx index d498a1acef5d7..542210a5d6433 100644 --- a/packages/toolbar/src/browser/toolbar.tsx +++ b/packages/toolbar/src/browser/toolbar.tsx @@ -51,7 +51,8 @@ export class ToolbarImpl extends TabBarToolbar { protected isBusyDeferred = new Deferred(); @postConstruct() - protected init(): void { + protected override init(): void { + super.init(); this.doInit(); } @@ -312,20 +313,6 @@ export class ToolbarImpl extends TabBarToolbar { ); } - protected resolveKeybindingForCommand(commandID: string | undefined): string { - if (!commandID) { - return ''; - } - const keybindings = this.keybindingRegistry.getKeybindingsForCommand(commandID); - if (keybindings.length > 0) { - const binding = keybindings[0]; - const bindingKeySequence = this.keybindingRegistry.resolveKeybinding(binding); - const keyCode = bindingKeySequence[0]; - return ` (${this.keybindingRegistry.acceleratorForKeyCode(keyCode, '+')})`; - } - return ''; - } - protected handleOnDragStart = (e: React.DragEvent): void => this.doHandleOnDragStart(e); protected doHandleOnDragStart(e: React.DragEvent): void { const draggedElement = e.currentTarget;