mirror of
https://github.com/Microsoft/vscode
synced 2024-08-25 03:56:03 +00:00
Create IHoverDelegate2.setUpdatableHover
This commit is contained in:
parent
c6a7ca7668
commit
85fb8cb6fd
|
@ -3,6 +3,7 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate';
|
||||
import type { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
|
||||
import type { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import type { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
|
@ -39,6 +40,9 @@ export interface IHoverDelegate2 {
|
|||
* simultaneously. #188822
|
||||
*/
|
||||
showAndFocusLastHover(): void;
|
||||
|
||||
// TODO: Change hoverDelegate arg to exclude the actual delegate and instead use the new options
|
||||
setupUpdatableHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, content: IUpdatableHoverContentOrFactory, options?: IUpdatableHoverOptions): IUpdatableHover;
|
||||
}
|
||||
|
||||
export interface IHoverWidget extends IDisposable {
|
||||
|
@ -237,6 +241,7 @@ export interface IUpdatableHoverTooltipMarkdownString {
|
|||
}
|
||||
|
||||
export type IUpdatableHoverContent = string | IUpdatableHoverTooltipMarkdownString | HTMLElement | undefined;
|
||||
export type IUpdatableHoverContentOrFactory = IUpdatableHoverContent | (() => IUpdatableHoverContent);
|
||||
|
||||
export interface IUpdatableHoverOptions {
|
||||
actions?: IHoverAction[];
|
||||
|
|
|
@ -9,6 +9,7 @@ let baseHoverDelegate: IHoverDelegate2 = {
|
|||
showHover: () => undefined,
|
||||
hideHover: () => undefined,
|
||||
showAndFocusLastHover: () => undefined,
|
||||
setupUpdatableHover: () => null!,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,9 +13,8 @@ import { stripIcons } from 'vs/base/common/iconLabels';
|
|||
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { isFunction, isString } from 'vs/base/common/types';
|
||||
import { localize } from 'vs/nls';
|
||||
import type { IHoverWidget, IUpdatableHover, IUpdatableHoverContent, IUpdatableHoverOptions, IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover';
|
||||
import type { IHoverWidget, IUpdatableHover, IUpdatableHoverContent, IUpdatableHoverContentOrFactory, IUpdatableHoverOptions, IUpdatableHoverTooltipMarkdownString } from 'vs/base/browser/ui/hover/hover';
|
||||
|
||||
type IUpdatableHoverContentOrFactory = IUpdatableHoverContent | (() => IUpdatableHoverContent);
|
||||
type IUpdatableHoverResolvedContent = IMarkdownString | string | HTMLElement | undefined;
|
||||
|
||||
export function setupNativeHover(htmlElement: HTMLElement, tooltip: string | IUpdatableHoverTooltipMarkdownString | undefined): void {
|
||||
|
|
|
@ -20,7 +20,10 @@ import { IAccessibilityService } from 'vs/platform/accessibility/common/accessib
|
|||
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
|
||||
import { mainWindow } from 'vs/base/browser/window';
|
||||
import { ContextViewHandler } from 'vs/platform/contextview/browser/contextViewService';
|
||||
import type { IHoverOptions, IHoverWidget } from 'vs/base/browser/ui/hover/hover';
|
||||
import type { IHoverOptions, IHoverWidget, IUpdatableHover, IUpdatableHoverContentOrFactory, IUpdatableHoverOptions } from 'vs/base/browser/ui/hover/hover';
|
||||
import type { IHoverDelegate, IHoverDelegateTarget } from 'vs/base/browser/ui/hover/hoverDelegate';
|
||||
import { UpdatableHoverWidget } from 'vs/editor/browser/services/hoverService/updatableHoverWidget';
|
||||
import { TimeoutTimer } from 'vs/base/common/async';
|
||||
|
||||
export class HoverService extends Disposable implements IHoverService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
@ -183,6 +186,135 @@ export class HoverService extends Disposable implements IHoverService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupUpdatableHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, content: IUpdatableHoverContentOrFactory, options?: IUpdatableHoverOptions | undefined): IUpdatableHover {
|
||||
|
||||
htmlElement.setAttribute('custom-hover', 'true');
|
||||
|
||||
if (htmlElement.title !== '') {
|
||||
console.warn('HTML element already has a title attribute, which will conflict with the custom hover. Please remove the title attribute.');
|
||||
console.trace('Stack trace:', htmlElement.title);
|
||||
htmlElement.title = '';
|
||||
}
|
||||
|
||||
let hoverPreparation: IDisposable | undefined;
|
||||
let hoverWidget: UpdatableHoverWidget | undefined;
|
||||
|
||||
const hideHover = (disposeWidget: boolean, disposePreparation: boolean) => {
|
||||
const hadHover = hoverWidget !== undefined;
|
||||
if (disposeWidget) {
|
||||
hoverWidget?.dispose();
|
||||
hoverWidget = undefined;
|
||||
}
|
||||
if (disposePreparation) {
|
||||
hoverPreparation?.dispose();
|
||||
hoverPreparation = undefined;
|
||||
}
|
||||
if (hadHover) {
|
||||
hoverDelegate.onDidHideHover?.();
|
||||
hoverWidget = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const triggerShowHover = (delay: number, focus?: boolean, target?: IHoverDelegateTarget) => {
|
||||
return new TimeoutTimer(async () => {
|
||||
if (!hoverWidget || hoverWidget.isDisposed) {
|
||||
hoverWidget = new UpdatableHoverWidget(hoverDelegate, target || htmlElement, delay > 0);
|
||||
await hoverWidget.update(typeof content === 'function' ? content() : content, focus, options);
|
||||
}
|
||||
}, delay);
|
||||
};
|
||||
|
||||
let isMouseDown = false;
|
||||
const mouseDownEmitter = addDisposableListener(htmlElement, EventType.MOUSE_DOWN, () => {
|
||||
isMouseDown = true;
|
||||
hideHover(true, true);
|
||||
}, true);
|
||||
const mouseUpEmitter = addDisposableListener(htmlElement, EventType.MOUSE_UP, () => {
|
||||
isMouseDown = false;
|
||||
}, true);
|
||||
const mouseLeaveEmitter = addDisposableListener(htmlElement, EventType.MOUSE_LEAVE, (e: MouseEvent) => {
|
||||
isMouseDown = false;
|
||||
hideHover(false, (<any>e).fromElement === htmlElement);
|
||||
}, true);
|
||||
|
||||
const onMouseOver = (e: MouseEvent) => {
|
||||
if (hoverPreparation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toDispose: DisposableStore = new DisposableStore();
|
||||
|
||||
const target: IHoverDelegateTarget = {
|
||||
targetElements: [htmlElement],
|
||||
dispose: () => { }
|
||||
};
|
||||
if (hoverDelegate.placement === undefined || hoverDelegate.placement === 'mouse') {
|
||||
// track the mouse position
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
target.x = e.x + 10;
|
||||
if ((e.target instanceof HTMLElement) && getHoverTargetElement(e.target, htmlElement) !== htmlElement) {
|
||||
hideHover(true, true);
|
||||
}
|
||||
};
|
||||
toDispose.add(addDisposableListener(htmlElement, EventType.MOUSE_MOVE, onMouseMove, true));
|
||||
}
|
||||
|
||||
hoverPreparation = toDispose;
|
||||
|
||||
if ((e.target instanceof HTMLElement) && getHoverTargetElement(e.target as HTMLElement, htmlElement) !== htmlElement) {
|
||||
return; // Do not show hover when the mouse is over another hover target
|
||||
}
|
||||
|
||||
toDispose.add(triggerShowHover(hoverDelegate.delay, false, target));
|
||||
};
|
||||
const mouseOverDomEmitter = addDisposableListener(htmlElement, EventType.MOUSE_OVER, onMouseOver, true);
|
||||
|
||||
const onFocus = () => {
|
||||
if (isMouseDown || hoverPreparation) {
|
||||
return;
|
||||
}
|
||||
const target: IHoverDelegateTarget = {
|
||||
targetElements: [htmlElement],
|
||||
dispose: () => { }
|
||||
};
|
||||
const toDispose: DisposableStore = new DisposableStore();
|
||||
const onBlur = () => hideHover(true, true);
|
||||
toDispose.add(addDisposableListener(htmlElement, EventType.BLUR, onBlur, true));
|
||||
toDispose.add(triggerShowHover(hoverDelegate.delay, false, target));
|
||||
hoverPreparation = toDispose;
|
||||
};
|
||||
|
||||
// Do not show hover when focusing an input or textarea
|
||||
let focusDomEmitter: undefined | IDisposable;
|
||||
const tagName = htmlElement.tagName.toLowerCase();
|
||||
if (tagName !== 'input' && tagName !== 'textarea') {
|
||||
focusDomEmitter = addDisposableListener(htmlElement, EventType.FOCUS, onFocus, true);
|
||||
}
|
||||
|
||||
const hover: IUpdatableHover = {
|
||||
show: focus => {
|
||||
hideHover(false, true); // terminate a ongoing mouse over preparation
|
||||
triggerShowHover(0, focus); // show hover immediately
|
||||
},
|
||||
hide: () => {
|
||||
hideHover(true, true);
|
||||
},
|
||||
update: async (newContent, hoverOptions) => {
|
||||
content = newContent;
|
||||
await hoverWidget?.update(content, undefined, hoverOptions);
|
||||
},
|
||||
dispose: () => {
|
||||
mouseOverDomEmitter.dispose();
|
||||
mouseLeaveEmitter.dispose();
|
||||
mouseDownEmitter.dispose();
|
||||
mouseUpEmitter.dispose();
|
||||
focusDomEmitter?.dispose();
|
||||
hideHover(true, true);
|
||||
}
|
||||
};
|
||||
return hover;
|
||||
}
|
||||
}
|
||||
|
||||
function getHoverOptionsIdentity(options: IHoverOptions | undefined): IHoverOptions | number | string | undefined {
|
||||
|
@ -227,6 +359,14 @@ class HoverContextViewDelegate implements IDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
function getHoverTargetElement(element: HTMLElement, stopElement?: HTMLElement): HTMLElement {
|
||||
stopElement = stopElement ?? getWindow(element).document.body;
|
||||
while (!element.hasAttribute('custom-hover') && element !== stopElement) {
|
||||
element = element.parentElement!;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
registerSingleton(IHoverService, HoverService, InstantiationType.Delayed);
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type { IHoverWidget, IUpdatableHoverContent, IUpdatableHoverOptions } from 'vs/base/browser/ui/hover/hover';
|
||||
import type { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/hover/hoverDelegate';
|
||||
import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { isMarkdownString, type IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { isFunction, isString } from 'vs/base/common/types';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
type IUpdatableHoverResolvedContent = IMarkdownString | string | HTMLElement | undefined;
|
||||
|
||||
export class UpdatableHoverWidget implements IDisposable {
|
||||
|
||||
private _hoverWidget: IHoverWidget | undefined;
|
||||
private _cancellationTokenSource: CancellationTokenSource | undefined;
|
||||
|
||||
constructor(private hoverDelegate: IHoverDelegate, private target: IHoverDelegateTarget | HTMLElement, private fadeInAnimation: boolean) {
|
||||
}
|
||||
|
||||
async update(content: IUpdatableHoverContent, focus?: boolean, options?: IUpdatableHoverOptions): Promise<void> {
|
||||
if (this._cancellationTokenSource) {
|
||||
// there's an computation ongoing, cancel it
|
||||
this._cancellationTokenSource.dispose(true);
|
||||
this._cancellationTokenSource = undefined;
|
||||
}
|
||||
if (this.isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let resolvedContent;
|
||||
if (content === undefined || isString(content) || content instanceof HTMLElement) {
|
||||
resolvedContent = content;
|
||||
} else if (!isFunction(content.markdown)) {
|
||||
resolvedContent = content.markdown ?? content.markdownNotSupportedFallback;
|
||||
} else {
|
||||
// compute the content, potentially long-running
|
||||
|
||||
// show 'Loading' if no hover is up yet
|
||||
if (!this._hoverWidget) {
|
||||
this.show(localize('iconLabel.loading', "Loading..."), focus);
|
||||
}
|
||||
|
||||
// compute the content
|
||||
this._cancellationTokenSource = new CancellationTokenSource();
|
||||
const token = this._cancellationTokenSource.token;
|
||||
resolvedContent = await content.markdown(token);
|
||||
if (resolvedContent === undefined) {
|
||||
resolvedContent = content.markdownNotSupportedFallback;
|
||||
}
|
||||
|
||||
if (this.isDisposed || token.isCancellationRequested) {
|
||||
// either the widget has been closed in the meantime
|
||||
// or there has been a new call to `update`
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.show(resolvedContent, focus, options);
|
||||
}
|
||||
|
||||
private show(content: IUpdatableHoverResolvedContent, focus?: boolean, options?: IUpdatableHoverOptions): void {
|
||||
const oldHoverWidget = this._hoverWidget;
|
||||
|
||||
if (this.hasContent(content)) {
|
||||
const hoverOptions: IHoverDelegateOptions = {
|
||||
content,
|
||||
target: this.target,
|
||||
appearance: {
|
||||
showPointer: this.hoverDelegate.placement === 'element',
|
||||
skipFadeInAnimation: !this.fadeInAnimation || !!oldHoverWidget, // do not fade in if the hover is already showing
|
||||
},
|
||||
position: {
|
||||
hoverPosition: HoverPosition.BELOW,
|
||||
},
|
||||
...options
|
||||
};
|
||||
|
||||
this._hoverWidget = this.hoverDelegate.showHover(hoverOptions, focus);
|
||||
}
|
||||
oldHoverWidget?.dispose();
|
||||
}
|
||||
|
||||
private hasContent(content: IUpdatableHoverResolvedContent): content is NonNullable<IUpdatableHoverResolvedContent> {
|
||||
if (!content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isMarkdownString(content)) {
|
||||
return !!content.value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
get isDisposed() {
|
||||
return this._hoverWidget?.isDisposed;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._hoverWidget?.dispose();
|
||||
this._cancellationTokenSource?.dispose(true);
|
||||
this._cancellationTokenSource = undefined;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue