mirror of
https://github.com/Microsoft/vscode
synced 2024-09-18 01:58:27 +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 { 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);
|
this.customHovers.clear();
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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;
|
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, '\\$&');
|
||||||
|
|
|
@ -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,8 +1017,23 @@ 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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue