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 d305cd16e9a4ab88a71ced7e7b8622d26542b2f1 Author: Eirik Bakke <[email protected]> AuthorDate: Wed May 29 16:13:30 2019 -0400 [NETBEANS-1586] Make ImageUtilities.createDisabledIcon work with HiDPI icons Introduced a FilteredIcon class which extends from a new CachedHiDPIIcon class. The CachedHiDPIIcon class will be used again in the future; it was originally designed for an SVG-based Icon implementation, which will be contributed separately in the future. It is generic enough to work with any kind of custom Icon implementation that needs caching. Also make color filtering of icons for dark themes work with HiDPI icons, using the same FilteredIcon class. Also improved a null handling case in ImageUtilities.loadImage. Passing a null resource used to be valid before (yielding a null return value), but caused a NPE on dark LAFs. Since it worked in the common non-dark case, it should arguably work in the dark case as well. This might fix NETBEANS-2401, or it might just lead to an NPE elsewhere in that case. --- .../src/org/openide/util/CachedHiDPIIcon.java | 211 +++++++++++++++++++++ .../src/org/openide/util/FilteredIcon.java | 79 ++++++++ .../src/org/openide/util/ImageUtilities.java | 62 ++---- 3 files changed, 308 insertions(+), 44 deletions(-) diff --git a/platform/openide.util.ui/src/org/openide/util/CachedHiDPIIcon.java b/platform/openide.util.ui/src/org/openide/util/CachedHiDPIIcon.java new file mode 100644 index 0000000..ecce663 --- /dev/null +++ b/platform/openide.util.ui/src/org/openide/util/CachedHiDPIIcon.java @@ -0,0 +1,211 @@ +/* + * 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.openide.util; + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.Image; +import java.awt.geom.AffineTransform; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import javax.swing.Icon; +import javax.swing.ImageIcon; + +/** + * Abstract base class for {@link javax.swing.Icon} implementations that need to cache scaled bitmap + * representations for HiDPI displays. Bitmaps for multiple HiDPI scaling factors can be cached at + * the same time, e.g. for multi-monitor setups. + */ +abstract class CachedHiDPIIcon extends ImageIcon { + /** + * The maximum size of the cache, as a multiple of the size of the icon at 100% scaling. For + * example, storing three images at 100%, 150%, and 200% scaling, respectively, yields a total + * cache size of 1.0^2 + 1.5^2 + 2^2 = 7.2. + */ + private static final double MAX_CACHE_SIZE = 10.0; + private final int width; + private final int height; + /** + * Cache map with least-recently-used iteration order. + */ + private final Map<CachedImageKey, Image> cache = + new LinkedHashMap<CachedImageKey, Image>(16, 0.75f, true); + /** + * Total size of the images currently in the cache, in the same units as + * {@link #MAX_CACHE_SIZE}. + */ + private double cacheSize = 0.0; + + /** + * Constructor to be used by subclasses. + */ + protected CachedHiDPIIcon(int width, int height) { + if (width < 0) { + throw new IllegalArgumentException(); + } + if (height < 0) { + throw new IllegalArgumentException(); + } + this.width = width; + this.height = height; + } + + private synchronized Image getScaledImageCached(Component c, CachedImageKey key) { + Image ret = cache.get(key); + if (ret != null) { + return ret; + } + final double scale = key.getScale(); + final int deviceWidth = (int) Math.ceil(getIconWidth() * scale); + final int deviceHeight = (int) Math.ceil(getIconHeight() * scale); + final Image img = + createImage(c, key.getGraphicsConfiguration(), deviceWidth, deviceHeight, scale); + final double imgSize = key.getSize(); + if (imgSize <= MAX_CACHE_SIZE) { + /* Evict least-recently-used images from the cache until we have space for the latest + image. */ + final Iterator<CachedImageKey> iter = cache.keySet().iterator(); + while (cacheSize + imgSize > MAX_CACHE_SIZE && iter.hasNext()) { + CachedImageKey removeKey = iter.next(); + iter.remove(); + cacheSize -= removeKey.getSize(); + } + cache.put(key, img); + cacheSize += imgSize; + } + return img; + } + + @Override + public final void paintIcon(Component c, Graphics g0, int x, int y) { + final Graphics2D g = (Graphics2D) g0; + CachedImageKey key = CachedImageKey.create(g); + final AffineTransform oldTransform = g.getTransform(); + try { + g.translate(x, y); + Image scaledImage = getScaledImageCached(c, key); + /* Scale the image down to its logical dimensions, then draw it at the device pixel + boundary. In VectorIcon, we tried to be a lot more conservative, taking great care not + to draw on any device pixels that were only partially bounded by the icon (due to + non-integral scaling factors, e.g. 150%). That was probably overkill; it's a lot easier + to assume that partially bounded pixels are OK to draw on, since all icon bitmaps of a + given scaling factor then end up being the same number of device pixels wide and tall. + And we need consistent dimensions to be able keep cached images in any case. For these + reasons, round the X and Y translations (which denote the position in device pixels) + _down_ here.*/ + AffineTransform tx2 = g.getTransform(); + g.setTransform(new AffineTransform(1, 0, 0, 1, + (int) tx2.getTranslateX(), + (int) tx2.getTranslateY())); + g.drawImage(scaledImage, 0, 0, null); + } finally { + g.setTransform(oldTransform); + } + } + + @Override + public final int getIconWidth() { + return width; + } + + @Override + public final int getIconHeight() { + return height; + } + + /** + * Create a scaled image containing the graphics of this icon. The result may be cached. + * + * @param c the component that was passed to {@link Icon#paintIcon(Component,Graphics,int,int)}. + * The cache will <em>not</em> be invalidated if {@code c} or its state changes, so + * subclasses should avoid depending on it if possible. This parameter exists mainly to + * ensure compatibility with existing Icon implementations that may be used as delegates. + * Future implementations might also elect to simply pass a dummy Component instance + * here. + * @param graphicsConfiguration the configuration of the surface on which the image will be + * painted + * @param deviceWidth the required width of the image, with scaling already applied + * @param deviceHeight the required height of the image, with scaling already applied + * @param scale the HiDPI scaling factor detected in {@code graphicsConfiguration} + */ + protected abstract Image createImage(Component c, GraphicsConfiguration graphicsConfiguration, + int deviceWidth, int deviceHeight, double scale); + + private static final class CachedImageKey { + private final GraphicsConfiguration gconf; + private final double scale; + + public CachedImageKey(GraphicsConfiguration gconf, double scale) { + Parameters.notNull("gconf", gconf); + if (scale <= 0.0) { + throw new IllegalArgumentException(); + } + this.gconf = gconf; + this.scale = scale; + } + + public static CachedImageKey create(Graphics2D g) { + final AffineTransform tx = g.getTransform(); + final int txType = tx.getType(); + final double scale; + if (txType == AffineTransform.TYPE_UNIFORM_SCALE || + txType == (AffineTransform.TYPE_UNIFORM_SCALE | AffineTransform.TYPE_TRANSLATION)) + { + scale = tx.getScaleX(); + } else { + scale = 1.0; + } + return new CachedImageKey(g.getDeviceConfiguration(), scale); + } + + public double getScale() { + return scale; + } + + /** + * Get the size of this image as a multiple of the original image's size at 100% scaling. + */ + public double getSize() { + return Math.pow(getScale(), 2.0); + } + + public GraphicsConfiguration getGraphicsConfiguration() { + return gconf; + } + + @Override + public int hashCode() { + return Objects.hash(gconf, scale); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof CachedImageKey)) { + return false; + } + final CachedImageKey other = (CachedImageKey) obj; + return this.gconf.equals(other.gconf) && + this.scale == other.scale; + } + } +} diff --git a/platform/openide.util.ui/src/org/openide/util/FilteredIcon.java b/platform/openide.util.ui/src/org/openide/util/FilteredIcon.java new file mode 100644 index 0000000..94499eb --- /dev/null +++ b/platform/openide.util.ui/src/org/openide/util/FilteredIcon.java @@ -0,0 +1,79 @@ +/* + * 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.openide.util; + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.Transparency; +import java.awt.image.BufferedImage; +import java.awt.image.FilteredImageSource; +import java.awt.image.RGBImageFilter; +import javax.swing.Icon; +import javax.swing.ImageIcon; + +/** + * A filtered variation of a provided delegate icon. Any kind of delegate implementation can be + * used. In particular, this class preserves the full fidelity of HiDPI icons, such as instances + * of {@link VectorIcon}, or {@link ImageIcon} instances delegating to a + * {@code java.awt.image.MultiResolutionImage} (available since Java 9 and above). + * + * <p>Note that state passed through the {code Component} parameter of the + * {@link Icon#paintIcon(Component,Graphics,int,int)} method will only be current as of the time the + * icon is initially entered into the cache. + */ +final class FilteredIcon extends CachedHiDPIIcon { + private final RGBImageFilter filter; + private final Icon delegate; + + private FilteredIcon(RGBImageFilter filter, Icon delegate) { + super(delegate.getIconWidth(), delegate.getIconHeight()); + Parameters.notNull("filter", filter); + Parameters.notNull("delegate", delegate); + this.filter = filter; + this.delegate = delegate; + } + + public static Icon create(RGBImageFilter filter, Icon delegate) { + return new FilteredIcon(filter, delegate); + } + + @Override + protected Image createImage( + Component c, GraphicsConfiguration graphicsConfiguration, + int deviceWidth, int deviceHeight, double scale) + { + final BufferedImage img = graphicsConfiguration.createCompatibleImage( + deviceWidth, deviceHeight, Transparency.TRANSLUCENT); + final Graphics2D imgG = img.createGraphics(); + try { + imgG.clip(new Rectangle(0, 0, img.getWidth(), img.getHeight())); + imgG.scale(scale, scale); + delegate.paintIcon(c, imgG, 0, 0); + } finally { + imgG.dispose(); + } + return Toolkit.getDefaultToolkit().createImage( + new FilteredImageSource(img.getSource(), filter)); + } +} diff --git a/platform/openide.util.ui/src/org/openide/util/ImageUtilities.java b/platform/openide.util.ui/src/org/openide/util/ImageUtilities.java index c90164e..4a46b6a 100644 --- a/platform/openide.util.ui/src/org/openide/util/ImageUtilities.java +++ b/platform/openide.util.ui/src/org/openide/util/ImageUtilities.java @@ -19,7 +19,6 @@ package org.openide.util; -import java.awt.Color; import java.awt.Component; import java.awt.Graphics; import java.awt.HeadlessException; @@ -31,8 +30,6 @@ import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.FilteredImageSource; import java.awt.image.ImageObserver; -import java.awt.image.ImageProducer; -import java.awt.image.IndexColorModel; import java.awt.image.RGBImageFilter; import java.awt.image.WritableRaster; import java.io.IOException; @@ -138,6 +135,10 @@ public final class ImageUtilities { * @return icon's Image or null if the icon cannot be loaded */ public static final Image loadImage(String resource, boolean localized) { + // Avoid a NPE that could previously occur in the isDarkLaF case only. See NETBEANS-2401. + if (resource == null) { + return null; + } Image image = null; if( isDarkLaF() ) { image = getIcon(addDarkSuffix(resource), localized); @@ -149,8 +150,7 @@ public final class ImageUtilities { // only non _dark images need filtering RGBImageFilter imageFilter = getImageIconFilter(); if (null != image && null != imageFilter) { - image = Toolkit.getDefaultToolkit() - .createImage(new FilteredImageSource(image.getSource(), imageFilter)); + image = createFilteredImage(imageFilter, image); } } return image; @@ -342,7 +342,10 @@ public final class ImageUtilities { */ public static Icon createDisabledIcon(Icon icon) { Parameters.notNull("icon", icon); - return new LazyDisabledIcon(icon2Image(icon)); + /* FilteredIcon's Javadoc mentions a caveat about the Component parameter that is passed to + Icon.paintIcon. It's not really a problem; previous implementations had the same + behavior. */ + return FilteredIcon.create(DisabledButtonFilter.INSTANCE, icon); } /** @@ -353,7 +356,14 @@ public final class ImageUtilities { */ public static Image createDisabledImage(Image image) { Parameters.notNull("image", image); - return LazyDisabledIcon.createDisabledImage(image); + return createFilteredImage(DisabledButtonFilter.INSTANCE, image); + } + + private static Image createFilteredImage(RGBImageFilter filter, Image image) { + Parameters.notNull("filter", filter); + Parameters.notNull("image", image); + return Toolkit.getDefaultToolkit().createImage( + new FilteredImageSource(image.getSource(), filter)); } /** @@ -880,44 +890,8 @@ public final class ImageUtilities { } } - private static class LazyDisabledIcon implements Icon { - - /** Shared instance of filter for disabled icons */ - private static final RGBImageFilter DISABLED_BUTTON_FILTER = new DisabledButtonFilter(); - private Image img; - private Icon disabledIcon; - - public LazyDisabledIcon(Image img) { - assert null != img; - this.img = img; - } - - public void paintIcon(Component c, Graphics g, int x, int y) { - getDisabledIcon().paintIcon(c, g, x, y); - } - - public int getIconWidth() { - return getDisabledIcon().getIconWidth(); - } - - public int getIconHeight() { - return getDisabledIcon().getIconHeight(); - } - - private synchronized Icon getDisabledIcon() { - if (null == disabledIcon) { - disabledIcon = new ImageIcon(createDisabledImage(img)); - } - return disabledIcon; - } - - static Image createDisabledImage(Image img) { - ImageProducer prod = new FilteredImageSource(img.getSource(), DISABLED_BUTTON_FILTER); - return Toolkit.getDefaultToolkit().createImage(prod); - } - } - private static class DisabledButtonFilter extends RGBImageFilter { + public static final RGBImageFilter INSTANCE = new DisabledButtonFilter(); DisabledButtonFilter() { canFilterIndexColorModel = true; --------------------------------------------------------------------- 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
