Skip to content

Commit

Permalink
Add preference for visualPreview on hover
Browse files Browse the repository at this point in the history
Made window.tabbar.enhancedPreview an enum preference with these options:
- classic: a simple unstyled preview, containing the name
- enhanced: a styled preview containing title and caption (#12350)
- visual: the enhanced preview +  visual preview of the view
Extended the hover service so that it supports the visual preview.
Added the `PreviewableWidget` interface.
Widgets, implementing this interface, can be previewed once they were loaded.
The loaded flag is set to true when a widget is set to active.
Widgets implementing the interface can specify how the preview should look like.
The default is simply the node of the widget.
Webviews are currently not previewable, as they raise some challenges.
For example, scripts in the webviews would need to be handled/blocked.
Therefore, an approach for webviews should be tackled in a follow up.

Fixes #12646

Contributed on behalf of STMicroelectronics
  • Loading branch information
sgraband committed Jun 22, 2023
1 parent 8831c9d commit fe3e7eb
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 7 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
## v1.39.0 - 06/29/2023

- [debug] added support for conditional exception breakpoints [#12445](https://github.com/eclipse-theia/theia/pull/12445)
- [core] made the `window.tabbar.enhancedPreview` preference an enum with 3 options: [#12648](https://github.com/eclipse-theia/theia/pull/12648) - Contributed on behalf of STMicroelectronics
- `classic`: Display a simple preview about the view, containing the name.
- `enhanced`: Display an enhanced preview containing the name and a caption. (The behavior introduced in [#12350](https://github.com/eclipse-theia/theia/pull/12350))
- `visual`: Display the enhanced preview together with a visual preview of the view. (The preview support was added with this PR)

<a name="breaking_changes_1.39.0">[Breaking Changes:](#breaking_changes_1.39.0)</a>

Expand Down
14 changes: 10 additions & 4 deletions packages/core/src/browser/core-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,15 @@ export const corePreferenceSchema: PreferenceSchema = {
markdownDescription: nls.localizeByDefault('Controls the dispatching logic for key presses to use either `code` (recommended) or `keyCode`.')
},
'window.tabbar.enhancedPreview': {
type: 'boolean',
default: false,
description: nls.localize('theia/core/enhancedPreview', 'Controls whether more information about the tab should be displayed in horizontal tab bars.')
type: 'string',
enum: ['classic', 'enhanced', 'visual'],
markdownEnumDescriptions: [
nls.localize('theia/core/enhancedPreview/classic', 'Display a simple preview about the view, containing the name.'),
nls.localize('theia/core/enhancedPreview/enhanced', 'Display an enhanced preview containing the name and a caption.'),
nls.localize('theia/core/enhancedPreview/visual', 'Display the enhanced preview together with a visual preview of the view.'),
],
default: 'classic',
description: nls.localize('theia/core/enhancedPreview', 'Controls what information about the tab should be displayed in horizontal tab bars, when hovering.')
},
'window.menuBarVisibility': {
type: 'string',
Expand Down Expand Up @@ -263,7 +269,7 @@ export interface CoreConfiguration {
'breadcrumbs.enabled': boolean;
'files.encoding': string;
'keyboard.dispatch': 'code' | 'keyCode';
'window.tabbar.enhancedPreview': boolean;
'window.tabbar.enhancedPreview': 'classic' | 'enhanced' | 'visual';
'window.menuBarVisibility': 'classic' | 'visible' | 'hidden' | 'compact';
'window.title': string;
'window.titleSeparator': string;
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/browser/hover-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export interface HoverRequest {
* Used to style certain boxes different e.g. for the extended tab preview.
*/
cssClasses?: string[]
/**
* A function to render a visual preview on the hover.
* Function that takes the desired width and returns a HTMLElement to be rendered.
*/
visualPreview?: (width: number) => HTMLElement | undefined;
}

@injectable()
Expand Down Expand Up @@ -106,25 +111,38 @@ export class HoverService {

protected async renderHover(request: HoverRequest): Promise<void> {
const host = this.hoverHost;
let firstChild: HTMLElement | undefined;
const { target, content, position, cssClasses } = request;
if (cssClasses) {
host.classList.add(...cssClasses);
}
this.hoverTarget = target;
if (content instanceof HTMLElement) {
host.appendChild(content);
firstChild = content;
} else if (typeof content === 'string') {
host.textContent = content;
} else {
const renderedContent = this.markdownRenderer.render(content);
this.disposeOnHide.push(renderedContent);
host.appendChild(renderedContent.element);
firstChild = renderedContent.element;
}
// browsers might insert linebreaks when the hover appears at the edge of the window
// resetting the position prevents that
host.style.left = '0px';
host.style.top = '0px';
document.body.append(host);

if (request.visualPreview) {
// If just a string is being rendered use the size of the outer box
const width = firstChild ? firstChild.offsetWidth : this.hoverHost.offsetWidth;
const visualPreview = request.visualPreview(Math.max(250, width));
if (visualPreview) {
host.appendChild(visualPreview);
}
}

await animationFrame(); // Allow the browser to size the host
const updatedPosition = this.setHostPosition(target, host, position);

Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/browser/shell/application-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { nls } from '../../common/nls';
import { SecondaryWindowHandler } from '../secondary-window-handler';
import URI from '../../common/uri';
import { OpenerService } from '../opener-service';
import { PreviewableWidget } from '../widgets/previewable-widget';

/** The class name added to ApplicationShell instances. */
const APPLICATION_SHELL_CLASS = 'theia-ApplicationShell';
Expand Down Expand Up @@ -1184,6 +1185,9 @@ export class ApplicationShell extends Widget {
newValue['onCloseRequest'](msg);
};
this.toDisposeOnActiveChanged.push(Disposable.create(() => newValue['onCloseRequest'] = onCloseRequest));
if (PreviewableWidget.is(newValue)) {
newValue.loaded = true;
}
}
this.onDidChangeActiveWidgetEmitter.fire(args);
}
Expand Down
57 changes: 55 additions & 2 deletions packages/core/src/browser/shell/tab-bars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { HoverService } from '../hover-service';
import { Root, createRoot } from 'react-dom/client';
import { SelectComponent } from '../widgets/select-component';
import { createElement } from 'react';
import { PreviewableWidget } from '../widgets/previewable-widget';

/** The class name added to hidden content nodes, which are required to render vertical side bars. */
const HIDDEN_CONTENT_CLASS = 'theia-TabBar-hidden-content';
Expand Down Expand Up @@ -164,7 +165,9 @@ export class TabBarRenderer extends TabBar.Renderer {
? nls.localizeByDefault('Unpin')
: nls.localizeByDefault('Close');

const hover = this.tabBar && (this.tabBar.orientation === 'horizontal' && !this.corePreferences?.['window.tabbar.enhancedPreview']) ? { title: title.caption } : {
const hover = this.tabBar && (this.tabBar.orientation === 'horizontal' && this.corePreferences?.['window.tabbar.enhancedPreview'] === 'classic')
? { title: title.caption }
: {
onmouseenter: this.handleMouseEnterEvent
};

Expand Down Expand Up @@ -501,6 +504,55 @@ export class TabBarRenderer extends TabBar.Renderer {
return hoverBox;
};

protected renderVisualPreview(desiredWidth: number, title: Title<Widget>): HTMLElement | undefined {
const widget = title.owner;
// Check that the widget is not currently shown, is a PreviewableWidget and it was already loaded before
if (this.tabBar && this.tabBar.currentTitle !== title && PreviewableWidget.isPreviewable(widget)) {
const html = document.getElementById(widget.id);
if (html) {
const previewNode: Node | undefined = widget.getPreviewNode();
if (previewNode) {
const clonedNode = previewNode.cloneNode(true);
const visualPreviewDiv = document.createElement('div');
visualPreviewDiv.classList.add('enhanced-preview-div');
// Add the clonedNode and get it from the children to have a HTMLElement instead of a Node
visualPreviewDiv.append(clonedNode);
const visualPreview = visualPreviewDiv.children.item(visualPreviewDiv.children.length - 1);
if (visualPreview instanceof HTMLElement) {
visualPreview.classList.remove('p-mod-hidden');
visualPreview.classList.add('enhanced-preview');
visualPreview.id = `preview:${widget.id}`;

// Use the current visible editor as a fallback if not available
const height: number = visualPreview.style.height === '' ? this.tabBar.currentTitle!.owner.node.offsetHeight : parseFloat(visualPreview.style.height);
const width: number = visualPreview.style.width === '' ? this.tabBar.currentTitle!.owner.node.offsetWidth : parseFloat(visualPreview.style.width);
const ratio = height / width;
visualPreviewDiv.style.width = `${desiredWidth}px`;
visualPreviewDiv.style.height = `${desiredWidth * ratio}px`;

const scale = desiredWidth / width;
visualPreview.style.transform = `scale(${scale},${scale})`;
visualPreview.style.removeProperty('top');
visualPreview.style.removeProperty('left');

// Copy canvases (They are cloned empty)
const originalCanvases = html.getElementsByTagName('canvas');
const previewCanvases = visualPreview.getElementsByTagName('canvas');
// If this is not given, something went wrong during the cloning
if (originalCanvases.length === previewCanvases.length) {
for (let i = 0; i < originalCanvases.length; i++) {
previewCanvases[i].getContext('2d')?.drawImage(originalCanvases[i], 0, 0);
}
}

return visualPreviewDiv;
}
}
}
}
return undefined;
}

protected handleMouseEnterEvent = (event: MouseEvent) => {
if (this.tabBar && this.hoverService && event.currentTarget instanceof HTMLElement) {
const id = event.currentTarget.id;
Expand All @@ -511,7 +563,8 @@ export class TabBarRenderer extends TabBar.Renderer {
content: this.renderEnhancedPreview(title),
target: event.currentTarget,
position: 'bottom',
cssClasses: ['extended-tab-preview']
cssClasses: ['extended-tab-preview'],
visualPreview: this.corePreferences?.['window.tabbar.enhancedPreview'] === 'visual' ? width => this.renderVisualPreview(width, title) : undefined
});
} else {
this.hoverService.requestHover({
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/browser/style/tabs.css
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,16 @@
margin: 0px 4px;
}

.enhanced-preview-div {
margin: 4px 4px;
pointer-events: none;
background: var(--theia-editor-background);
}

.enhanced-preview {
transform-origin: top left;
}

.theia-horizontal-tabBar-hover-title {
font-weight: bolder;
font-size: medium;
Expand Down
30 changes: 30 additions & 0 deletions packages/core/src/browser/widgets/previewable-widget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// *****************************************************************************
// Copyright (C) 2023 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { isFunction, isObject } from '../../common';

export interface PreviewableWidget {
loaded?: boolean;
getPreviewNode(): Node | undefined;
}

export namespace PreviewableWidget {
export function is(arg: unknown): arg is PreviewableWidget {
return isObject<PreviewableWidget>(arg) && isFunction(arg.getPreviewNode);
}
export function isPreviewable(arg: unknown): arg is PreviewableWidget {
return isObject<PreviewableWidget>(arg) && isFunction(arg.getPreviewNode) && arg.loaded === true;
}
}
7 changes: 6 additions & 1 deletion packages/core/src/browser/widgets/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { Emitter, Event, Disposable, DisposableCollection, MaybePromise, isObjec
import { KeyCode, KeysOrKeyCodes } from '../keyboard/keys';

import PerfectScrollbar from 'perfect-scrollbar';
import { PreviewableWidget } from '../widgets/previewable-widget';

decorate(injectable(), Widget);
decorate(unmanaged(), Widget, 0);
Expand Down Expand Up @@ -93,7 +94,7 @@ export namespace UnsafeWidgetUtilities {
}

@injectable()
export class BaseWidget extends Widget {
export class BaseWidget extends Widget implements PreviewableWidget {

protected readonly onScrollYReachEndEmitter = new Emitter<void>();
readonly onScrollYReachEnd: Event<void> = this.onScrollYReachEndEmitter.event;
Expand Down Expand Up @@ -216,6 +217,10 @@ export class BaseWidget extends Widget {
this.toDisposeOnDetach.push(addClipboardListener(element, type, listener));
}

getPreviewNode(): Node | undefined {
return this.node;
}

override setFlag(flag: Widget.Flag): void {
super.setFlag(flag);
if (flag === Widget.Flag.IsVisible) {
Expand Down
4 changes: 4 additions & 0 deletions packages/preferences/src/browser/views/preference-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ export class PreferencesWidget extends Panel implements StatefulWidget {
this.update();
}

getPreviewNode(): Node | undefined {
return this.node;
}

storeState(): PreferencesWidgetState {
return {
scopeTabBarState: this.tabBarWidget.storeState(),
Expand Down

0 comments on commit fe3e7eb

Please sign in to comment.