mirror of
https://github.com/Microsoft/vscode
synced 2024-08-28 13:29:44 +00:00
support custom hover on status bar entries
This commit is contained in:
parent
f55a5243a4
commit
8d41153ffc
|
@ -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;
|
||||
}
|
|
@ -10,13 +10,10 @@ import { IMatch } from 'vs/base/common/filters';
|
|||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Range } from 'vs/base/common/range';
|
||||
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 { isFunction, isString } from 'vs/base/common/types';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { localize } from 'vs/nls';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { setupCustomHover, setupNativeHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover';
|
||||
|
||||
export interface IIconLabelCreationOptions {
|
||||
supportHighlights?: boolean;
|
||||
|
@ -91,17 +88,17 @@ class FastLabelNode {
|
|||
|
||||
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 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();
|
||||
|
||||
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'))));
|
||||
}
|
||||
|
||||
if (options?.hoverDelegate) {
|
||||
this.hoverDelegate = options.hoverDelegate;
|
||||
}
|
||||
this.hoverDelegate = options?.hoverDelegate;
|
||||
}
|
||||
|
||||
get element(): HTMLElement {
|
||||
|
@ -185,116 +180,21 @@ export class IconLabel extends Disposable {
|
|||
}
|
||||
|
||||
if (!this.hoverDelegate) {
|
||||
return this.setupNativeHover(htmlElement, tooltip);
|
||||
setupNativeHover(htmlElement, tooltip);
|
||||
} 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 {
|
||||
if (hoverOptions && isHovering) {
|
||||
if (mouseX !== undefined) {
|
||||
(<IHoverDelegateTarget>hoverOptions.target).x = mouseX + 10;
|
||||
}
|
||||
return hoverDelegate.showHover(hoverOptions);
|
||||
public override dispose() {
|
||||
super.dispose();
|
||||
for (const disposable of this.customHovers.values()) {
|
||||
disposable.dispose();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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;
|
||||
this.customHovers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
129
src/vs/base/browser/ui/iconLabel/iconLabelHover.ts
Normal file
129
src/vs/base/browser/ui/iconLabel/iconLabelHover.ts
Normal 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;
|
||||
}
|
|
@ -89,6 +89,16 @@ export function isMarkdownString(thing: any): thing is IMarkdownString {
|
|||
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 {
|
||||
// escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash
|
||||
return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&');
|
||||
|
|
|
@ -43,6 +43,11 @@ import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
|
|||
import { CATEGORIES } from 'vs/workbench/common/actions';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
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 {
|
||||
|
||||
|
@ -426,6 +431,8 @@ export class StatusbarPart extends Part implements IStatusbarService {
|
|||
private leftItemsContainer: HTMLElement | undefined;
|
||||
private rightItemsContainer: HTMLElement | undefined;
|
||||
|
||||
private hoverDelegate: IHoverDelegate;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
|
@ -434,10 +441,17 @@ export class StatusbarPart extends Part implements IStatusbarService {
|
|||
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
|
||||
@IContextMenuService private contextMenuService: IContextMenuService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IHoverService hoverService: IHoverService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
) {
|
||||
super(Parts.STATUSBAR_PART, { hasTitle: false }, themeService, storageService, layoutService);
|
||||
|
||||
this.registerListeners();
|
||||
|
||||
this.hoverDelegate = {
|
||||
showHover: (options: IHoverDelegateOptions) => hoverService.showHover(options),
|
||||
delay: <number>configurationService.getValue('workbench.hover.delay')
|
||||
};
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
|
@ -489,7 +503,7 @@ export class StatusbarPart extends Part implements IStatusbarService {
|
|||
|
||||
// Create item
|
||||
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
|
||||
this.appendOneStatusbarEntry(itemContainer, alignment, priority);
|
||||
|
@ -821,6 +835,8 @@ class StatusbarEntryItem extends Disposable {
|
|||
readonly labelContainer: HTMLElement;
|
||||
private readonly label: StatusBarCodiconLabel;
|
||||
|
||||
private customHover: IDisposable | undefined;
|
||||
|
||||
private entry: IStatusbarEntry | undefined = undefined;
|
||||
get name(): string { return assertIsDefined(this.entry).name; }
|
||||
|
||||
|
@ -833,6 +849,7 @@ class StatusbarEntryItem extends Disposable {
|
|||
constructor(
|
||||
private container: HTMLElement,
|
||||
entry: IStatusbarEntry,
|
||||
private readonly customHoverDelegate: IHoverDelegate,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
|
@ -881,8 +898,15 @@ class StatusbarEntryItem extends Disposable {
|
|||
}
|
||||
|
||||
// Update: Tooltip (on the container, because label can be disabled)
|
||||
if (!this.entry || entry.tooltip !== this.entry.tooltip) {
|
||||
if (entry.tooltip) {
|
||||
if (!this.entry || !isEqualTooltip(this.entry, entry)) {
|
||||
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;
|
||||
} else {
|
||||
this.container.title = '';
|
||||
|
@ -993,9 +1017,24 @@ class StatusbarEntryItem extends Disposable {
|
|||
dispose(this.backgroundListener);
|
||||
dispose(this.commandMouseListener);
|
||||
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) => {
|
||||
if (theme.type !== ColorScheme.HIGH_CONTRAST) {
|
||||
const statusBarItemHoverBackground = theme.getColor(STATUS_BAR_ITEM_HOVER_BACKGROUND);
|
||||
|
|
|
@ -18,6 +18,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
|
|||
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer';
|
||||
import { isString } from 'vs/base/common/types';
|
||||
|
||||
const $ = dom.$;
|
||||
type TargetRect = {
|
||||
|
@ -66,7 +67,7 @@ export class HoverWidget extends Widget {
|
|||
) {
|
||||
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);
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle';
|
|||
import { ThemeColor } from 'vs/platform/theme/common/themeService';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { Command } from 'vs/editor/common/modes';
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
|
||||
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
|
||||
*/
|
||||
readonly tooltip?: string;
|
||||
readonly tooltip?: string | IMarkdownString;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
readonly showProgress?: boolean;
|
||||
|
||||
}
|
||||
|
||||
export interface IStatusbarService {
|
||||
|
|
Loading…
Reference in a new issue