diff --git a/.gitignore b/.gitignore index 60a636e6c..fe56f1749 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ **/bin/ **/target/ **/guide/**/*.html +**/guide/**/*.svg /gef-updates *~ *.rej diff --git a/org.eclipse.draw2d.doc.isv/guide-src/guide.adoc b/org.eclipse.draw2d.doc.isv/guide-src/guide.adoc index d21ff9118..8c9c273b7 100644 --- a/org.eclipse.draw2d.doc.isv/guide-src/guide.adoc +++ b/org.eclipse.draw2d.doc.isv/guide-src/guide.adoc @@ -20,3 +20,4 @@ coordinates, working with absolute coordinates behavior * xref:migration-guide.adoc[Plug-in Migration Guide] - changes between individual releases +* xref:hidpi.adoc[High-DPI Support] - taming the native display zoom diff --git a/org.eclipse.draw2d.doc.isv/guide-src/hidpi.adoc b/org.eclipse.draw2d.doc.isv/guide-src/hidpi.adoc new file mode 100644 index 000000000..bafb80a21 --- /dev/null +++ b/org.eclipse.draw2d.doc.isv/guide-src/hidpi.adoc @@ -0,0 +1,44 @@ +ifdef::env-github[] +:imagesdir: ../guide/ +endif::[] + += High-DPI Support + +Modern versions of SWT automatically scale the Draw2D figures by the native display zoom. In Draw2D, this feature is +enabled by default. If clients require more control over the scaling behavior, this functionality may be configured by +passing a link:../reference/api/org/eclipse/draw2d/MonitorAwareLightweightSystem.html[`MonitorAwareLightweightSystem`], +when creating a new link:../reference/api/org/eclipse/draw2d/FigureCanvas.html[`FigureCanvas`] instance. + +[source,java] +---- +FigureCanvas figureCanvas = new FigureCanvas(shell, new MonitorAwareLightweightSystem()); +---- + +This custom lightweight-system performs three actions: + +1) It disables the auto-scaling that would normally be done by SWT, by setting the widget-defined `AUTOSCALE_DISABLED` + property. + +2) It creates a link:../reference/api/org/eclipse/draw2d/internal/MonitorAwareViewport.html[`MonitorAwareViewport`] that is used + to inject a link:../reference/api/org/eclipse/draw2d/ScalableLayeredPane.html[`ScalableLayeredPane`] between the root figure + and the contents of the viewport. + +3) It hooks a listener to the canvas that updates the scale of the scalable pane, whenever the native zoom of the widget + is changed. + +image:images/hidpi-lws.png[Figure Hierarchy of the MonitorAwareLightweightSystem] + +Clients may optionally set the `draw2d.autoScale` system property to define a scaling that is independent from the display zoom. +Similar to the SWT system property, the value must be a positive integer. + +Example: 100, 125, 200, which correlate to 100%, 125% and 200%. + +In order to use this custom LightweightSystem in a GEF view, override the `createLightweightSystem()` method of the +`GraphicalViewerImpl`. + +[source,java] +---- +protected LightweightSystem createLightweightSystem() { + return new MonitorAwareLightweightSystem(); +} +---- \ No newline at end of file diff --git a/org.eclipse.draw2d.doc.isv/guide-src/images/hidpi-lws.svg b/org.eclipse.draw2d.doc.isv/guide-src/images/hidpi-lws.svg new file mode 100644 index 000000000..68830d925 --- /dev/null +++ b/org.eclipse.draw2d.doc.isv/guide-src/images/hidpi-lws.svg @@ -0,0 +1,322 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Root Figure + + + contents + + + + MonitorAwareLightweightSystem + + + + FigureCanvas + + + + + + + MonitorAwareViewport + + + Scalable Pane + + + + Viewport + + + + + diff --git a/org.eclipse.draw2d.doc.isv/guide/images/hidpi-lws.png b/org.eclipse.draw2d.doc.isv/guide/images/hidpi-lws.png new file mode 100644 index 000000000..1597627cf Binary files /dev/null and b/org.eclipse.draw2d.doc.isv/guide/images/hidpi-lws.png differ diff --git a/org.eclipse.draw2d.doc.isv/topics_Guide.xml b/org.eclipse.draw2d.doc.isv/topics_Guide.xml index 0297c4c34..c99dc0b6a 100644 --- a/org.eclipse.draw2d.doc.isv/topics_Guide.xml +++ b/org.eclipse.draw2d.doc.isv/topics_Guide.xml @@ -15,6 +15,8 @@ + + diff --git a/org.eclipse.draw2d.examples/src/org/eclipse/draw2d/examples/AutoScaleExample.java b/org.eclipse.draw2d.examples/src/org/eclipse/draw2d/examples/AutoScaleExample.java new file mode 100644 index 000000000..040549bd4 --- /dev/null +++ b/org.eclipse.draw2d.examples/src/org/eclipse/draw2d/examples/AutoScaleExample.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ + +package org.eclipse.draw2d.examples; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +import org.eclipse.draw2d.ColorConstants; +import org.eclipse.draw2d.Figure; +import org.eclipse.draw2d.FigureCanvas; +import org.eclipse.draw2d.FlowLayout; +import org.eclipse.draw2d.Graphics; +import org.eclipse.draw2d.IFigure; +import org.eclipse.draw2d.MonitorAwareLightweightSystem; +import org.eclipse.draw2d.geometry.Dimension; + +public class AutoScaleExample { + public static void main(String[] args) { + System.setProperty("swt.autoScale.updateOnRuntime", Boolean.TRUE.toString()); //$NON-NLS-1$ + System.setProperty("draw2d.autoScale", "200"); //$NON-NLS-1$ //$NON-NLS-2$ + Shell shell = new Shell(); + shell.setSize(400, 400); + shell.setLayout(new FillLayout()); + + FigureCanvas canvas = new FigureCanvas(shell, SWT.NONE, new MonitorAwareLightweightSystem()); + Figure root = new Figure(); + root.setLayoutManager(new ListLayout()); + root.add(createLabel(ColorConstants.red)); + root.add(createLabel(ColorConstants.green)); + root.add(createLabel(ColorConstants.blue)); + root.add(createLabel(ColorConstants.yellow)); + root.add(createLabel(ColorConstants.cyan)); + root.add(createLabel(ColorConstants.white)); + root.add(createLabel(ColorConstants.gray)); + root.add(createLabel(ColorConstants.orange)); + canvas.getViewport().setContentsTracksHeight(true); + canvas.getViewport().setContentsTracksWidth(true); + canvas.getViewport().setContents(root); + + shell.open(); + + Display display = shell.getDisplay(); + while (!display.isDisposed()) { + display.readAndDispatch(); + } + } + + private static IFigure createLabel(Color bg) { + return new Figure() { + @Override + protected void paintFigure(Graphics graphics) { + super.paintFigure(graphics); + graphics.setBackgroundColor(bg); + graphics.fillRectangle(getClientArea()); + } + + @Override + public Dimension getPreferredSize(int wHint, int hHint) { + return new Dimension(wHint, 100); + } + }; + } + + private static class ListLayout extends FlowLayout { + public ListLayout() { + setMajorSpacing(0); + } + } +} diff --git a/org.eclipse.draw2d.tests/src/org/eclipse/draw2d/test/Draw2dTestSuite.java b/org.eclipse.draw2d.tests/src/org/eclipse/draw2d/test/Draw2dTestSuite.java index 40bf1af01..94784e728 100644 --- a/org.eclipse.draw2d.tests/src/org/eclipse/draw2d/test/Draw2dTestSuite.java +++ b/org.eclipse.draw2d.tests/src/org/eclipse/draw2d/test/Draw2dTestSuite.java @@ -60,7 +60,8 @@ InsetsTest.class, DirectedGraphLayoutTest.class, ScrollPaneTests.class, - LabelTest.class + LabelTest.class, + HiDPITest.class }) public class Draw2dTestSuite { } diff --git a/org.eclipse.draw2d.tests/src/org/eclipse/draw2d/test/HiDPITest.java b/org.eclipse.draw2d.tests/src/org/eclipse/draw2d/test/HiDPITest.java new file mode 100644 index 000000000..a49453343 --- /dev/null +++ b/org.eclipse.draw2d.tests/src/org/eclipse/draw2d/test/HiDPITest.java @@ -0,0 +1,151 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ + +package org.eclipse.draw2d.test; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Shell; + +import org.eclipse.draw2d.Figure; +import org.eclipse.draw2d.FigureCanvas; +import org.eclipse.draw2d.IFigure; +import org.eclipse.draw2d.MonitorAwareLightweightSystem; +import org.eclipse.draw2d.ScalableLayeredPane; +import org.eclipse.draw2d.Viewport; +import org.eclipse.draw2d.XYLayout; +import org.eclipse.draw2d.geometry.Rectangle; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class HiDPITest extends BaseTestCase { + private Shell shell; + private FigureCanvas canvas; + + private Figure parent; + private IFigure figure1; + private IFigure figure2; + + @BeforeEach + public void setUp() { + shell = new Shell(SWT.NO_TRIM); + shell.setSize(400, 400); + shell.setLayout(new FillLayout()); + + figure1 = new Figure(); + figure1.setBounds(Rectangle.SINGLETON.setBounds(50, 50, 100, 100)); + figure2 = new Figure(); + figure2.setBounds(Rectangle.SINGLETON.setBounds(200, 200, 50, 50)); + + parent = new Figure(); + parent.setLayoutManager(new XYLayout()); + parent.add(figure1); + parent.add(figure2); + + canvas = new FigureCanvas(shell, new MonitorAwareLightweightSystem()); + canvas.setContents(parent); + } + + @AfterEach + public void tearDown() { + shell.dispose(); + } + + @Test + public void test_FigureCanvas_setViewport1() { + canvas.setViewport(new Viewport()); + // Only set the ScalableLayeredPane when there is something to set + assertNull(canvas.getContents()); + } + + @Test + public void test_FigureCanvas_setViewport2() { + IFigure figure = new Figure(); + canvas.setViewport(new Viewport()); + canvas.setContents(figure); + // Inject the ScalableLayeredPane when a new Viewport is set + IFigure contents = canvas.getContents(); + assertTrue(contents instanceof ScalableLayeredPane); + assertEquals(contents.getChildren().size(), 1); + assertEquals(contents.getChildren().get(0), figure); + } + + @Test + public void test_FigureCanvas_setContents() { + Figure figure = new Figure(); + canvas.setContents(figure); + // Update contents of ScalableLayeredPane together with Viewport + IFigure contents = canvas.getContents(); + assertTrue(contents instanceof ScalableLayeredPane); + assertEquals(contents.getChildren().size(), 1); + assertEquals(contents.getChildren().get(0), figure); + } + + @Test + public void test_Viewport_getContents() { + IFigure contents = canvas.getContents(); + assertTrue(contents instanceof ScalableLayeredPane); + assertEquals(contents.getChildren().size(), 1); + assertEquals(contents.getChildren().get(0), parent); + } + + @Test + public void test_Viewport_getContents_Scale() { + ScalableLayeredPane contents = (ScalableLayeredPane) canvas.getContents(); + assertEquals(contents.getScale(), 1.0); + canvas.notifyListeners(SWT.ZoomChanged, createZoomEvent(200)); + assertEquals(contents.getScale(), 2.0); + } + + @Test + public void test_Contents_NoZoom() { + Rectangle bounds1 = figure1.getBounds().getCopy(); + Rectangle bounds2 = figure2.getBounds().getCopy(); + + assertEquals(50, 50, 100, 100, bounds1); + assertEquals(200, 200, 50, 50, bounds2); + + figure1.translateToAbsolute(bounds1); + figure2.translateToAbsolute(bounds2); + + assertEquals(50, 50, 100, 100, bounds1); + assertEquals(200, 200, 50, 50, bounds2); + } + + @Test + public void test_Contents_200Zoom() { + canvas.notifyListeners(SWT.ZoomChanged, createZoomEvent(200)); + + Rectangle bounds1 = figure1.getBounds().getCopy(); + Rectangle bounds2 = figure2.getBounds().getCopy(); + + assertEquals(50, 50, 100, 100, bounds1); + assertEquals(200, 200, 50, 50, bounds2); + + figure1.translateToAbsolute(bounds1); + figure2.translateToAbsolute(bounds2); + + assertEquals(100, 100, 200, 200, bounds1); + assertEquals(400, 400, 100, 100, bounds2); + } + + private static Event createZoomEvent(int zoom) { + Event event = new Event(); + event.type = SWT.ZoomChanged; + event.detail = zoom; + return event; + } +} diff --git a/org.eclipse.draw2d/META-INF/MANIFEST.MF b/org.eclipse.draw2d/META-INF/MANIFEST.MF index fbc441744..403cc1ae5 100644 --- a/org.eclipse.draw2d/META-INF/MANIFEST.MF +++ b/org.eclipse.draw2d/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %Plugin.name Bundle-SymbolicName: org.eclipse.draw2d;singleton:=true -Bundle-Version: 3.20.100.qualifier +Bundle-Version: 3.21.0.qualifier Bundle-Vendor: %Plugin.providerName Bundle-Localization: plugin Export-Package: org.eclipse.draw2d, diff --git a/org.eclipse.draw2d/src/org/eclipse/draw2d/MonitorAwareLightweightSystem.java b/org.eclipse.draw2d/src/org/eclipse/draw2d/MonitorAwareLightweightSystem.java new file mode 100644 index 000000000..d3c1d4909 --- /dev/null +++ b/org.eclipse.draw2d/src/org/eclipse/draw2d/MonitorAwareLightweightSystem.java @@ -0,0 +1,218 @@ +/******************************************************************************* + * Copyright (c) 2025 Patrick Ziegler and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Patrick Ziegler - initial API and implementation + *******************************************************************************/ + +package org.eclipse.draw2d; + +import java.beans.PropertyChangeListener; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Control; + +/** + * Subclass of the {@link LightweightSystem} to handle native HiDPI scaling. + * + *

+ * Primary purpose of this class is to take over the scaling that would normally + * be done by SWT. When this class is used, all figures are painted at 100% zoom + * and then up-scaled to match the display zoom, in an attempt to reduce the + * number of visual artifacts that would otherwise be introduced as a result of + * rounding errors that result from fractional scaling. + *

+ * + *

+ * Important: This class modifies the viewport that is set as its + * contents. Calling {@link Viewport#getContents()} returns a + * {@link ScalableLayeredPane} with the figure that is set via + * {@link Viewport#setContents(IFigure)} as its only child. + * + *

+ * + *

+ * EXPERIMENTAL. This class or interface has been added as part + * of a work in progress. There is no guarantee that this API will work nor that + * it will remain the same. Please do not use this API without consulting with + * the GEF-classic team. + *

+ * + * @since 3.21 + */ +public class MonitorAwareLightweightSystem extends LightweightSystem { + + /** + * Data set by SWT that describes the native device zoom used for this widget. + */ + private static final String DATA_NATIVE_ZOOM = "NATIVE_ZOOM"; //$NON-NLS-1$ + + /** + * Data that can be set to scale this widget at 100%. + */ + private static final String DATA_AUTOSCALE_DISABLED = "AUTOSCALE_DISABLED"; //$NON-NLS-1$ + + /** + * Environment variable that when enabled, takes over the native HiDPI scaling + * that would otherwise be done by SWT. The value needs to be a positive + * integer. + */ + private static final String PROP_DRAW2D_AUTO_SCALE = "draw2d.autoScale"; //$NON-NLS-1$ + + /** + * Set via the {@link #PROP_DRAW2D_AUTO_SCALE} property. If not {@code null}, + * the auto-scaling done by SWT is disabled and instead, the lightweight-system + * is scaled by this factor. + */ + private static final Double DRAW2D_AUTO_SCALE; + + static { + String autoScale = System.getProperty(PROP_DRAW2D_AUTO_SCALE); + if (autoScale != null) { + int zoom = parseIntUnchecked(autoScale); + DRAW2D_AUTO_SCALE = zoom / 100.0; + } else { + DRAW2D_AUTO_SCALE = null; + } + } + + /** + * Convenience method to convert the integer defined by + * {@link #PROP_DRAW2D_AUTO_SCALE} without throwing a checked exception. + * + * @param property The property to convert. + * @return The integer described by the given string. + */ + private static int parseIntUnchecked(String property) { + try { + return Integer.parseInt(property); + } catch (NumberFormatException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + /** + * Returns the native device zoom for this widget. + * + * @return 1.0 at 100%, 1.25 at 125% etc. + */ + private static double getNativeZoom(Control c) { + Object displayZoom = c.getData(DATA_NATIVE_ZOOM); + if (displayZoom == null) { + return 1.0; + } + return Double.valueOf(displayZoom.toString()) / 100.0; + } + + /** + * The scalable pane that is injected between the root figure and the contents + * of this viewport. + */ + private ScalableLayeredPane scalablePane; + + /** + * This property-change listener is hooked to the viewport of this + * lightweight-system and will automatically inject the scalable pane when + * {@link Viewport#setContents(IFigure)} is called. + */ + private final PropertyChangeListener viewportListener; + + public MonitorAwareLightweightSystem() { + scalablePane = new ScalableLayeredPane(false); + scalablePane.setLayoutManager(new StackLayout()); + viewportListener = event -> { + if (Viewport.PROPERTY_CONTENTS.equals(event.getPropertyName())) { + Viewport viewport = (Viewport) event.getSource(); + // ignore event fired when calling viewport.setContents(scalablePane) + if (scalablePane == viewport.getContents()) { + return; + } + + injectScalablePane(viewport); + } + }; + } + + /** + * Updates the scale factor of the scalable pane to the given value. This value + * is ignored if {@link #PROP_DRAW2D_AUTO_SCALE} has been set. + * + * @param nativeZoom The new scale factor. + */ + private void setScale(double nativeZoom) { + if (DRAW2D_AUTO_SCALE != null) { + scalablePane.setScale(DRAW2D_AUTO_SCALE); + } else { + scalablePane.setScale(nativeZoom); + } + } + + @Override + public void setControl(Canvas c) { + if (c == null) { + return; + } + + c.setData(DATA_AUTOSCALE_DISABLED, true); + c.addListener(SWT.ZoomChanged, e -> setScale(e.detail / 100.0)); + setScale(getNativeZoom(c)); + + super.setControl(c); + } + + @Override + public void setContents(IFigure figure) { + if (contents instanceof Viewport vp) { + unhookViewport(vp); + } + super.setContents(figure); + if (figure instanceof Viewport vp) { + hookViewport(vp); + } + } + + /** + * Remove the property-change listener from the given viewport. Called from + * {@link #setContents(IFigure)}. + * + * @param viewport The old viewport of this lightweight-system. + */ + private void unhookViewport(Viewport viewport) { + viewport.removePropertyChangeListener(viewportListener); + } + + /** + * Add the property-change listener to the given viewport and inject the + * scalable pane. Called from {@link #setContents(IFigure)}. + * + * @param viewport The new viewport of this lightweight-system. + */ + private void hookViewport(Viewport viewport) { + injectScalablePane(viewport); + viewport.addPropertyChangeListener(viewportListener); + } + + /** + * Injects a scalable pane between the given viewport and its contents. The old + * contents is moved and becomes a child of the scalable pane. + * + * @param viewport The viewport of this lightweight-system. + */ + private void injectScalablePane(Viewport viewport) { + IFigure contents = viewport.getContents(); + + scalablePane.removeAll(); + + if (contents != null) { + viewport.setContents(scalablePane); + scalablePane.add(contents); + } + } +} diff --git a/org.eclipse.draw2d/src/org/eclipse/draw2d/Viewport.java b/org.eclipse.draw2d/src/org/eclipse/draw2d/Viewport.java index f66906fd5..9ef30f6c1 100644 --- a/org.eclipse.draw2d/src/org/eclipse/draw2d/Viewport.java +++ b/org.eclipse.draw2d/src/org/eclipse/draw2d/Viewport.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2010 IBM Corporation and others. + * Copyright (c) 2000, 2025 IBM Corporation and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at @@ -27,6 +27,13 @@ public class Viewport extends Figure implements PropertyChangeListener { /** ID for the view location property */ public static final String PROPERTY_VIEW_LOCATION = "viewLocation"; //$NON-NLS-1$ + /** + * ID for the contents property. An event with this ID is fired whenever the + * content of this viewport is set. + * + * @see #setContents(IFigure) + */ + /* package */ static final String PROPERTY_CONTENTS = "contents"; //$NON-NLS-1$ private IFigure view; private boolean useTranslate = false; @@ -223,6 +230,7 @@ public void setContents(IFigure figure) { if (view == figure) { return; } + IFigure oldView = view; if (view != null) { remove(view); } @@ -230,6 +238,7 @@ public void setContents(IFigure figure) { if (view != null) { add(figure); } + firePropertyChange(PROPERTY_CONTENTS, oldView, view); } /**