Skip to content

Commit

Permalink
Show command shortcuts in toolbar item tooltips. Fixes #12656
Browse files Browse the repository at this point in the history
Contributed on behalf of STMicroelectronics

Signed-off-by: Thomas Mäder <t.s.maeder@gmail.com>
  • Loading branch information
tsmaeder committed Jul 6, 2023
1 parent cb1ccfc commit a5b9c23
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 28 deletions.
23 changes: 14 additions & 9 deletions packages/core/src/browser/keybinding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,17 +434,18 @@ export class KeybindingRegistry {
*/
getKeybindingsForCommand(commandId: string): ScopedKeybinding[] {
const result: ScopedKeybinding[] = [];
const disabledBindings: ScopedKeybinding[] = [];
const disabledBindings = new Set<string>();
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 });
}
}
});
}
Expand Down Expand Up @@ -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, <HTMLElement>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, <HTMLElement>event.target)) {
if (binding.when && !this.whenContextService.match(binding.when, target)) {
return false;
}
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
Expand All @@ -45,20 +46,34 @@ export class TabBarToolbar extends ReactWidget {
protected contextKeyListener: Disposable | undefined;
protected toDisposeOnUpdateItems: DisposableCollection = new DisposableCollection();

protected keybindingContextKeys = new Set<string>();

@inject(CommandRegistry) protected readonly commands: CommandRegistry;
@inject(LabelParser) protected readonly labelParser: LabelParser;
@inject(MenuModelRegistry) protected readonly menus: MenuModelRegistry;
@inject(MenuCommandExecutor) protected readonly menuCommandExecutor: MenuCommandExecutor;
@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();
this.addClass(TabBarToolbar.Styles.TAB_BAR_TOOLBAR);
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<TabBarToolbarItem | ReactTabBarToolbarItem>, current: Widget | undefined): void {
this.toDisposeOnUpdateItems.dispose();
this.toDisposeOnUpdateItems = new DisposableCollection();
Expand Down Expand Up @@ -131,12 +146,33 @@ export class TabBarToolbar extends ReactWidget {
}

protected render(): React.ReactNode {
this.keybindingContextKeys.clear();
return <React.Fragment>
{this.renderMore()}
{[...this.inline.values()].map(item => TabBarToolbarItem.is(item) ? this.renderItem(item) : item.render(this.current))}
</React.Fragment>;
}

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 = [];
Expand All @@ -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 <div key={item.id}
Expand Down
28 changes: 26 additions & 2 deletions packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ import {
TooltipAttributes,
TreeSelection,
HoverService,
ApplicationShell
ApplicationShell,
KeybindingRegistry
} from '@theia/core/lib/browser';
import { MenuPath, MenuModelRegistry, ActionMenuNode } from '@theia/core/lib/common/menu';
import * as React from '@theia/core/shared/react';
Expand Down Expand Up @@ -404,6 +405,9 @@ export class TreeViewWidget extends TreeViewWelcomeWidget {
@inject(MenuModelRegistry)
protected readonly menus: MenuModelRegistry;

@inject(KeybindingRegistry)
protected readonly keybindings: KeybindingRegistry;

@inject(ContextKeyService)
protected readonly contextKeys: ContextKeyService;

Expand Down Expand Up @@ -441,6 +445,7 @@ export class TreeViewWidget extends TreeViewWelcomeWidget {
this.toDispose.push(this.model.onDidChangeWelcomeState(this.update, this));
this.toDispose.push(this.onDidChangeVisibilityEmitter);
this.toDispose.push(this.contextKeyService.onDidChange(() => this.update()));
this.toDispose.push(this.keybindings.onKeybindingsChanged(() => this.update()));
this.treeDragType = `application/vnd.code.tree.${this.id.toLowerCase()}`;
}

Expand Down Expand Up @@ -731,14 +736,33 @@ 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)) {
return false;
}
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 <div key={index} className={className} title={actionMenuNode.label} tabIndex={tabIndex} onClick={e => {
const titleString = actionMenuNode.label + this.resolveKeybindingForCommand(actionMenuNode.command);

return <div key={index} className={className} title={titleString} tabIndex={tabIndex} onClick={e => {
e.stopPropagation();
this.commands.executeCommand(actionMenuNode.command, ...args);
}} />;
Expand Down
17 changes: 2 additions & 15 deletions packages/toolbar/src/browser/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ export class ToolbarImpl extends TabBarToolbar {
protected isBusyDeferred = new Deferred<void>();

@postConstruct()
protected init(): void {
protected override init(): void {
super.init();
this.doInit();
}

Expand Down Expand Up @@ -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<HTMLDivElement>): void => this.doHandleOnDragStart(e);
protected doHandleOnDragStart(e: React.DragEvent<HTMLDivElement>): void {
const draggedElement = e.currentTarget;
Expand Down

0 comments on commit a5b9c23

Please sign in to comment.