diff --git a/org.eclipse.wb.doc.user/html-src/whatsnew/v121.asciidoc b/org.eclipse.wb.doc.user/html-src/whatsnew/v121.asciidoc new file mode 100644 index 000000000..3aba06e1e --- /dev/null +++ b/org.eclipse.wb.doc.user/html-src/whatsnew/v121.asciidoc @@ -0,0 +1,21 @@ +ifdef::env-github[] +:imagesdir: ../../html/whatsnew +endif::[] + += What's New - v1.21.0 + +== Swing + +- Improved HighDPI support for Swing components + +Previously, the preview for Swing components was taken at 100% zoom and then +artificially upscaled to the current display zoom, leading to blurry results in +the designer. Components are now directly captured with the correct zoom level. + +[cols="a,a"] +|=== +| image:images/1.21/Scaling_Old.png[Old scaling at 200% zoom] +| image:images/1.21/Scaling_New.png[New scaling at 200% zoom] +|=== + +What's new - xref:v120.adoc[*v1.20.0*] \ No newline at end of file diff --git a/org.eclipse.wb.doc.user/html/whatsnew/images/1.21/Scaling_New.png b/org.eclipse.wb.doc.user/html/whatsnew/images/1.21/Scaling_New.png new file mode 100644 index 000000000..f5973620e Binary files /dev/null and b/org.eclipse.wb.doc.user/html/whatsnew/images/1.21/Scaling_New.png differ diff --git a/org.eclipse.wb.doc.user/html/whatsnew/images/1.21/Scaling_Old.png b/org.eclipse.wb.doc.user/html/whatsnew/images/1.21/Scaling_Old.png new file mode 100644 index 000000000..94930a3b3 Binary files /dev/null and b/org.eclipse.wb.doc.user/html/whatsnew/images/1.21/Scaling_Old.png differ diff --git a/org.eclipse.wb.doc.user/toc.xml b/org.eclipse.wb.doc.user/toc.xml index c0e41c367..ac4774888 100644 --- a/org.eclipse.wb.doc.user/toc.xml +++ b/org.eclipse.wb.doc.user/toc.xml @@ -2,7 +2,7 @@ - + diff --git a/org.eclipse.wb.swing/src/org/eclipse/wb/internal/swing/utils/SwingImageUtils.java b/org.eclipse.wb.swing/src/org/eclipse/wb/internal/swing/utils/SwingImageUtils.java index 471bb1839..183a790c6 100644 --- a/org.eclipse.wb.swing/src/org/eclipse/wb/internal/swing/utils/SwingImageUtils.java +++ b/org.eclipse.wb.swing/src/org/eclipse/wb/internal/swing/utils/SwingImageUtils.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2011, 2024 Google, Inc. and others. + * Copyright (c) 2011, 2025 Google, Inc. 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 @@ -28,6 +28,8 @@ import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageDataProvider; +import org.eclipse.swt.graphics.PaletteData; import org.apache.commons.lang3.StringUtils; @@ -36,6 +38,7 @@ import java.awt.Container; import java.awt.Dialog; import java.awt.EventQueue; +import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Point; import java.awt.Window; @@ -44,6 +47,7 @@ import java.awt.image.ImageConsumer; import java.awt.image.ImageProducer; import java.util.ArrayList; +import java.util.HashMap; import java.util.Hashtable; import java.util.Map; import java.util.WeakHashMap; @@ -72,7 +76,8 @@ public class SwingImageUtils { * @return the {@link ImageDescriptor} of given {@link Component}. */ public static ImageDescriptor createComponentShot(final Component component) throws Exception { - return SwingUtils.runObjectLaterAndWait(() -> convertImage_AWT_to_SWT(createComponentShotAWT(component))); + double zoom = getDisplayZoom(component); + return SwingUtils.runObjectLaterAndWait(() -> convertImage_AWT_to_SWT(createComponentShotAWT(component), zoom)); } /** @@ -82,13 +87,14 @@ public static ImageDescriptor createComponentShot(final Component component) thr static java.awt.Image createComponentShotAWT(final Component component) throws Exception { Assert.isNotNull(component); // prepare sizes - final int componentWidth = component.getWidth(); - final int componentHeight = component.getHeight(); + final double componentZoom = getDisplayZoom(component); + final int componentWidth = (int) (component.getWidth() * componentZoom); + final int componentHeight = (int) (component.getHeight() * componentZoom); final int imageWidth = Math.max(1, componentWidth); final int imageHeight = Math.max(1, componentHeight); // prepare empty image - final BufferedImage componentImage = - new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB); + final BufferedImage componentImage = component.getGraphicsConfiguration() // + .createCompatibleImage(imageWidth, imageHeight); // If actual size on component is zero, then we are done. if (componentWidth == 0 || componentHeight == 0) { return componentImage; @@ -101,7 +107,12 @@ static java.awt.Image createComponentShotAWT(final Component component) throws E // Linux only: it seems that printAll() should be invoked in AWT dispatch thread // to prevent deadlocks between main thread and AWT event queue. // See also SwingUtils.invokeLaterAndWait(). - runInDispatchThread(() -> component.printAll(componentImage.getGraphics())); + runInDispatchThread(() -> { + Graphics2D graphics = componentImage.createGraphics(); + graphics.scale(componentZoom, componentZoom); + component.printAll(graphics); + graphics.dispose(); + }); } finally { shotConfigurator.dispose(); } @@ -500,24 +511,23 @@ private static void fetchMenuVisualData_items(MenuVisualData menuData, Container // //////////////////////////////////////////////////////////////////////////// /** - * Converts AWT image into SWT one. Yours, C.O. ;-) + * Converts AWT image at 100% zoom into SWT one. Yours, C.O. ;-) */ public static ImageDescriptor convertImage_AWT_to_SWT(final java.awt.Image image) throws Exception { + return convertImage_AWT_to_SWT(image, 1.0); + } + + /** + * Converts AWT image at given zoom level into SWT one. Yours, C.O. ;-) + */ + public static ImageDescriptor convertImage_AWT_to_SWT(final java.awt.Image image, double zoom) throws Exception { return SwingUtils.runObjectLaterAndWait(() -> { BufferedImage bufferedImage = (BufferedImage) image; - int imageWidth = bufferedImage.getWidth(); - int imageHeight = bufferedImage.getHeight(); - Image swtImage = new Image(null, imageWidth, imageHeight); - final ImageData swtImageData = swtImage.getImageData(); try { - ImageProducer source = image.getSource(); - source.startProduction(new AwtToSwtImageConverter(bufferedImage, swtImageData)); - return ImageDescriptor.createFromImageDataProvider(zoom -> zoom == 100 ? swtImageData : null); + return ImageDescriptor.createFromImageDataProvider(new AwtImageDataProvider(bufferedImage, zoom)); } catch (Throwable e) { // fallback to ImageIO. return ImageUtils.convertToSWT(image); - } finally { - swtImage.dispose(); } }); } @@ -527,6 +537,22 @@ public static ImageDescriptor convertImage_AWT_to_SWT(final java.awt.Image image // Utils // //////////////////////////////////////////////////////////////////////////// + + /** + * Returns the native zoom level of the given component. + */ + public static double getDisplayZoom(final Component component) { + // A lof of hardcoded paint operations were added with + // 6d8cdc275b5b94a03a5a613783396f0e6db89f97, which very + // likely don't work when taking HighDPI into account. + // Without a Mac to test whether this issue is even + // relevant anymore, the check is disabled instead. + if (EnvironmentUtils.IS_MAC) { + return 1.0; + } + return component.getGraphicsConfiguration().getDefaultTransform().getScaleX(); + } + /** * Runs given runnable in dispatch thread. */ @@ -543,6 +569,53 @@ public static void runInDispatchThread(final Runnable runnable) throws Exception // AWT -> SWT converter // //////////////////////////////////////////////////////////////////////////// + + /** + * This class handles the conversion from an AWT {@link BufferedImage} to an SWT + * {@link ImageData} at any given {@code zoom} level. The original image is + * based on the current display zoom and scaled artificially to the requested + * zoom level. + */ + private static class AwtImageDataProvider implements ImageDataProvider { + /** + * Cache the image data for the individual zoom levels. + */ + private final Map imageDataAtZoom = new HashMap<>(); + private final BufferedImage image; + private final int imageZoom; + + public AwtImageDataProvider(BufferedImage image, double imageZoom) { + this.image = image; + // Convert AWT zoom to SWT zoom + this.imageZoom = (int) (imageZoom * 100); + } + + @Override + public ImageData getImageData(int zoom) { + return imageDataAtZoom.computeIfAbsent(zoom, this::createImageData); + } + + private ImageData createImageData(int zoom) { + BufferedImage imageToUse = image; + if (zoom != imageZoom) { + int scaledImageWidth = image.getWidth() * zoom / imageZoom; + int scaledImageHeight = image.getHeight() * zoom / imageZoom; + BufferedImage scaledImageToUse = new BufferedImage(scaledImageWidth, scaledImageHeight, imageToUse.getType()); + Graphics2D graphics = scaledImageToUse.createGraphics(); + graphics.drawImage(imageToUse, 0, 0, scaledImageWidth, scaledImageHeight, null); + graphics.dispose(); + imageToUse = scaledImageToUse; + } + final ImageProducer source = imageToUse.getSource(); + final int imageWidth = imageToUse.getWidth(); + final int imageHeight = imageToUse.getHeight(); + final PaletteData swtPaletteData = new PaletteData(0xFF0000, 0x00FF00, 0x0000FF); + final ImageData swtImageData = new ImageData(imageWidth, imageHeight, 24, swtPaletteData); + source.startProduction(new AwtToSwtImageConverter(imageToUse, swtImageData)); + return swtImageData; + } + } + /** * @author mitin_aa */ diff --git a/org.eclipse.wb.swing/src/org/eclipse/wb/internal/swing/utils/SwingScreenshotMaker.java b/org.eclipse.wb.swing/src/org/eclipse/wb/internal/swing/utils/SwingScreenshotMaker.java index cc0f14cbe..ea56fadae 100644 --- a/org.eclipse.wb.swing/src/org/eclipse/wb/internal/swing/utils/SwingScreenshotMaker.java +++ b/org.eclipse.wb.swing/src/org/eclipse/wb/internal/swing/utils/SwingScreenshotMaker.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2011, 2024 Google, Inc. + * Copyright (c) 2011, 2025 Google, Inc. 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 @@ -28,6 +28,7 @@ import java.awt.Component; import java.awt.Container; import java.awt.Frame; +import java.awt.Graphics2D; import java.awt.Point; import java.awt.Window; import java.awt.image.BufferedImage; @@ -122,8 +123,9 @@ public void endVisit(ObjectInfo objectInfo) throws Exception { */ public void makeShots() throws Exception { SwingImageUtils.checkForDialog(m_component); - final int componentWidth = Math.max(1, m_component.getWidth()); - final int componentHeight = Math.max(1, m_component.getHeight()); + final double componentZoom = SwingImageUtils.getDisplayZoom(m_component); + final int componentWidth = Math.max(1, (int) (m_component.getWidth() * componentZoom)); + final int componentHeight = Math.max(1, (int) (m_component.getHeight() * componentZoom)); m_oldComponentLocation = m_component.getLocation(); boolean isResizable = false; // When the size of the frame exceeds the size of the screen, then the frame @@ -142,10 +144,15 @@ public void makeShots() throws Exception { final BufferedImage windowImage; // print component and its children { - int windowWidth = Math.max(1, m_window.getWidth()); - int windowHeight = Math.max(1, m_window.getHeight()); - windowImage = new BufferedImage(windowWidth, windowHeight, BufferedImage.TYPE_INT_RGB); - m_window.printAll(windowImage.getGraphics()); + double windowZoom = SwingImageUtils.getDisplayZoom(m_window); + int windowWidth = Math.max(1, (int) (m_window.getWidth() * windowZoom)); + int windowHeight = Math.max(1, (int) (m_window.getHeight() * windowZoom)); + windowImage = m_window.getGraphicsConfiguration() // + .createCompatibleImage(windowWidth, windowHeight); + Graphics2D graphics = windowImage.createGraphics(); + graphics.scale(windowZoom, windowZoom); + m_window.printAll(graphics); + graphics.dispose(); } // prepare component image if (m_component == m_window) { @@ -166,9 +173,10 @@ public void makeShots() throws Exception { componentLocation = new Point(p_component.x - p_window.x, p_component.y - p_window.y); } // copy part of window image - BufferedImage componentImage = - new BufferedImage(componentWidth, componentHeight, BufferedImage.TYPE_INT_RGB); - componentImage.getGraphics().drawImage( + BufferedImage componentImage = m_component.getGraphicsConfiguration() // + .createCompatibleImage(componentWidth, componentHeight); + Graphics2D graphics = componentImage.createGraphics(); + graphics.drawImage( windowImage, 0, 0, @@ -179,6 +187,7 @@ public void makeShots() throws Exception { componentLocation.x + componentWidth, componentLocation.y + componentHeight, m_window); + graphics.dispose(); image = componentImage; } // store image for top-level first @@ -190,7 +199,8 @@ public void makeShots() throws Exception { for (Component keyComponent : Collections.unmodifiableMap(m_componentImages).keySet()) { java.awt.Image image2 = m_componentImages.get(keyComponent); if (image2 != null) { - convertedImages.put(keyComponent, SwingImageUtils.convertImage_AWT_to_SWT(image2).createImage()); + double zoom = SwingImageUtils.getDisplayZoom(keyComponent); + convertedImages.put(keyComponent, SwingImageUtils.convertImage_AWT_to_SWT(image2, zoom).createImage()); } } // draw decorations on OS X