This is an automated email from the ASF dual-hosted git repository. ebakke pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/netbeans.git
commit 2b18f36ee90a3d2ecc8f0307bc839e3bc1772f55 Author: Eirik Bakke <[email protected]> AuthorDate: Sat May 11 23:56:59 2019 +0200 Support proper HiDPI rendering of splash screen and About box images. (The splash screen image was already added in high resolution in a previous commit.) --- .../netbeans/core/startup/ScaledBitmapIcon.java | 128 +++++++++++++++++++++ .../src/org/netbeans/core/startup/Splash.java | 84 ++++++++++---- .../netbeans/core/ui/ProductInformationPanel.java | 6 +- 3 files changed, 192 insertions(+), 26 deletions(-) diff --git a/platform/core.startup/src/org/netbeans/core/startup/ScaledBitmapIcon.java b/platform/core.startup/src/org/netbeans/core/startup/ScaledBitmapIcon.java new file mode 100644 index 0000000..d902d2e --- /dev/null +++ b/platform/core.startup/src/org/netbeans/core/startup/ScaledBitmapIcon.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.core.startup; + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.Image; +import java.awt.RenderingHints; +import java.awt.Transparency; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.swing.Icon; + +/* Package-private for now. At some later point it might be useful to expose the DPI-based caching +functionality in this class as a more general utility. */ +/** + * An Icon implementation that scales a source image to the specified dimensions. Can be used to + * produce sharp images on HiDPI displays, without relying on MultiResolutionImage, which exists + * only since JDK 9. This also sidesteps https://bugs.openjdk.java.net/browse/JDK-8212226 on + * Windows. For HiDPI displays, the source image's dimensions should be at least double those of the + * icon's logical dimensions. A double-resolution source image will automatically be scaled down + * to 1x, 1.5x, or other HiDPI scaling factors as needed. + */ +final class ScaledBitmapIcon implements Icon { + private final Map<Double,Image> cache = new ConcurrentHashMap<>(); + private final Image sourceImage; + private final int width; + private final int height; + + public ScaledBitmapIcon(Image sourceImage, int width, int height) { + if (sourceImage == null) + throw new NullPointerException(); + if (width <= 0) + throw new IllegalArgumentException(); + if (height <= 0) + throw new IllegalArgumentException(); + this.sourceImage = sourceImage; + this.width = width; + this.height = height; + } + + private Image getScaledImage(GraphicsConfiguration gc, double dpiScaling) { + Image ret = cache.get(dpiScaling); + if (ret != null) { + return ret; + } + final BufferedImage img = gc.createCompatibleImage( + (int) Math.ceil(getIconWidth() * dpiScaling), + (int) Math.ceil(getIconHeight() * dpiScaling), Transparency.TRANSLUCENT); + final double sourceWidth = sourceImage.getWidth(null); + final double sourceHeight = sourceImage.getHeight(null); + if (sourceWidth >= 1 && sourceHeight >= 1) { + final Graphics2D imgG = (Graphics2D) img.getGraphics(); + try { + imgG.setTransform(AffineTransform.getScaleInstance( + dpiScaling * getIconWidth() / sourceWidth, + dpiScaling * getIconHeight() / sourceHeight)); + imgG.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + imgG.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + imgG.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + imgG.drawImage(sourceImage, 0, 0, null); + } finally { + imgG.dispose(); + } + if (dpiScaling <= 3.0) + cache.put(dpiScaling, img); + } + return img; + } + + @Override + public void paintIcon(Component c, Graphics g0, int x, int y) { + final Graphics2D g = (Graphics2D) g0; + final AffineTransform oldTransform = g.getTransform(); + g.translate(x, y); + final AffineTransform tx = g.getTransform(); + + final double dpiScaling; + final int txType = tx.getType(); + if (txType == AffineTransform.TYPE_UNIFORM_SCALE || + txType == (AffineTransform.TYPE_UNIFORM_SCALE | AffineTransform.TYPE_TRANSLATION)) + { + dpiScaling = tx.getScaleX(); + } else { + dpiScaling = 1.0; + } + Image scaledImage = getScaledImage(g.getDeviceConfiguration(), dpiScaling); + if (dpiScaling != 1.0) { + // Scale the image down to its logical dimensions, then draw it at the device pixel boundary. + AffineTransform tx2 = g.getTransform(); + g.setTransform(new AffineTransform(1, 0, 0, 1, + (int) tx2.getTranslateX(), + (int) tx2.getTranslateY())); + } + g.drawImage(scaledImage, 0, 0, null); + g.setTransform(oldTransform); + } + + @Override + public int getIconWidth() { + return width; + } + + @Override + public int getIconHeight() { + return height; + } +} diff --git a/platform/core.startup/src/org/netbeans/core/startup/Splash.java b/platform/core.startup/src/org/netbeans/core/startup/Splash.java index 8a2e59f..feeb088 100644 --- a/platform/core.startup/src/org/netbeans/core/startup/Splash.java +++ b/platform/core.startup/src/org/netbeans/core/startup/Splash.java @@ -92,11 +92,30 @@ public final class Splash implements Stamps.Updater { private SplashPainter painter; private SplashComponent comp; private SplashScreen splashScreen; + /** + * Indicate if we should try to take advantage of java's "-splash" parameter, which allows + * the splash screen to be displayed at an earlier stage in the app startup sequence. See the + * original Buzilla RFE at https://netbeans.org/bugzilla/show_bug.cgi?id=60142 . This requires + * splash screen image(s) to be written to the cache directory the first time NetBeans starts, + * to be available during subsequent NetBeans startups. Despite + * https://bugs.openjdk.java.net/browse/JDK-8145173 and + * https://bugs.openjdk.java.net/browse/JDK-8151787 , as of OpenJDK 10.0.2 and OpenJDK 12.0.1 + * I have found no way to make this work properly with HiDPI screens on Windows. HiDPI filenames + * attempted include "[email protected]", "[email protected]", "splash.scale-200.png", and + * "splash.java-scale200.png". In all of these cases, the regular "splash.png" file is used + * instead of one of the 2x-scaled ones (for a system DPI scaling of 200%), and the splash + * screen becomes half the expected size. Thus, to we disable this feature for now, in favor of + * a slightly delayed splash screen that appears with the correct size and resolution on HiDPI + * screens. + * + * <p>See also https://issues.apache.org/jira/browse/NETBEANS-67 . + */ + private static final boolean USE_LAUNCHER_SPLASH = false; private Splash() { Stamps s = Stamps.getModulesJARs(); if (!CLIOptions.isNoSplash() && !GraphicsEnvironment.isHeadless()) { - if (!s.exists("splash.png")) { + if (USE_LAUNCHER_SPLASH && !s.exists("splash.png")) { s.scheduleSave(this, "splash.png", false); } try { @@ -209,17 +228,29 @@ public final class Splash implements Stamps.Updater { c.setBounds(Utilities.findCenterBounds(c.getSize())); } - /** Loads a splash image from its source + /** + * Loads a splash image from its source. For high-resolution rendering on HiDPI displays, the + * returned image should be converted to a HiDPI-aware Icon instance via + * {@link ImageUtilities#image2Icon} prior to painting. + * * @param about if true then about image is loaded, if false splash image is loaded */ public static Image loadContent(boolean about) { + return ImageUtilities.icon2Image(loadContentIcon(about)); + } + + private static Icon loadContentIcon(boolean about) { + Image ret = null; if (about) { - Image img = ImageUtilities.loadImage("org/netbeans/core/startup/about.png", true); - if (img != null) { - return img; - } + ret = ImageUtilities.loadImage("org/netbeans/core/startup/about.png", true); } - return ImageUtilities.loadImage("org/netbeans/core/startup/splash.gif", true); + if (ret == null) + ret = ImageUtilities.loadImage("org/netbeans/core/startup/splash.gif", true); + if (ret == null) + return null; + return new ScaledBitmapIcon(ret, + Integer.parseInt(NbBundle.getMessage(Splash.class, "SPLASH_WIDTH")), + Integer.parseInt(NbBundle.getMessage(Splash.class, "SPLASH_HEIGHT"))); } @Override @@ -256,7 +287,7 @@ public final class Splash implements Stamps.Updater { */ @Override public void paint(Graphics graphics) { - painter.graphics = graphics; + painter.graphics = (Graphics2D) graphics; painter.paint(); } @Override @@ -388,9 +419,9 @@ public final class Splash implements Stamps.Updater { private int maxSteps = 0; private int barStart = 0; private int barLength = 0; - private Image image; + private Icon image; private String text; - private Graphics graphics; + private Graphics2D graphics; private final JComponent comp; private final boolean about; @@ -399,7 +430,7 @@ public final class Splash implements Stamps.Updater { * param about true is this component will be used in about dialog */ public SplashPainter(Graphics graphics, JComponent comp, boolean about) { - this.graphics = graphics; + this.graphics = (Graphics2D) graphics; this.comp = comp; this.about = about; } @@ -440,7 +471,7 @@ public final class Splash implements Stamps.Updater { bar = new Rectangle(0, 0, 80, 10); } - image = loadContent(about); + image = loadContentIcon(about); if (comp != null) comp.setFont(statusBox.font); @@ -565,12 +596,14 @@ public final class Splash implements Stamps.Updater { int bl = bar.width * progress / maxSteps - barStart; if (bl > 1 || barStart % 2 == 0) { barLength = bl; - bar_inc = new Rectangle(bar.x + barStart, bar.y, barLength + 1, bar.height); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { init(); - repaint(bar_inc); + /* Don't try to be smart about which section of the bar to repaint. + There can be tricky rounding issues on HiDPI screens with non-integral + scaling factors (e.g. 150%). */ + repaint(bar); } }); } @@ -600,10 +633,9 @@ public final class Splash implements Stamps.Updater { } void paint() { - graphics.drawImage(image, 0, 0, null); + image.paintIcon(comp, graphics, 0, 0); // turn anti-aliasing on for the splash text - Graphics2D g2d = (Graphics2D) graphics; - g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); if (versionBox != null) { @@ -618,12 +650,18 @@ public final class Splash implements Stamps.Updater { if (!noBar && maxSteps > 0/* && barLength > 0*/) { graphics.setColor(color_bar); graphics.fillRect(bar.x, bar.y, barStart + barLength, bar.height); - graphics.setColor(color_corner); - graphics.drawLine(bar.x, bar.y, bar.x, bar.y + bar.height); - graphics.drawLine(bar.x + barStart + barLength, bar.y, bar.x + barStart + barLength, bar.y + bar.height); - graphics.setColor(color_edge); - graphics.drawLine(bar.x, bar.y + bar.height / 2, bar.x, bar.y + bar.height / 2); - graphics.drawLine(bar.x + barStart + barLength, bar.y + bar.height / 2, bar.x + barStart + barLength, bar.y + bar.height / 2); + /* To discourage visual artifacts on HiDPI displays, only paint the distinct + corner/edge colors if the branding actually calls for them. */ + if (!color_bar.equals(color_corner)) { + graphics.setColor(color_corner); + graphics.drawLine(bar.x, bar.y, bar.x, bar.y + bar.height); + graphics.drawLine(bar.x + barStart + barLength, bar.y, bar.x + barStart + barLength, bar.y + bar.height); + } + if (!color_bar.equals(color_edge)) { + graphics.setColor(color_edge); + graphics.drawLine(bar.x, bar.y + bar.height / 2, bar.x, bar.y + bar.height / 2); + graphics.drawLine(bar.x + barStart + barLength, bar.y + bar.height / 2, bar.x + barStart + barLength, bar.y + bar.height / 2); + } barStart += barLength; barLength = 0; } diff --git a/platform/o.n.core/src/org/netbeans/core/ui/ProductInformationPanel.java b/platform/o.n.core/src/org/netbeans/core/ui/ProductInformationPanel.java index ea4fa22..a5ad4d2 100644 --- a/platform/o.n.core/src/org/netbeans/core/ui/ProductInformationPanel.java +++ b/platform/o.n.core/src/org/netbeans/core/ui/ProductInformationPanel.java @@ -21,7 +21,6 @@ package org.netbeans.core.ui; import java.awt.Cursor; import java.awt.EventQueue; -import java.awt.Font; import java.awt.Window; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; @@ -36,7 +35,6 @@ import java.net.URL; import java.text.MessageFormat; import java.util.Locale; import javax.swing.Icon; -import javax.swing.ImageIcon; import javax.swing.JEditorPane; import javax.swing.JPanel; import javax.swing.SwingUtilities; @@ -51,6 +49,7 @@ import org.openide.awt.HtmlBrowser.URLDisplayer; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.modules.Places; +import org.openide.util.ImageUtilities; import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; @@ -121,7 +120,8 @@ public class ProductInformationPanel extends JPanel implements HyperlinkListener copyright.setBackground(getBackground()); copyright.putClientProperty( JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE); - about = new ImageIcon(org.netbeans.core.startup.Splash.loadContent(true)); + // Use image2Icon to preserve the underlying HiDPI-aware Icon instance. + about = ImageUtilities.image2Icon(org.netbeans.core.startup.Splash.loadContent(true)); imageLabel.setIcon(about); imageLabel.addMouseListener(new MouseAdapter() { --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected] For further information about the NetBeans mailing lists, visit: https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists
