support custom hover on status bar entries

This commit is contained in:
Martin Aeschlimann 2021-05-31 09:55:16 +02:00
parent f55a5243a4
commit 8d41153ffc
No known key found for this signature in database
GPG key ID: 2609A01E695523E3
7 changed files with 206 additions and 256 deletions

View file

@ -1,131 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isFunction, isString } from 'vs/base/common/types';
import * as dom from 'vs/base/browser/dom';
import { IIconLabelMarkdownString } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { domEvent } from 'vs/base/browser/event';
import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
import { localize } from 'vs/nls';
import { IMarkdownString } from 'vs/base/common/htmlContent';
export class CustomHoverProvider extends Disposable {
private readonly customHovers: Map<HTMLElement, IDisposable> = new Map();
constructor(private hoverDelegate: IHoverDelegate) {
super();
}
public setupHover(htmlElement: HTMLElement, tooltip: string | IIconLabelMarkdownString | undefined): void {
const previousCustomHover = this.customHovers.get(htmlElement);
if (previousCustomHover) {
previousCustomHover.dispose();
this.customHovers.delete(htmlElement);
}
if (tooltip) {
return this.setupCustomHover(this.hoverDelegate, htmlElement, tooltip);
}
}
private getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString): (token: CancellationToken) => Promise<string | IMarkdownString | undefined> {
if (isString(markdownTooltip)) {
return async () => markdownTooltip;
} else if (isFunction(markdownTooltip.markdown)) {
return markdownTooltip.markdown;
} else {
const markdown = markdownTooltip.markdown;
return async () => markdown;
}
}
private setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString): void {
let tooltip = this.getTooltipForCustom(markdownTooltip);
let hoverOptions: IHoverDelegateOptions | undefined;
let mouseX: number | undefined;
let isHovering = false;
let tokenSource: CancellationTokenSource;
let hoverDisposable: IDisposable | undefined;
function mouseOver(this: HTMLElement, e: MouseEvent): void {
if (isHovering) {
return;
}
tokenSource = new CancellationTokenSource();
function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): void {
const isMouseDown = e.type === dom.EventType.MOUSE_DOWN;
if (isMouseDown) {
hoverDisposable?.dispose();
hoverDisposable = undefined;
}
if (isMouseDown || (<any>e).fromElement === htmlElement) {
isHovering = false;
hoverOptions = undefined;
tokenSource.dispose(true);
mouseLeaveDisposable.dispose();
mouseDownDisposable.dispose();
}
}
const mouseLeaveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_LEAVE, true)(mouseLeaveOrDown.bind(htmlElement));
const mouseDownDisposable = domEvent(htmlElement, dom.EventType.MOUSE_DOWN, true)(mouseLeaveOrDown.bind(htmlElement));
isHovering = true;
function mouseMove(this: HTMLElement, e: MouseEvent): void {
mouseX = e.x;
}
const mouseMoveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_MOVE, true)(mouseMove.bind(htmlElement));
setTimeout(async () => {
if (isHovering && tooltip) {
// Re-use the already computed hover options if they exist.
if (!hoverOptions) {
const target: IHoverDelegateTarget = {
targetElements: [this],
dispose: () => { }
};
hoverOptions = {
text: localize('iconLabel.loading', "Loading..."),
target,
hoverPosition: HoverPosition.BELOW
};
hoverDisposable = adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined);
if (resolvedTooltip) {
hoverOptions = {
text: resolvedTooltip,
target,
hoverPosition: HoverPosition.BELOW
};
// awaiting the tooltip could take a while. Make sure we're still hovering.
hoverDisposable = adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
} else if (hoverDisposable) {
hoverDisposable.dispose();
hoverDisposable = undefined;
}
}
}
mouseMoveDisposable.dispose();
}, hoverDelegate.delay);
}
const mouseOverDisposable = this._register(domEvent(htmlElement, dom.EventType.MOUSE_OVER, true)(mouseOver.bind(htmlElement)));
this.customHovers.set(htmlElement, mouseOverDisposable);
}
}
function adjustXAndShowCustomHover(hoverOptions: IHoverDelegateOptions | undefined, mouseX: number | undefined, hoverDelegate: IHoverDelegate, isHovering: boolean): IDisposable | undefined {
if (hoverOptions && isHovering) {
if (mouseX !== undefined) {
(<IHoverDelegateTarget>hoverOptions.target).x = mouseX + 10;
}
return hoverDelegate.showHover(hoverOptions);
}
return undefined;
}

View file

@ -10,13 +10,10 @@ import { IMatch } from 'vs/base/common/filters';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Range } from 'vs/base/common/range'; import { Range } from 'vs/base/common/range';
import { equals } from 'vs/base/common/objects'; import { equals } from 'vs/base/common/objects';
import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IMarkdownString } from 'vs/base/common/htmlContent';
import { isFunction, isString } from 'vs/base/common/types'; import { CancellationToken } from 'vs/base/common/cancellation';
import { domEvent } from 'vs/base/browser/event'; import { setupCustomHover, setupNativeHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover';
import { localize } from 'vs/nls';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
export interface IIconLabelCreationOptions { export interface IIconLabelCreationOptions {
supportHighlights?: boolean; supportHighlights?: boolean;
@ -91,17 +88,17 @@ class FastLabelNode {
export class IconLabel extends Disposable { export class IconLabel extends Disposable {
private domNode: FastLabelNode; private readonly domNode: FastLabelNode;
private nameNode: Label | LabelWithHighlights; private readonly nameNode: Label | LabelWithHighlights;
private descriptionContainer: FastLabelNode; private readonly descriptionContainer: FastLabelNode;
private descriptionNode: FastLabelNode | HighlightedLabel | undefined; private descriptionNode: FastLabelNode | HighlightedLabel | undefined;
private descriptionNodeFactory: () => FastLabelNode | HighlightedLabel; private readonly descriptionNodeFactory: () => FastLabelNode | HighlightedLabel;
private labelContainer: HTMLElement; private readonly labelContainer: HTMLElement;
private hoverDelegate: IHoverDelegate | undefined = undefined; private readonly hoverDelegate: IHoverDelegate | undefined;
private readonly customHovers: Map<HTMLElement, IDisposable> = new Map(); private readonly customHovers: Map<HTMLElement, IDisposable> = new Map();
constructor(container: HTMLElement, options?: IIconLabelCreationOptions) { constructor(container: HTMLElement, options?: IIconLabelCreationOptions) {
@ -126,9 +123,7 @@ export class IconLabel extends Disposable {
this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.descriptionContainer.element, dom.$('span.label-description')))); this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.descriptionContainer.element, dom.$('span.label-description'))));
} }
if (options?.hoverDelegate) { this.hoverDelegate = options?.hoverDelegate;
this.hoverDelegate = options.hoverDelegate;
}
} }
get element(): HTMLElement { get element(): HTMLElement {
@ -185,116 +180,21 @@ export class IconLabel extends Disposable {
} }
if (!this.hoverDelegate) { if (!this.hoverDelegate) {
return this.setupNativeHover(htmlElement, tooltip); setupNativeHover(htmlElement, tooltip);
} else { } else {
return this.setupCustomHover(this.hoverDelegate, htmlElement, tooltip); const hoverDisposable = setupCustomHover(this.hoverDelegate, htmlElement, tooltip);
if (hoverDisposable) {
this.customHovers.set(htmlElement, hoverDisposable);
}
} }
} }
private static adjustXAndShowCustomHover(hoverOptions: IHoverDelegateOptions | undefined, mouseX: number | undefined, hoverDelegate: IHoverDelegate, isHovering: boolean): IDisposable | undefined { public override dispose() {
if (hoverOptions && isHovering) { super.dispose();
if (mouseX !== undefined) { for (const disposable of this.customHovers.values()) {
(<IHoverDelegateTarget>hoverOptions.target).x = mouseX + 10; disposable.dispose();
}
return hoverDelegate.showHover(hoverOptions);
} }
return undefined; this.customHovers.clear();
}
private getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString): (token: CancellationToken) => Promise<string | IMarkdownString | undefined> {
if (isString(markdownTooltip)) {
return async () => markdownTooltip;
} else if (isFunction(markdownTooltip.markdown)) {
return markdownTooltip.markdown;
} else {
const markdown = markdownTooltip.markdown;
return async () => markdown;
}
}
private setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString): void {
htmlElement.setAttribute('title', '');
htmlElement.removeAttribute('title');
let tooltip = this.getTooltipForCustom(markdownTooltip);
let hoverOptions: IHoverDelegateOptions | undefined;
let mouseX: number | undefined;
let isHovering = false;
let tokenSource: CancellationTokenSource;
let hoverDisposable: IDisposable | undefined;
function mouseOver(this: HTMLElement, e: MouseEvent): void {
if (isHovering) {
return;
}
tokenSource = new CancellationTokenSource();
function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): void {
const isMouseDown = e.type === dom.EventType.MOUSE_DOWN;
if (isMouseDown) {
hoverDisposable?.dispose();
hoverDisposable = undefined;
}
if (isMouseDown || (<any>e).fromElement === htmlElement) {
isHovering = false;
hoverOptions = undefined;
tokenSource.dispose(true);
mouseLeaveDisposable.dispose();
mouseDownDisposable.dispose();
}
}
const mouseLeaveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_LEAVE, true)(mouseLeaveOrDown.bind(htmlElement));
const mouseDownDisposable = domEvent(htmlElement, dom.EventType.MOUSE_DOWN, true)(mouseLeaveOrDown.bind(htmlElement));
isHovering = true;
function mouseMove(this: HTMLElement, e: MouseEvent): void {
mouseX = e.x;
}
const mouseMoveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_MOVE, true)(mouseMove.bind(htmlElement));
setTimeout(async () => {
if (isHovering && tooltip) {
// Re-use the already computed hover options if they exist.
if (!hoverOptions) {
const target: IHoverDelegateTarget = {
targetElements: [this],
dispose: () => { }
};
hoverOptions = {
text: localize('iconLabel.loading', "Loading..."),
target,
hoverPosition: HoverPosition.BELOW
};
hoverDisposable = IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined);
if (resolvedTooltip) {
hoverOptions = {
text: resolvedTooltip,
target,
hoverPosition: HoverPosition.BELOW
};
// awaiting the tooltip could take a while. Make sure we're still hovering.
hoverDisposable = IconLabel.adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
} else if (hoverDisposable) {
hoverDisposable.dispose();
hoverDisposable = undefined;
}
}
}
mouseMoveDisposable.dispose();
}, hoverDelegate.delay);
}
const mouseOverDisposable = this._register(domEvent(htmlElement, dom.EventType.MOUSE_OVER, true)(mouseOver.bind(htmlElement)));
this.customHovers.set(htmlElement, mouseOverDisposable);
}
private setupNativeHover(htmlElement: HTMLElement, tooltip: string | IIconLabelMarkdownString | undefined): void {
let stringTooltip: string = '';
if (isString(tooltip)) {
stringTooltip = tooltip;
} else if (tooltip?.markdownNotSupportedFallback) {
stringTooltip = tooltip.markdownNotSupportedFallback;
}
htmlElement.title = stringTooltip;
} }
} }

View file

@ -0,0 +1,129 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isFunction, isString } from 'vs/base/common/types';
import * as dom from 'vs/base/browser/dom';
import { IIconLabelMarkdownString } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { IDisposable } from 'vs/base/common/lifecycle';
import { DomEmitter } from 'vs/base/browser/event';
import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
import { localize } from 'vs/nls';
import { IMarkdownString } from 'vs/base/common/htmlContent';
export function setupNativeHover(htmlElement: HTMLElement, tooltip: string | IIconLabelMarkdownString | undefined): void {
if (isString(tooltip)) {
htmlElement.title = tooltip;
} else if (tooltip?.markdownNotSupportedFallback) {
htmlElement.title = tooltip.markdownNotSupportedFallback;
} else {
htmlElement.removeAttribute('title');
}
}
export function setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString | undefined): IDisposable | undefined {
if (!markdownTooltip) {
return undefined;
}
const tooltip = getTooltipForCustom(markdownTooltip);
let hoverOptions: IHoverDelegateOptions | undefined;
let mouseX: number | undefined;
let isHovering = false;
let tokenSource: CancellationTokenSource;
let hoverDisposable: IDisposable | undefined;
const mouseOverDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_OVER, true);
mouseOverDomEmitter.event((e: MouseEvent) => {
if (isHovering) {
return;
}
tokenSource = new CancellationTokenSource();
function mouseLeaveOrDown(e: MouseEvent): void {
const isMouseDown = e.type === dom.EventType.MOUSE_DOWN;
if (isMouseDown) {
hoverDisposable?.dispose();
hoverDisposable = undefined;
}
if (isMouseDown || (<any>e).fromElement === htmlElement) {
isHovering = false;
hoverOptions = undefined;
tokenSource.dispose(true);
mouseLeaveDomEmitter.dispose();
mouseDownDomEmitter.dispose();
}
}
const mouseLeaveDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_LEAVE, true);
mouseLeaveDomEmitter.event(mouseLeaveOrDown);
const mouseDownDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_DOWN, true);
mouseDownDomEmitter.event(mouseLeaveOrDown);
isHovering = true;
function mouseMove(e: MouseEvent): void {
mouseX = e.x;
}
const mouseMoveDomEmitter = new DomEmitter(htmlElement, dom.EventType.MOUSE_MOVE, true);
mouseMoveDomEmitter.event(mouseMove);
setTimeout(async () => {
if (isHovering && tooltip) {
// Re-use the already computed hover options if they exist.
if (!hoverOptions) {
const target: IHoverDelegateTarget = {
targetElements: [htmlElement],
dispose: () => { }
};
hoverOptions = {
text: localize('iconLabel.loading', "Loading..."),
target,
hoverPosition: HoverPosition.BELOW
};
hoverDisposable = adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
const resolvedTooltip = (await tooltip(tokenSource.token)) ?? (!isString(markdownTooltip) ? markdownTooltip.markdownNotSupportedFallback : undefined);
if (resolvedTooltip) {
hoverOptions = {
text: resolvedTooltip,
target,
hoverPosition: HoverPosition.BELOW
};
// awaiting the tooltip could take a while. Make sure we're still hovering.
hoverDisposable = adjustXAndShowCustomHover(hoverOptions, mouseX, hoverDelegate, isHovering);
} else if (hoverDisposable) {
hoverDisposable.dispose();
hoverDisposable = undefined;
}
}
}
mouseMoveDomEmitter.dispose();
}, hoverDelegate.delay);
});
return mouseOverDomEmitter;
}
function getTooltipForCustom(markdownTooltip: string | IIconLabelMarkdownString): (token: CancellationToken) => Promise<string | IMarkdownString | undefined> {
if (isString(markdownTooltip)) {
return async () => markdownTooltip;
} else if (isFunction(markdownTooltip.markdown)) {
return markdownTooltip.markdown;
} else {
const markdown = markdownTooltip.markdown;
return async () => markdown;
}
}
function adjustXAndShowCustomHover(hoverOptions: IHoverDelegateOptions | undefined, mouseX: number | undefined, hoverDelegate: IHoverDelegate, isHovering: boolean): IDisposable | undefined {
if (hoverOptions && isHovering) {
if (mouseX !== undefined) {
(<IHoverDelegateTarget>hoverOptions.target).x = mouseX + 10;
}
return hoverDelegate.showHover(hoverOptions);
}
return undefined;
}

View file

@ -89,6 +89,16 @@ export function isMarkdownString(thing: any): thing is IMarkdownString {
return false; return false;
} }
export function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean {
if (a === b) {
return true;
} else if (!a || !b) {
return false;
} else {
return a.value === b.value && a.isTrusted === b.isTrusted && a.supportThemeIcons === b.supportThemeIcons;
}
}
export function escapeMarkdownSyntaxTokens(text: string): string { export function escapeMarkdownSyntaxTokens(text: string): string {
// escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash
return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&');

View file

@ -43,6 +43,11 @@ import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
import { CATEGORIES } from 'vs/workbench/common/actions'; import { CATEGORIES } from 'vs/workbench/common/actions';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { hash } from 'vs/base/common/hash'; import { hash } from 'vs/base/common/hash';
import { setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover';
import { IHoverService } from 'vs/workbench/services/hover/browser/hover';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { isMarkdownString, markdownStringEqual } from 'vs/base/common/htmlContent';
import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
interface IStatusbarEntryPriority { interface IStatusbarEntryPriority {
@ -426,6 +431,8 @@ export class StatusbarPart extends Part implements IStatusbarService {
private leftItemsContainer: HTMLElement | undefined; private leftItemsContainer: HTMLElement | undefined;
private rightItemsContainer: HTMLElement | undefined; private rightItemsContainer: HTMLElement | undefined;
private hoverDelegate: IHoverDelegate;
constructor( constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService, @IInstantiationService private readonly instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService, @IThemeService themeService: IThemeService,
@ -434,10 +441,17 @@ export class StatusbarPart extends Part implements IStatusbarService {
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@IContextMenuService private contextMenuService: IContextMenuService, @IContextMenuService private contextMenuService: IContextMenuService,
@IContextKeyService private readonly contextKeyService: IContextKeyService, @IContextKeyService private readonly contextKeyService: IContextKeyService,
@IHoverService hoverService: IHoverService,
@IConfigurationService configurationService: IConfigurationService
) { ) {
super(Parts.STATUSBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); super(Parts.STATUSBAR_PART, { hasTitle: false }, themeService, storageService, layoutService);
this.registerListeners(); this.registerListeners();
this.hoverDelegate = {
showHover: (options: IHoverDelegateOptions) => hoverService.showHover(options),
delay: <number>configurationService.getValue('workbench.hover.delay')
};
} }
private registerListeners(): void { private registerListeners(): void {
@ -489,7 +503,7 @@ export class StatusbarPart extends Part implements IStatusbarService {
// Create item // Create item
const itemContainer = this.doCreateStatusItem(id, alignment, ...coalesce([entry.showBeak ? 'has-beak' : undefined])); const itemContainer = this.doCreateStatusItem(id, alignment, ...coalesce([entry.showBeak ? 'has-beak' : undefined]));
const item = this.instantiationService.createInstance(StatusbarEntryItem, itemContainer, entry); const item = this.instantiationService.createInstance(StatusbarEntryItem, itemContainer, entry, this.hoverDelegate);
// Append to parent // Append to parent
this.appendOneStatusbarEntry(itemContainer, alignment, priority); this.appendOneStatusbarEntry(itemContainer, alignment, priority);
@ -821,6 +835,8 @@ class StatusbarEntryItem extends Disposable {
readonly labelContainer: HTMLElement; readonly labelContainer: HTMLElement;
private readonly label: StatusBarCodiconLabel; private readonly label: StatusBarCodiconLabel;
private customHover: IDisposable | undefined;
private entry: IStatusbarEntry | undefined = undefined; private entry: IStatusbarEntry | undefined = undefined;
get name(): string { return assertIsDefined(this.entry).name; } get name(): string { return assertIsDefined(this.entry).name; }
@ -833,6 +849,7 @@ class StatusbarEntryItem extends Disposable {
constructor( constructor(
private container: HTMLElement, private container: HTMLElement,
entry: IStatusbarEntry, entry: IStatusbarEntry,
private readonly customHoverDelegate: IHoverDelegate,
@ICommandService private readonly commandService: ICommandService, @ICommandService private readonly commandService: ICommandService,
@INotificationService private readonly notificationService: INotificationService, @INotificationService private readonly notificationService: INotificationService,
@ITelemetryService private readonly telemetryService: ITelemetryService, @ITelemetryService private readonly telemetryService: ITelemetryService,
@ -881,8 +898,15 @@ class StatusbarEntryItem extends Disposable {
} }
// Update: Tooltip (on the container, because label can be disabled) // Update: Tooltip (on the container, because label can be disabled)
if (!this.entry || entry.tooltip !== this.entry.tooltip) { if (!this.entry || !isEqualTooltip(this.entry, entry)) {
if (entry.tooltip) { if (this.customHover) {
this.customHover.dispose();
this.customHover = undefined;
}
if (isMarkdownString(entry.tooltip)) {
this.container.removeAttribute('title');
this.customHover = setupCustomHover(this.customHoverDelegate, this.container, { markdown: entry.tooltip, markdownNotSupportedFallback: undefined });
} else if (entry.tooltip) {
this.container.title = entry.tooltip; this.container.title = entry.tooltip;
} else { } else {
this.container.title = ''; this.container.title = '';
@ -993,9 +1017,24 @@ class StatusbarEntryItem extends Disposable {
dispose(this.backgroundListener); dispose(this.backgroundListener);
dispose(this.commandMouseListener); dispose(this.commandMouseListener);
dispose(this.commandKeyboardListener); dispose(this.commandKeyboardListener);
if (this.customHover) {
this.customHover.dispose();
}
} }
} }
function isEqualTooltip(e1: IStatusbarEntry, e2: IStatusbarEntry) {
const t1 = e1.tooltip;
const t2 = e2.tooltip;
if (t1 === undefined) {
return t2 === undefined;
}
if (isMarkdownString(t1)) {
return isMarkdownString(t2) && markdownStringEqual(t1, t2);
}
return t1 === t2;
}
registerThemingParticipant((theme, collector) => { registerThemingParticipant((theme, collector) => {
if (theme.type !== ColorScheme.HIGH_CONTRAST) { if (theme.type !== ColorScheme.HIGH_CONTRAST) {
const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND); const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND);

View file

@ -18,6 +18,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer';
import { isString } from 'vs/base/common/types';
const $ = dom.$; const $ = dom.$;
type TargetRect = { type TargetRect = {
@ -66,7 +67,7 @@ export class HoverWidget extends Widget {
) { ) {
super(); super();
this._linkHandler = options.linkHandler || this._openerService.open; this._linkHandler = options.linkHandler || (url => this._openerService.open(url, { allowCommands: (!isString(options.text) && options.text.isTrusted) }));
this._target = 'targetElements' in options.target ? options.target : new ElementHoverTarget(options.target); this._target = 'targetElements' in options.target ? options.target : new ElementHoverTarget(options.target);

View file

@ -8,6 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle';
import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { ThemeColor } from 'vs/platform/theme/common/themeService';
import { Event } from 'vs/base/common/event'; import { Event } from 'vs/base/common/event';
import { Command } from 'vs/editor/common/modes'; import { Command } from 'vs/editor/common/modes';
import { IMarkdownString } from 'vs/base/common/htmlContent';
export const IStatusbarService = createDecorator<IStatusbarService>('statusbarService'); export const IStatusbarService = createDecorator<IStatusbarService>('statusbarService');
@ -48,7 +49,7 @@ export interface IStatusbarEntry {
/** /**
* An optional tooltip text to show when you hover over the entry * An optional tooltip text to show when you hover over the entry
*/ */
readonly tooltip?: string; readonly tooltip?: string | IMarkdownString;
/** /**
* An optional color to use for the entry * An optional color to use for the entry
@ -74,6 +75,7 @@ export interface IStatusbarEntry {
* Will enable a spinning icon in front of the text to indicate progress. * Will enable a spinning icon in front of the text to indicate progress.
*/ */
readonly showProgress?: boolean; readonly showProgress?: boolean;
} }
export interface IStatusbarService { export interface IStatusbarService {