Skip to content

Commit

Permalink
Polish explorer tooltip rendering (#174085)
Browse files Browse the repository at this point in the history
* Place explorer tooltip rendering behind a setting

* Implement windows hover behavior

* Pass the actual mouse event rather than a synthetic click

* Remove explorer setting

* Remove more code that was added for the setting

* Fix bad merge

* Address PR comments
  • Loading branch information
lramos15 authored Feb 14, 2023
1 parent e8a6622 commit 4119dea
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
height: 100%;
}

.explorer-item-hover {
/* -- Must set important as hover overrides the cursor -- */
cursor: pointer !important;
padding-left: 6px;
height: 22px;
font-size: 13px;
}

.explorer-folders-view .monaco-list-row {
padding-left: 4px; /* align top level twistie with `Explorer` title label */
}
Expand Down
70 changes: 66 additions & 4 deletions src/vs/workbench/contrib/files/browser/views/explorerViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'
import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree';
import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
import { ILabelService } from 'vs/platform/label/common/label';
import { isNumber } from 'vs/base/common/types';
import { isNumber, isStringArray } from 'vs/base/common/types';
import { IEditableData } from 'vs/workbench/common/views';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity';
Expand All @@ -62,6 +62,9 @@ import { ResourceSet } from 'vs/base/common/map';
import { TernarySearchTree } from 'vs/base/common/ternarySearchTree';
import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles';
import { timeout } from 'vs/base/common/async';
import { IHoverDelegate, IHoverDelegateOptions, IHoverWidget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
import { IHoverService } from 'vs/workbench/services/hover/browser/hover';
import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';

export class ExplorerDelegate implements IListVirtualDelegate<ExplorerItem> {

Expand Down Expand Up @@ -275,6 +278,64 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
private _onDidChangeActiveDescendant = new EventMultiplexer<void>();
readonly onDidChangeActiveDescendant = this._onDidChangeActiveDescendant.event;

private readonly hoverDelegate = new class implements IHoverDelegate {

readonly placement = 'element';

get delay() {
return this.configurationService.getValue<number>('workbench.hover.delay');
}

constructor(
private readonly configurationService: IConfigurationService,
private readonly hoverService: IHoverService
) { }

showHover(options: IHoverDelegateOptions, focus?: boolean): IHoverWidget | undefined {
let element: HTMLElement;
if (options.target instanceof HTMLElement) {
element = options.target;
} else {
element = options.target.targetElements[0];
}

const row = element.closest('.monaco-tl-row') as HTMLElement | undefined;

const child = element.querySelector('div.monaco-icon-label-container') as Element | undefined;
const childOfChild = child?.querySelector('span.monaco-icon-name-container') as HTMLElement | undefined;
let overflowed = false;
if (childOfChild && child) {
const width = child.clientWidth;
const childWidth = childOfChild.offsetWidth;
// Check if element is overflowing its parent container
overflowed = width <= childWidth;
}

const hasDecoration = element.classList.toString().includes('monaco-decoration-iconBadge');
// If it's overflowing or has a decoration show the tooltip
overflowed = overflowed || hasDecoration;

const indentGuideElement = row?.querySelector('.monaco-tl-indent') as HTMLElement | undefined;
if (!indentGuideElement) {
return;
}

return overflowed ? this.hoverService.showHover({
...options,
target: indentGuideElement,
compact: true,
additionalClasses: ['explorer-item-hover'],
skipFadeInAnimation: true,
showPointer: false,
onClick: (e) => {
this.hoverService.hideHover();
element.dispatchEvent(new MouseEvent(e.type, { ...e, bubbles: true }));
},
hoverPosition: HoverPosition.RIGHT,
}, focus) : undefined;
}
}(this.configurationService, this.hoverService);

constructor(
container: HTMLElement,
private labels: ResourceLabels,
Expand All @@ -285,7 +346,8 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
@IExplorerService private readonly explorerService: IExplorerService,
@ILabelService private readonly labelService: ILabelService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IContextMenuService private readonly contextMenuService: IContextMenuService
@IContextMenuService private readonly contextMenuService: IContextMenuService,
@IHoverService private readonly hoverService: IHoverService
) {
this.config = this.configurationService.getValue<IFilesConfiguration>();

Expand Down Expand Up @@ -317,8 +379,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu

renderTemplate(container: HTMLElement): IFileTemplateData {
const templateDisposables = new DisposableStore();

const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true }));
const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true, hoverDelegate: this.hoverDelegate }));
templateDisposables.add(label.onDidRender(() => {
try {
if (templateData.currentContext) {
Expand Down Expand Up @@ -417,6 +478,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
const realignNestedChildren = stat.nestedParent && themeIsUnhappyWithNesting;

templateData.label.setResource({ resource: stat.resource, name: label }, {
title: isStringArray(label) ? label[0] : label,
fileKind: stat.isRoot ? FileKind.ROOT_FOLDER : stat.isDirectory ? FileKind.FOLDER : FileKind.FILE,
extraClasses: realignNestedChildren ? [...extraClasses, 'align-nest-icon-with-parent-icon'] : extraClasses,
fileDecorations: this.config.explorer.decorations,
Expand Down
6 changes: 5 additions & 1 deletion src/vs/workbench/services/hover/browser/hover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { IDisposable } from 'vs/base/common/lifecycle';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget';
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';

export const IHoverService = createDecorator<IHoverService>('hoverService');

Expand Down Expand Up @@ -129,6 +128,11 @@ export interface IHoverOptions {
trapFocus?: boolean;

/**
* A callback which will be executed when the hover is clicked
*/
onClick?(e: MouseEvent): void;

/*
* The container to pass to {@link IContextViewProvider.showContextView} which renders the hover
* in. This is particularly useful for more natural tab focusing behavior, where the hover is
* created as the next tab index after the element being hovered and/or to workaround the
Expand Down
5 changes: 5 additions & 0 deletions src/vs/workbench/services/hover/browser/hoverService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ export class HoverService implements IHoverService {
options.container
);
hover.onRequestLayout(() => provider.layout());
if (options.onClick) {
hoverDisposables.add(addDisposableListener(hover.domNode, EventType.CLICK, e => {
options.onClick!(e);
}));
}
if ('targetElements' in options.target) {
for (const element of options.target.targetElements) {
hoverDisposables.add(addDisposableListener(element, EventType.CLICK, () => this.hideHover()));
Expand Down

0 comments on commit 4119dea

Please sign in to comment.