diff --git a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts index 3d5e9c617214a..89a3bd7b4f3d5 100644 --- a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts +++ b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts @@ -4,23 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from '../../../../nls.js'; +import { Action, IAction, IActionRunner } from '../../../common/actions.js'; +import { Codicon } from '../../../common/codicons.js'; +import { Emitter } from '../../../common/event.js'; +import { ResolvedKeybinding } from '../../../common/keybindings.js'; +import { KeyCode } from '../../../common/keyCodes.js'; +import { IDisposable } from '../../../common/lifecycle.js'; +import { ThemeIcon } from '../../../common/themables.js'; import { IContextMenuProvider } from '../../contextmenu.js'; import { $, addDisposableListener, append, EventType, h } from '../../dom.js'; import { StandardKeyboardEvent } from '../../keyboardEvent.js'; import { IActionViewItemProvider } from '../actionbar/actionbar.js'; import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions, IBaseActionViewItemOptions } from '../actionbar/actionViewItems.js'; import { AnchorAlignment } from '../contextview/contextview.js'; -import { DropdownMenu, IActionProvider, IDropdownMenuOptions, ILabelRenderer } from './dropdown.js'; -import { Action, IAction, IActionRunner } from '../../../common/actions.js'; -import { Codicon } from '../../../common/codicons.js'; -import { ThemeIcon } from '../../../common/themables.js'; -import { Emitter } from '../../../common/event.js'; -import { KeyCode } from '../../../common/keyCodes.js'; -import { ResolvedKeybinding } from '../../../common/keybindings.js'; -import { IDisposable } from '../../../common/lifecycle.js'; -import './dropdown.css'; -import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js'; import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js'; +import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js'; +import './dropdown.css'; +import { DropdownMenu, IActionProvider, IDropdownMenuOptions, ILabelRenderer } from './dropdown.js'; export interface IKeybindingProvider { (action: IAction): ResolvedKeybinding | undefined; @@ -73,31 +73,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem { const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => { this.element = append(el, $('a.action-label')); - - let classNames: string[] = []; - - if (typeof this.options.classNames === 'string') { - classNames = this.options.classNames.split(/\s+/g).filter(s => !!s); - } else if (this.options.classNames) { - classNames = this.options.classNames; - } - - // todo@aeschli: remove codicon, should come through `this.options.classNames` - if (!classNames.find(c => c === 'icon')) { - classNames.push('codicon'); - } - - this.element.classList.add(...classNames); - - this.element.setAttribute('role', 'button'); - this.element.setAttribute('aria-haspopup', 'true'); - this.element.setAttribute('aria-expanded', 'false'); - if (this._action.label) { - this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.element, this._action.label)); - } - this.element.ariaLabel = this._action.label || ''; - - return null; + return this.renderLabel(this.element); }; const isActionsArray = Array.isArray(this.menuActionsOrProvider); @@ -138,6 +114,36 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem { this.updateEnabled(); } + protected renderLabel(element: HTMLElement): IDisposable | null { + let classNames: string[] = []; + + if (typeof this.options.classNames === 'string') { + classNames = this.options.classNames.split(/\s+/g).filter(s => !!s); + } else if (this.options.classNames) { + classNames = this.options.classNames; + } + + // todo@aeschli: remove codicon, should come through `this.options.classNames` + if (!classNames.find(c => c === 'icon')) { + classNames.push('codicon'); + } + + element.classList.add(...classNames); + + if (this._action.label) { + this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), element, this._action.label)); + } + + return null; + } + + protected setAriaLabelAttributes(element: HTMLElement): void { + element.setAttribute('role', 'button'); + element.setAttribute('aria-haspopup', 'true'); + element.setAttribute('aria-expanded', 'false'); + element.ariaLabel = this._action.label || ''; + } + protected override getTooltip(): string | undefined { let title: string | null = null; diff --git a/src/vs/platform/actions/browser/dropdownActionViewItemWithKeybinding.ts b/src/vs/platform/actions/browser/dropdownActionViewItemWithKeybinding.ts new file mode 100644 index 0000000000000..2fd4cc5d0e58a --- /dev/null +++ b/src/vs/platform/actions/browser/dropdownActionViewItemWithKeybinding.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IContextMenuProvider } from '../../../base/browser/contextmenu.js'; +import { IActionProvider } from '../../../base/browser/ui/dropdown/dropdown.js'; +import { DropdownMenuActionViewItem, IDropdownMenuActionViewItemOptions } from '../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; +import { IAction } from '../../../base/common/actions.js'; +import * as nls from '../../../nls.js'; +import { IContextKeyService } from '../../contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../keybinding/common/keybinding.js'; + +export class DropdownMenuActionViewItemWithKeybinding extends DropdownMenuActionViewItem { + constructor( + action: IAction, + menuActionsOrProvider: readonly IAction[] | IActionProvider, + contextMenuProvider: IContextMenuProvider, + options: IDropdownMenuActionViewItemOptions = Object.create(null), + @IKeybindingService private readonly keybindingService: IKeybindingService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + ) { + super(action, menuActionsOrProvider, contextMenuProvider, options); + } + + protected override getTooltip() { + const keybinding = this.keybindingService.lookupKeybinding(this.action.id, this.contextKeyService); + const keybindingLabel = keybinding && keybinding.getLabel(); + + const tooltip = this.action.tooltip ?? this.action.label; + return keybindingLabel + ? nls.localize('titleAndKb', "{0} ({1})", tooltip, keybindingLabel) + : tooltip; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 0dcc74fe0cf9c..1c410e9a66eb3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -10,6 +10,7 @@ import { IHistoryNavigationWidget } from '../../../../base/browser/history.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import * as aria from '../../../../base/browser/ui/aria/aria.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; +import { IActionProvider } from '../../../../base/browser/ui/dropdown/dropdown.js'; import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { getBaseLayerHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate2.js'; @@ -17,7 +18,7 @@ import { createInstantHoverDelegate, getDefaultHoverDelegate } from '../../../.. import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { ProgressBar } from '../../../../base/browser/ui/progressbar/progressbar.js'; -import { IAction, Separator, toAction } from '../../../../base/common/actions.js'; +import { IAction } from '../../../../base/common/actions.js'; import { coalesce } from '../../../../base/common/arrays.js'; import { Promises } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; @@ -51,8 +52,9 @@ import { SuggestController } from '../../../../editor/contrib/suggest/browser/su import { localize } from '../../../../nls.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { MenuWorkbenchButtonBar } from '../../../../platform/actions/browser/buttonbar.js'; +import { DropdownMenuActionViewItemWithKeybinding } from '../../../../platform/actions/browser/dropdownActionViewItemWithKeybinding.js'; import { DropdownWithPrimaryActionViewItem, IDropdownWithPrimaryActionViewItemOptions } from '../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js'; -import { getFlatActionBarActions, IMenuEntryActionViewItemOptions, MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { IMenuService, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; @@ -837,10 +839,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge }, getModels: () => this.getModels() }; - return this.instantiationService.createInstance(ModelPickerActionViewItem, action, this._currentLanguageModel, itemDelegate, { hoverDelegate: options.hoverDelegate, keybinding: options.keybinding ?? undefined }); + return this.instantiationService.createInstance(ModelPickerActionViewItem, action, this._currentLanguageModel, itemDelegate); } } else if (action.id === ToggleAgentModeActionId && action instanceof MenuItemAction) { - return this.instantiationService.createInstance(ToggleAgentActionViewItem, action, options as IMenuEntryActionViewItemOptions); + return this.instantiationService.createInstance(ToggleAgentActionViewItem, action); } return undefined; @@ -1637,155 +1639,105 @@ interface ModelPickerDelegate { getModels(): ILanguageModelChatMetadataAndIdentifier[]; } -class ModelPickerActionViewItem extends MenuEntryActionViewItem { +class ModelPickerActionViewItem extends DropdownMenuActionViewItemWithKeybinding { constructor( action: MenuItemAction, private currentLanguageModel: ILanguageModelChatMetadataAndIdentifier, private readonly delegate: ModelPickerDelegate, - options: IMenuEntryActionViewItemOptions, - @IKeybindingService keybindingService: IKeybindingService, - @INotificationService notificationService: INotificationService, - @IContextKeyService contextKeyService: IContextKeyService, - @IThemeService themeService: IThemeService, @IContextMenuService contextMenuService: IContextMenuService, @IAccessibilityService _accessibilityService: IAccessibilityService, - @ICommandService private readonly _commandService: ICommandService, + @IKeybindingService keybindingService: IKeybindingService, + @IContextKeyService contextKeyService: IContextKeyService, ) { - super(action, options, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, _accessibilityService); + const modelActionsProvider: IActionProvider = { + getActions: () => { + const setLanguageModelAction = (entry: ILanguageModelChatMetadataAndIdentifier): IAction => { + return { + id: entry.identifier, + label: entry.metadata.name, + tooltip: '', + class: undefined, + enabled: true, + checked: entry.identifier === this.currentLanguageModel.identifier, + run: () => { + this.currentLanguageModel = entry; + this.renderLabel(this.element!); + this.delegate.setModel(entry); + } + }; + }; + + const models: ILanguageModelChatMetadataAndIdentifier[] = this.delegate.getModels(); + return models.map(entry => setLanguageModelAction(entry)); + } + }; + super(action, modelActionsProvider, contextMenuService, undefined, keybindingService, contextKeyService); this._register(delegate.onDidChangeModel(modelId => { this.currentLanguageModel = modelId; - this.updateLabel(); + this.renderLabel(this.element!); })); } - override async onClick(event: MouseEvent): Promise { - this._openContextMenu(); + protected override renderLabel(element: HTMLElement): IDisposable | null { + this.setAriaLabelAttributes(element); + dom.reset(element, dom.$('span.chat-model-label', undefined, this.currentLanguageModel.metadata.name), ...renderLabelWithIcons(`$(chevron-down)`)); + return null; } override render(container: HTMLElement): void { super.render(container); container.classList.add('chat-modelPicker-item'); - - // TODO@roblourens this should be a DropdownMenuActionViewItem, but we can't customize how it's rendered yet. - this._register(dom.addDisposableListener(container, dom.EventType.KEY_UP, e => { - const event = new StandardKeyboardEvent(e); - if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { - this._openContextMenu(); - } - })); - } - - protected override updateLabel(): void { - if (this.label && this.currentLanguageModel) { - dom.reset(this.label, dom.$('span.chat-model-label', undefined, this.currentLanguageModel.metadata.name), ...renderLabelWithIcons(`$(chevron-down)`)); - } - } - - private _openContextMenu() { - const setLanguageModelAction = (entry: ILanguageModelChatMetadataAndIdentifier): IAction => { - return { - id: entry.identifier, - label: entry.metadata.name, - tooltip: '', - class: undefined, - enabled: true, - checked: entry.identifier === this.currentLanguageModel.identifier, - run: () => { - this.currentLanguageModel = entry; - this.updateLabel(); - this.delegate.setModel(entry); - } - }; - }; - - const models: ILanguageModelChatMetadataAndIdentifier[] = this.delegate.getModels(); - this._contextMenuService.showContextMenu({ - getAnchor: () => this.element!, - getActions: () => { - const actions = models.map(entry => setLanguageModelAction(entry)); - - if (this._contextKeyService.getContextKeyValue(ChatContextKeys.Setup.limited.key) === true) { - actions.push(new Separator()); - actions.push(toAction({ id: 'moreModels', label: localize('chat.moreModels', "Add More Models..."), run: () => this._commandService.executeCommand('workbench.action.chat.upgradePlan') })); - } - - return actions; - }, - }); } } const chatInputEditorContainerSelector = '.interactive-input-editor'; setupSimpleEditorSelectionStyling(chatInputEditorContainerSelector); -class ToggleAgentActionViewItem extends MenuEntryActionViewItem { +class ToggleAgentActionViewItem extends DropdownMenuActionViewItemWithKeybinding { private readonly agentStateActions: IAction[]; constructor( action: MenuItemAction, - options: IMenuEntryActionViewItemOptions, + @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService keybindingService: IKeybindingService, - @INotificationService notificationService: INotificationService, @IContextKeyService contextKeyService: IContextKeyService, - @IThemeService themeService: IThemeService, - @IContextMenuService contextMenuService: IContextMenuService, - @IAccessibilityService _accessibilityService: IAccessibilityService ) { - options.keybindingNotRenderedWithLabel = true; - super(action, options, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, _accessibilityService); - - this.agentStateActions = [ + const agentStateActions = [ { - ...this.action, + ...action, id: 'agentMode', label: localize('chat.agentMode', "Agent"), class: undefined, enabled: true, - run: () => this.action.run({ agentMode: true } satisfies IToggleAgentModeArgs) + run: () => action.run({ agentMode: true } satisfies IToggleAgentModeArgs) }, { - ...this.action, + ...action, id: 'normalMode', label: localize('chat.normalMode', "Edit"), class: undefined, enabled: true, - checked: !this.action.checked, - run: () => this.action.run({ agentMode: false } satisfies IToggleAgentModeArgs) + checked: !action.checked, + run: () => action.run({ agentMode: false } satisfies IToggleAgentModeArgs) }, ]; + + super(action, agentStateActions, contextMenuService, undefined, keybindingService, contextKeyService); + this.agentStateActions = agentStateActions; } - override async onClick(event: MouseEvent): Promise { - this._openContextMenu(); + protected override renderLabel(element: HTMLElement): IDisposable | null { + // Can't call super.renderLabel because it has a hack of forcing the 'codicon' class + this.setAriaLabelAttributes(element); + + const state = this.agentStateActions.find(action => action.checked)?.label ?? ''; + dom.reset(element, dom.$('span.chat-model-label', undefined, state), ...renderLabelWithIcons(`$(chevron-down)`)); + return null; } override render(container: HTMLElement): void { super.render(container); container.classList.add('chat-modelPicker-item'); - - // TODO@roblourens this should be a DropdownMenuActionViewItem, but we can't customize how it's rendered yet. - this._register(dom.addDisposableListener(container, dom.EventType.KEY_UP, e => { - const event = new StandardKeyboardEvent(e); - if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { - this._openContextMenu(); - } - })); - } - - protected override updateLabel(): void { - if (this.label) { - const state = this.agentStateActions.find(action => action.checked)?.label ?? ''; - dom.reset(this.label, dom.$('span.chat-model-label', undefined, state), ...renderLabelWithIcons(`$(chevron-down)`)); - } - } - - private _openContextMenu() { - if (this.action.enabled) { - this._contextMenuService.showContextMenu({ - getAnchor: () => this.element!, - getActions: () => this.agentStateActions - }); - } } } diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 138dd32b1507f..f0148315808bc 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -807,10 +807,13 @@ have to be updated for changes to the rules above, or to support more deeply nes .chat-modelPicker-item { min-width: 0px; - .chat-model-label { + .action-label { min-width: 0px; - overflow: hidden; - text-overflow: ellipsis; + + .chat-model-label { + overflow: hidden; + text-overflow: ellipsis; + } } .codicon { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts index f9b8bb771c500..6e742fa265ebb 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellActionView.ts @@ -55,7 +55,7 @@ export class UnifiedSubmenuActionView extends SubmenuEntryActionViewItem { constructor( action: SubmenuItemAction, options: IMenuEntryActionViewItemOptions | undefined, - readonly renderLabel: boolean, + private readonly _renderLabel: boolean, readonly subActionProvider: IActionProvider, readonly subActionViewItemProvider: IActionViewItemProvider | undefined, @IKeybindingService _keybindingService: IKeybindingService, @@ -108,13 +108,13 @@ export class UnifiedSubmenuActionView extends SubmenuEntryActionViewItem { element.classList.add(...iconClasses); } - if (this.renderLabel) { + if (this._renderLabel) { this._actionLabel.classList.add('notebook-label'); this._actionLabel.innerText = this._action.label; this._hover?.update(primaryAction.tooltip.length ? primaryAction.tooltip : primaryAction.label); } } else { - if (this.renderLabel) { + if (this._renderLabel) { this._actionLabel.classList.add('notebook-label'); this._actionLabel.innerText = this._action.label; this._hover?.update(this._action.tooltip.length ? this._action.tooltip : this._action.label);