diff --git a/src/vs/base/browser/ui/actionbar/actionViewItems.ts b/src/vs/base/browser/ui/actionbar/actionViewItems.ts index 42bbf152ec5f9..61340ea1ba253 100644 --- a/src/vs/base/browser/ui/actionbar/actionViewItems.ts +++ b/src/vs/base/browser/ui/actionbar/actionViewItems.ts @@ -9,6 +9,8 @@ import { $, addDisposableListener, append, EventHelper, EventLike, EventType } f import { EventType as TouchEventType, Gesture } from 'vs/base/browser/touch'; import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; +import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; import { ISelectBoxOptions, ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { Action, ActionRunner, IAction, IActionChangeEvent, IActionRunner, Separator } from 'vs/base/common/actions'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -28,7 +30,7 @@ export class BaseActionViewItem extends Disposable implements IActionViewItem { element: HTMLElement | undefined; _context: unknown; - _action: IAction; + readonly _action: IAction; get action() { return this._action; @@ -234,6 +236,7 @@ export interface IActionViewItemOptions extends IBaseActionViewItemOptions { icon?: boolean; label?: boolean; keybinding?: string | null; + hoverDelegate?: IHoverDelegate; } export class ActionViewItem extends BaseActionViewItem { @@ -242,6 +245,7 @@ export class ActionViewItem extends BaseActionViewItem { protected override options: IActionViewItemOptions; private cssClass?: string; + private customHover?: ICustomHover; constructor(context: unknown, action: IAction, options: IActionViewItemOptions = {}) { super(context, action, options); @@ -326,10 +330,23 @@ export class ActionViewItem extends BaseActionViewItem { title = nls.localize({ key: 'titleLabel', comment: ['action title', 'action keybinding'] }, "{0} ({1})", title, this.options.keybinding); } } + this._applyUpdateTooltip(title); + } + protected _applyUpdateTooltip(title: string | undefined | null): void { if (title && this.label) { - this.label.title = title; this.label.setAttribute('aria-label', title); + if (!this.options.hoverDelegate) { + this.label.title = title; + } else { + this.label.title = ''; + if (!this.customHover) { + this.customHover = setupCustomHover(this.options.hoverDelegate, this.label, title); + this._store.add(this.customHover); + } else { + this.customHover.update(title); + } + } } } diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index 2a5548664c0b3..7280c8a47371d 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -6,6 +6,7 @@ import * as DOM from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { ActionRunner, IAction, IActionRunner, IRunEvent, Separator } from 'vs/base/common/actions'; import { Emitter } from 'vs/base/common/event'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; @@ -49,6 +50,7 @@ export interface IActionBarOptions { readonly allowContextMenu?: boolean; readonly preventLoopNavigation?: boolean; readonly focusOnlyEnabledItems?: boolean; + readonly hoverDelegate?: IHoverDelegate; } export interface IActionOptions extends IActionViewItemOptions { @@ -327,7 +329,7 @@ export class ActionBar extends Disposable implements IActionRunner { } if (!item) { - item = new ActionViewItem(this.context, action, options); + item = new ActionViewItem(this.context, action, { hoverDelegate: this.options.hoverDelegate, ...options }); } // Prevent native context menu on actions diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 6a05b051a40fc..2624e1cf36628 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -25,6 +25,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { isDark } from 'vs/platform/theme/common/theme'; +import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[] }, primaryGroup?: string): IDisposable { const groups = menu.getActions(options); @@ -125,6 +126,7 @@ function fillInActions( export interface IMenuEntryActionViewItemOptions { draggable?: boolean; keybinding?: string; + hoverDelegate?: IHoverDelegate; } export class MenuEntryActionViewItem extends ActionViewItem { @@ -134,14 +136,14 @@ export class MenuEntryActionViewItem extends ActionViewItem { private readonly _altKey: ModifierKeyEmitter; constructor( - _action: MenuItemAction, + action: MenuItemAction, options: IMenuEntryActionViewItemOptions | undefined, @IKeybindingService protected readonly _keybindingService: IKeybindingService, @INotificationService protected _notificationService: INotificationService, @IContextKeyService protected _contextKeyService: IContextKeyService, @IThemeService protected _themeService: IThemeService ) { - super(undefined, _action, { icon: !!(_action.class || _action.item.icon), label: !_action.class && !_action.item.icon, draggable: options?.draggable, keybinding: options?.keybinding }); + super(undefined, action, { icon: !!(action.class || action.item.icon), label: !action.class && !action.item.icon, draggable: options?.draggable, keybinding: options?.keybinding, hoverDelegate: options?.hoverDelegate }); this._altKey = ModifierKeyEmitter.getInstance(); } @@ -209,26 +211,24 @@ export class MenuEntryActionViewItem extends ActionViewItem { } override updateTooltip(): void { - if (this.label) { - const keybinding = this._keybindingService.lookupKeybinding(this._commandAction.id, this._contextKeyService); - const keybindingLabel = keybinding && keybinding.getLabel(); - - const tooltip = this._commandAction.tooltip || this._commandAction.label; - let title = keybindingLabel - ? localize('titleAndKb', "{0} ({1})", tooltip, keybindingLabel) - : tooltip; - if (!this._wantsAltCommand && this._menuItemAction.alt?.enabled) { - const altTooltip = this._menuItemAction.alt.tooltip || this._menuItemAction.alt.label; - const altKeybinding = this._keybindingService.lookupKeybinding(this._menuItemAction.alt.id, this._contextKeyService); - const altKeybindingLabel = altKeybinding && altKeybinding.getLabel(); - const altTitleSection = altKeybindingLabel - ? localize('titleAndKb', "{0} ({1})", altTooltip, altKeybindingLabel) - : altTooltip; - title += `\n[${UILabelProvider.modifierLabels[OS].altKey}] ${altTitleSection}`; - } - this.label.title = title; - this.label.setAttribute('aria-label', title); + const keybinding = this._keybindingService.lookupKeybinding(this._commandAction.id, this._contextKeyService); + const keybindingLabel = keybinding && keybinding.getLabel(); + + const tooltip = this._commandAction.tooltip || this._commandAction.label; + let title = keybindingLabel + ? localize('titleAndKb', "{0} ({1})", tooltip, keybindingLabel) + : tooltip; + if (!this._wantsAltCommand && this._menuItemAction.alt?.enabled) { + const altTooltip = this._menuItemAction.alt.tooltip || this._menuItemAction.alt.label; + const altKeybinding = this._keybindingService.lookupKeybinding(this._menuItemAction.alt.id, this._contextKeyService); + const altKeybindingLabel = altKeybinding && altKeybinding.getLabel(); + const altTitleSection = altKeybindingLabel + ? localize('titleAndKb', "{0} ({1})", altTooltip, altKeybindingLabel) + : altTooltip; + + title = localize('titleAndKbAndAlt', "{0}\n[{1}] {2}", title, UILabelProvider.modifierLabels[OS].altKey, altTitleSection); } + this._applyUpdateTooltip(title); } override updateClass(): void { @@ -481,9 +481,9 @@ export class DropdownWithDefaultActionViewItem extends BaseActionViewItem { /** * Creates action view items for menu actions or submenu actions. */ -export function createActionViewItem(instaService: IInstantiationService, action: IAction, options?: IDropdownMenuActionViewItemOptions): undefined | MenuEntryActionViewItem | SubmenuEntryActionViewItem | BaseActionViewItem { +export function createActionViewItem(instaService: IInstantiationService, action: IAction, options?: IDropdownMenuActionViewItemOptions | IMenuEntryActionViewItemOptions): undefined | MenuEntryActionViewItem | SubmenuEntryActionViewItem | BaseActionViewItem { if (action instanceof MenuItemAction) { - return instaService.createInstance(MenuEntryActionViewItem, action, undefined); + return instaService.createInstance(MenuEntryActionViewItem, action, options); } else if (action instanceof SubmenuItemAction) { if (action.item.rememberDefaultAction) { return instaService.createInstance(DropdownWithDefaultActionViewItem, action, options); diff --git a/src/vs/workbench/browser/parts/titlebar/titleMenuControl.ts b/src/vs/workbench/browser/parts/titlebar/titleMenuControl.ts index 81bd1e5ee1663..e72d497d73a39 100644 --- a/src/vs/workbench/browser/parts/titlebar/titleMenuControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/titleMenuControl.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { Action, IAction } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; @@ -11,13 +12,16 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { createActionViewItem, createAndFillInContextMenuActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import * as colors from 'vs/platform/theme/common/colorRegistry'; import { WindowTitle } from 'vs/workbench/browser/parts/titlebar/windowTitle'; import { MENUBAR_SELECTION_BACKGROUND, MENUBAR_SELECTION_FOREGROUND, TITLE_BAR_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; export class TitleMenuControl { @@ -32,14 +36,37 @@ export class TitleMenuControl { @IInstantiationService instantiationService: IInstantiationService, @IMenuService menuService: IMenuService, @IQuickInputService quickInputService: IQuickInputService, + @IHoverService hoverService: IHoverService, + @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService, ) { this.element.classList.add('title-menu'); + + const hoverDelegate = new class implements IHoverDelegate { + + private _lastHoverHideTime: number = 0; + + readonly showHover = hoverService.showHover.bind(hoverService); + readonly placement = 'element'; + + get delay(): number { + return Date.now() - this._lastHoverHideTime < 200 + ? 0 // show instantly when a hover was recently shown + : configurationService.getValue('workbench.hover.delay'); + } + + onDidHideHover() { + this._lastHoverHideTime = Date.now(); + } + }; + const titleToolbar = new ToolBar(this.element, contextMenuService, { actionViewItemProvider: (action) => { if (action instanceof MenuItemAction && action.id === 'workbench.action.quickOpen') { class InputLikeViewItem extends MenuEntryActionViewItem { + override render(container: HTMLElement): void { super.render(container); container.classList.add('quickopen'); @@ -49,11 +76,17 @@ export class TitleMenuControl { } private _updateFromWindowTitle() { - if (this.label) { - this.label.classList.add('search'); - this.label.innerText = localize('search', "Search {0}", windowTitle.workspaceName); - this.label.title = windowTitle.value; + if (!this.label) { + return; } + this.label.classList.add('search'); + this.label.innerText = localize('search', "Search {0}", windowTitle.workspaceName); + + const kb = keybindingService.lookupKeybinding(action.id)?.getLabel(); + const title = kb + ? localize('title', "Search {0} ({1}) \u2014 {2}", windowTitle.workspaceName, kb, windowTitle.value) + : localize('title2', "Search {0} \u2014 {1}", windowTitle.workspaceName, windowTitle.value); + this._applyUpdateTooltip(title); } private _renderAllQuickPickItem(parent: HTMLElement): void { @@ -63,16 +96,16 @@ export class TitleMenuControl { const action = new Action('all', localize('all', "Show Quick Pick Options..."), Codicon.chevronDown.classNames, true, () => { quickInputService.quickAccess.show('?'); }); - const dropdown = new ActionViewItem(undefined, action, { icon: true, label: false }); + const dropdown = new ActionViewItem(undefined, action, { icon: true, label: false, hoverDelegate }); dropdown.render(container); this._store.add(dropdown); this._store.add(action); } } - return instantiationService.createInstance(InputLikeViewItem, action, undefined); + return instantiationService.createInstance(InputLikeViewItem, action, { hoverDelegate }); } - return createActionViewItem(instantiationService, action); + return createActionViewItem(instantiationService, action, { hoverDelegate }); } }); const titleMenu = this._disposables.add(menuService.createMenu(MenuId.TitleMenu, contextKeyService));