diff --git a/CHANGELOG.md b/CHANGELOG.md
index d49e58e21eaa1..130c6c0e32385 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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: [#](https://github.com/eclipse-theia/theia/pull/) - 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)
[Breaking Changes:](#breaking_changes_1.39.0)
diff --git a/packages/core/src/browser/core-preferences.ts b/packages/core/src/browser/core-preferences.ts
index 5b81d80ea1d48..96f17489d313e 100644
--- a/packages/core/src/browser/core-preferences.ts
+++ b/packages/core/src/browser/core-preferences.ts
@@ -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',
@@ -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;
diff --git a/packages/core/src/browser/hover-service.ts b/packages/core/src/browser/hover-service.ts
index 360a004dd6b32..ac8d1576381a1 100644
--- a/packages/core/src/browser/hover-service.ts
+++ b/packages/core/src/browser/hover-service.ts
@@ -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()
@@ -106,6 +111,7 @@ export class HoverService {
protected async renderHover(request: HoverRequest): Promise {
const host = this.hoverHost;
+ let firstChild: HTMLElement | undefined;
const { target, content, position, cssClasses } = request;
if (cssClasses) {
host.classList.add(...cssClasses);
@@ -113,18 +119,30 @@ export class HoverService {
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);
diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts
index 4901fc4065796..40ba5d85ed30d 100644
--- a/packages/core/src/browser/shell/application-shell.ts
+++ b/packages/core/src/browser/shell/application-shell.ts
@@ -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';
@@ -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);
}
diff --git a/packages/core/src/browser/shell/tab-bars.ts b/packages/core/src/browser/shell/tab-bars.ts
index 918384027076f..e972a86ef9930 100644
--- a/packages/core/src/browser/shell/tab-bars.ts
+++ b/packages/core/src/browser/shell/tab-bars.ts
@@ -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';
@@ -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
};
@@ -501,6 +504,55 @@ export class TabBarRenderer extends TabBar.Renderer {
return hoverBox;
};
+ protected renderVisualPreview(desiredWidth: number, title: Title): 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;
@@ -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({
diff --git a/packages/core/src/browser/style/tabs.css b/packages/core/src/browser/style/tabs.css
index 21b9b9885a5b2..70aa5915d19cb 100644
--- a/packages/core/src/browser/style/tabs.css
+++ b/packages/core/src/browser/style/tabs.css
@@ -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;
diff --git a/packages/core/src/browser/widgets/previewable-widget.ts b/packages/core/src/browser/widgets/previewable-widget.ts
new file mode 100644
index 0000000000000..c241c7c2c15ea
--- /dev/null
+++ b/packages/core/src/browser/widgets/previewable-widget.ts
@@ -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 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(arg) && isFunction(arg.getPreviewNode);
+ }
+ export function isPreviewable(arg: unknown): arg is PreviewableWidget {
+ return isObject(arg) && isFunction(arg.getPreviewNode) && arg.loaded === true;
+ }
+}
diff --git a/packages/core/src/browser/widgets/widget.ts b/packages/core/src/browser/widgets/widget.ts
index f9afe9d6e77c5..8ea9b5959e141 100644
--- a/packages/core/src/browser/widgets/widget.ts
+++ b/packages/core/src/browser/widgets/widget.ts
@@ -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);
@@ -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();
readonly onScrollYReachEnd: Event = this.onScrollYReachEndEmitter.event;
@@ -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) {
diff --git a/packages/preferences/src/browser/views/preference-widget.tsx b/packages/preferences/src/browser/views/preference-widget.tsx
index 2256fec5737e9..709e29a2a9fcf 100644
--- a/packages/preferences/src/browser/views/preference-widget.tsx
+++ b/packages/preferences/src/browser/views/preference-widget.tsx
@@ -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(),