From a3bef1e7e69b1c150106e66a8a6fa2087926aafa Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Fri, 10 Feb 2023 11:03:02 -0700 Subject: [PATCH 1/7] Place explorer tooltip rendering behind a setting --- src/vs/base/browser/ui/iconLabel/iconLabel.ts | 5 +++-- src/vs/workbench/browser/labels.ts | 4 ++-- src/vs/workbench/contrib/files/browser/files.contribution.ts | 5 +++++ .../workbench/contrib/files/browser/views/explorerViewer.ts | 5 +++++ src/vs/workbench/contrib/files/common/files.ts | 1 + 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index 599abeaf6a177..a24ca9a1b0643 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -12,6 +12,7 @@ import { IMatch } from 'vs/base/common/filters'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { equals } from 'vs/base/common/objects'; import { Range } from 'vs/base/common/range'; +import { withNullAsUndefined } from 'vs/base/common/types'; export interface IIconLabelCreationOptions { readonly supportHighlights?: boolean; @@ -21,7 +22,7 @@ export interface IIconLabelCreationOptions { } export interface IIconLabelValueOptions { - title?: string | ITooltipMarkdownString; + title?: string | ITooltipMarkdownString | null; descriptionTitle?: string; hideIcon?: boolean; extraClasses?: readonly string[]; @@ -140,7 +141,7 @@ export class IconLabel extends Disposable { this.domNode.className = labelClasses.join(' '); this.labelContainer.className = containerClasses.join(' '); - this.setupHover(options?.descriptionTitle ? this.labelContainer : this.element, options?.title); + this.setupHover(options?.descriptionTitle ? this.labelContainer : this.element, withNullAsUndefined(options?.title)); this.nameNode.setLabel(label, options); diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 44a200e8fb31c..772c8de9345f1 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -562,9 +562,9 @@ class ResourceLabelWidget extends IconLabel { this.computedPathLabel = this.labelService.getUriLabel(resource); } - if (!iconLabelOptions.title || (typeof iconLabelOptions.title === 'string')) { + if (iconLabelOptions.title === undefined || (typeof iconLabelOptions.title === 'string')) { iconLabelOptions.title = this.computedPathLabel; - } else if (!iconLabelOptions.title.markdownNotSupportedFallback) { + } else if (iconLabelOptions.title && !iconLabelOptions.title.markdownNotSupportedFallback) { iconLabelOptions.title.markdownNotSupportedFallback = this.computedPathLabel; } } diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index ffd761948639e..e00d4a863aaaf 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -431,6 +431,11 @@ configurationRegistry.registerConfiguration({ 'description': nls.localize('expandSingleFolderWorkspaces', "Controls whether the Explorer should expand multi-root workspaces containing only one folder during initialization"), 'default': true }, + 'explorer.renderTooltip': { + 'type': 'boolean', + 'description': nls.localize('explorer.renderTooltip', "Controls whether the Explorer should render the tooltip containing the full path on hover."), + 'default': true + }, 'explorer.sortOrder': { 'type': 'string', 'enum': [SortOrder.Default, SortOrder.Mixed, SortOrder.FilesFirst, SortOrder.Type, SortOrder.Modified, SortOrder.FoldersNestsFiles], diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 65a9ad1461435..9ba43c1d8a741 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -298,6 +298,9 @@ export class FilesRenderer implements ICompressibleTreeRenderer { if (e.affectsConfiguration('explorer')) { this.config = this.configurationService.getValue(); + if (e.affectsConfiguration('explorer.renderTooltip')) { + this.explorerService.refresh(); + } } if (e.affectsConfiguration('workbench.tree.indent')) { updateOffsetStyles(); @@ -417,6 +420,8 @@ export class FilesRenderer implements ICompressibleTreeRenderer Date: Mon, 13 Feb 2023 13:02:42 -0700 Subject: [PATCH 2/7] Implement windows hover behavior --- .../files/browser/views/explorerViewer.ts | 67 +++++++++++++++++-- .../workbench/services/hover/browser/hover.ts | 5 ++ .../services/hover/browser/hoverService.ts | 14 ++++ 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 9ba43c1d8a741..7bc4aeaf69e29 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -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'; @@ -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 { @@ -275,6 +278,60 @@ export class FilesRenderer implements ICompressibleTreeRenderer(); readonly onDidChangeActiveDescendant = this._onDidChangeActiveDescendant.event; + private readonly hoverDelegate = new class implements IHoverDelegate { + + readonly placement = 'element'; + + get delay() { + return this.configurationService.getValue('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 child = element.children[0] as Element | undefined; + const childOfChild = child?.children[0] 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; + // TODO @lramos15 find a better way to do this, grabs the indent element to position hover above the label + const indentGuideElement = element.parentElement?.parentElement?.children[0] as HTMLElement | undefined; + + if (!indentGuideElement) { + return; + } + + return overflowed ? this.hoverService.showHover({ + ...options, + target: indentGuideElement, + compact: true, + additionalClasses: ['explorer-item-hover'], + skipFadeInAnimation: true, + showPointer: false, + forwardClickEvent: true, + hoverPosition: HoverPosition.RIGHT, + }, focus) : undefined; + } + }(this.configurationService, this.hoverService); + constructor( container: HTMLElement, private labels: ResourceLabels, @@ -285,7 +342,8 @@ export class FilesRenderer implements ICompressibleTreeRenderer(); @@ -320,8 +378,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer { try { if (templateData.currentContext) { @@ -421,7 +478,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer provider.layout()); + if (options.forwardClickEvent) { + hoverDisposables.add(addDisposableListener(hover.domNode, EventType.CLICK, e => { + // Forward the click to the target element(s) + if (options.target) { + if ('targetElements' in options.target) { + for (const element of options.target.targetElements) { + element.click(); + } + } else { + options.target.click(); + } + } + })); + } if ('targetElements' in options.target) { for (const element of options.target.targetElements) { hoverDisposables.add(addDisposableListener(element, EventType.CLICK, () => this.hideHover())); From c6545e0f3cc17e59aebd3e8d33f679bed8493963 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Mon, 13 Feb 2023 13:40:44 -0700 Subject: [PATCH 3/7] Pass the actual mouse event rather than a synthetic click --- src/vs/workbench/services/hover/browser/hoverService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/services/hover/browser/hoverService.ts b/src/vs/workbench/services/hover/browser/hoverService.ts index 2796ed9fa0fe3..a30819b31552c 100644 --- a/src/vs/workbench/services/hover/browser/hoverService.ts +++ b/src/vs/workbench/services/hover/browser/hoverService.ts @@ -67,10 +67,10 @@ export class HoverService implements IHoverService { if (options.target) { if ('targetElements' in options.target) { for (const element of options.target.targetElements) { - element.click(); + element.dispatchEvent(new MouseEvent(e.type, { ...e, bubbles: true })); } } else { - options.target.click(); + options.target.dispatchEvent(new MouseEvent(e.type, { ...e, bubbles: true })); } } })); From d469adff49d2153a167ac2af6a44484d6e5a8cb2 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Mon, 13 Feb 2023 13:58:32 -0700 Subject: [PATCH 4/7] Remove explorer setting --- src/vs/workbench/contrib/files/browser/files.contribution.ts | 5 ----- .../workbench/contrib/files/browser/views/explorerViewer.ts | 5 +---- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index e00d4a863aaaf..ffd761948639e 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -431,11 +431,6 @@ configurationRegistry.registerConfiguration({ 'description': nls.localize('expandSingleFolderWorkspaces', "Controls whether the Explorer should expand multi-root workspaces containing only one folder during initialization"), 'default': true }, - 'explorer.renderTooltip': { - 'type': 'boolean', - 'description': nls.localize('explorer.renderTooltip', "Controls whether the Explorer should render the tooltip containing the full path on hover."), - 'default': true - }, 'explorer.sortOrder': { 'type': 'string', 'enum': [SortOrder.Default, SortOrder.Mixed, SortOrder.FilesFirst, SortOrder.Type, SortOrder.Modified, SortOrder.FoldersNestsFiles], diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 7bc4aeaf69e29..b4f507d711b2e 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -356,9 +356,6 @@ export class FilesRenderer implements ICompressibleTreeRenderer { if (e.affectsConfiguration('explorer')) { this.config = this.configurationService.getValue(); - if (e.affectsConfiguration('explorer.renderTooltip')) { - this.explorerService.refresh(); - } } if (e.affectsConfiguration('workbench.tree.indent')) { updateOffsetStyles(); @@ -478,7 +475,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer Date: Mon, 13 Feb 2023 14:14:26 -0700 Subject: [PATCH 5/7] Remove more code that was added for the setting --- src/vs/base/browser/ui/iconLabel/iconLabel.ts | 5 ++--- src/vs/workbench/browser/labels.ts | 4 ++-- src/vs/workbench/contrib/files/common/files.ts | 1 - 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/vs/base/browser/ui/iconLabel/iconLabel.ts b/src/vs/base/browser/ui/iconLabel/iconLabel.ts index a24ca9a1b0643..599abeaf6a177 100644 --- a/src/vs/base/browser/ui/iconLabel/iconLabel.ts +++ b/src/vs/base/browser/ui/iconLabel/iconLabel.ts @@ -12,7 +12,6 @@ import { IMatch } from 'vs/base/common/filters'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { equals } from 'vs/base/common/objects'; import { Range } from 'vs/base/common/range'; -import { withNullAsUndefined } from 'vs/base/common/types'; export interface IIconLabelCreationOptions { readonly supportHighlights?: boolean; @@ -22,7 +21,7 @@ export interface IIconLabelCreationOptions { } export interface IIconLabelValueOptions { - title?: string | ITooltipMarkdownString | null; + title?: string | ITooltipMarkdownString; descriptionTitle?: string; hideIcon?: boolean; extraClasses?: readonly string[]; @@ -141,7 +140,7 @@ export class IconLabel extends Disposable { this.domNode.className = labelClasses.join(' '); this.labelContainer.className = containerClasses.join(' '); - this.setupHover(options?.descriptionTitle ? this.labelContainer : this.element, withNullAsUndefined(options?.title)); + this.setupHover(options?.descriptionTitle ? this.labelContainer : this.element, options?.title); this.nameNode.setLabel(label, options); diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 772c8de9345f1..44a200e8fb31c 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -562,9 +562,9 @@ class ResourceLabelWidget extends IconLabel { this.computedPathLabel = this.labelService.getUriLabel(resource); } - if (iconLabelOptions.title === undefined || (typeof iconLabelOptions.title === 'string')) { + if (!iconLabelOptions.title || (typeof iconLabelOptions.title === 'string')) { iconLabelOptions.title = this.computedPathLabel; - } else if (iconLabelOptions.title && !iconLabelOptions.title.markdownNotSupportedFallback) { + } else if (!iconLabelOptions.title.markdownNotSupportedFallback) { iconLabelOptions.title.markdownNotSupportedFallback = this.computedPathLabel; } } diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index 4dbdada044390..c9141c6880f70 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -95,7 +95,6 @@ export interface IFilesConfiguration extends PlatformIFilesConfiguration, IWorkb enableUndo: boolean; confirmUndo: UndoConfirmLevel; expandSingleFolderWorkspaces: boolean; - renderTooltip: boolean; sortOrder: SortOrder; sortOrderLexicographicOptions: LexicographicOptions; decorations: { From 57938a38952b51d934ea2201c1562d4085fb0cc1 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Tue, 14 Feb 2023 10:20:43 -0700 Subject: [PATCH 6/7] Fix bad merge --- src/vs/workbench/services/hover/browser/hover.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/services/hover/browser/hover.ts b/src/vs/workbench/services/hover/browser/hover.ts index 99ea8717b1ebd..fd60f09885efd 100644 --- a/src/vs/workbench/services/hover/browser/hover.ts +++ b/src/vs/workbench/services/hover/browser/hover.ts @@ -133,6 +133,7 @@ export interface IHoverOptions { */ forwardClickEvent?: boolean; + /* * 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 From ffefdf6a69b619e155b6fe964b1354eb9e9e9316 Mon Sep 17 00:00:00 2001 From: Logan Ramos Date: Tue, 14 Feb 2023 11:49:28 -0700 Subject: [PATCH 7/7] Address PR comments --- .../files/browser/media/explorerviewlet.css | 8 ++++++++ .../contrib/files/browser/views/explorerViewer.ts | 15 +++++++++------ src/vs/workbench/services/hover/browser/hover.ts | 5 ++--- .../services/hover/browser/hoverService.ts | 13 ++----------- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index 89e5008d3c429..ba3fc0686f6d5 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -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 */ } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index b4f507d711b2e..709f03e594836 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -299,8 +299,10 @@ export class FilesRenderer implements ICompressibleTreeRenderer { + this.hoverService.hideHover(); + element.dispatchEvent(new MouseEvent(e.type, { ...e, bubbles: true })); + }, hoverPosition: HoverPosition.RIGHT, }, focus) : undefined; } @@ -474,7 +478,6 @@ export class FilesRenderer implements ICompressibleTreeRenderer('hoverService'); @@ -129,9 +128,9 @@ export interface IHoverOptions { trapFocus?: boolean; /** - * Whether to forward all click events to the target element. + * A callback which will be executed when the hover is clicked */ - forwardClickEvent?: boolean; + onClick?(e: MouseEvent): void; /* * The container to pass to {@link IContextViewProvider.showContextView} which renders the hover diff --git a/src/vs/workbench/services/hover/browser/hoverService.ts b/src/vs/workbench/services/hover/browser/hoverService.ts index 6829a5f9ff1d1..ab51a888eb3d2 100644 --- a/src/vs/workbench/services/hover/browser/hoverService.ts +++ b/src/vs/workbench/services/hover/browser/hoverService.ts @@ -64,18 +64,9 @@ export class HoverService implements IHoverService { options.container ); hover.onRequestLayout(() => provider.layout()); - if (options.forwardClickEvent) { + if (options.onClick) { hoverDisposables.add(addDisposableListener(hover.domNode, EventType.CLICK, e => { - // Forward the click to the target element(s) - if (options.target) { - if ('targetElements' in options.target) { - for (const element of options.target.targetElements) { - element.dispatchEvent(new MouseEvent(e.type, { ...e, bubbles: true })); - } - } else { - options.target.dispatchEvent(new MouseEvent(e.type, { ...e, bubbles: true })); - } - } + options.onClick!(e); })); } if ('targetElements' in options.target) {