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 5f7820042fcc229a9ad8bd9c84398bc96cdbfe10 Author: Eirik Bakke <eba...@ultorg.com> AuthorDate: Thu Jun 6 16:18:49 2019 -0400 Add more tests for ImageUtilities, and fix some broken ones. --- .../src/org/openide/util/ImageUtilities.java | 28 ++- .../src/org/openide/util/ImageUtilitiesTest.java | 234 +++++++++++++++++++-- 2 files changed, 243 insertions(+), 19 deletions(-) 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 b133ad9..5171bd9 100644 --- a/platform/openide.util.ui/src/org/openide/util/ImageUtilities.java +++ b/platform/openide.util.ui/src/org/openide/util/ImageUtilities.java @@ -20,6 +20,7 @@ package org.openide.util; import java.awt.Component; +import java.awt.EventQueue; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; @@ -54,6 +55,7 @@ import javax.imageio.stream.ImageInputStream; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JLabel; +import javax.swing.SwingUtilities; import javax.swing.UIManager; /** @@ -113,12 +115,18 @@ public final class ImageUtilities { private static volatile Component dummyIconComponent; static { - Mutex.EVENT.writeAccess(new Runnable() { - @Override - public void run() { - dummyIconComponent = new JLabel(); - } - }); + /* Could have used Mutex.EVENT.writeAccess here, but it doesn't seem to be available during + testing. */ + if (EventQueue.isDispatchThread()) { + dummyIconComponent = new JLabel(); + } else { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + dummyIconComponent = new JLabel(); + } + }); + } } private ImageUtilities() { @@ -973,7 +981,9 @@ public final class ImageUtilities { } public ToolTipImage(Icon delegateIcon, String toolTipText, int imageType) { - super(delegateIcon.getIconWidth(), delegateIcon.getIconHeight(), imageType); + // BufferedImage must have width/height > 0. + super(Math.max(1, delegateIcon.getIconWidth()), + Math.max(1, delegateIcon.getIconHeight()), imageType); this.delegateIcon = delegateIcon; this.toolTipText = toolTipText; this.url = null; @@ -1044,6 +1054,10 @@ public final class ImageUtilities { @Override public Object getProperty(String name, ImageObserver observer) { if ("url".equals(name)) { // NOI18N + /* In some cases it might strictly be more appropriate to return + Image.UndefinedProperty rather than null (see Javadoc spec for this method), but + retain the existing behavior and use null instead here. That way there won't be a + ClassCastException if someone tries to cast to URL. */ if (url != null) { return url; } else if (!(delegateIcon instanceof ImageIcon)) { diff --git a/platform/openide.util.ui/test/unit/src/org/openide/util/ImageUtilitiesTest.java b/platform/openide.util.ui/test/unit/src/org/openide/util/ImageUtilitiesTest.java index 087a49d..de8f537 100644 --- a/platform/openide.util.ui/test/unit/src/org/openide/util/ImageUtilitiesTest.java +++ b/platform/openide.util.ui/test/unit/src/org/openide/util/ImageUtilitiesTest.java @@ -19,13 +19,22 @@ package org.openide.util; import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; import java.awt.Image; +import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; +import java.lang.reflect.InvocationTargetException; import java.net.URL; +import java.util.function.Function; import javax.swing.Icon; import javax.swing.ImageIcon; +import javax.swing.SwingUtilities; import javax.swing.UIManager; import junit.framework.*; +import static org.openide.util.ImageUtilities.icon2Image; +import static org.openide.util.ImageUtilities.image2Icon; /** * @@ -234,23 +243,224 @@ public class ImageUtilitiesTest extends TestCase { } public void testConversions() { - Image image = ImageUtilities.loadImage("org/openide/util/testimage.png", false); - Icon icon = ImageUtilities.loadImageIcon("org/openide/util/testimage.png", false); + /* Note: these are rather implementation-oriented tests. Implementation changes in + ImageUtilities (addition or removal of caches etc.) might require this test to be + updated, even when the API is unchanged. */ - assertNotNull("Should not be null", icon); - assertNotNull("Should not be null", image); + for (boolean useExternalImage : new boolean[] {false, true}) { + Object urlProperty; + Image image; + ImageIcon imageIcon; + if (useExternalImage) { + // Test an Image and ImageIcon instance that did not originate from ImageUtilities. + urlProperty = null; + image = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB); + imageIcon = new ImageIcon(image); + } else { + urlProperty = getClass().getResource("/org/openide/util/testimage.png"); + image = ImageUtilities.loadImage("org/openide/util/testimage.png", false); + imageIcon = ImageUtilities.loadImageIcon("org/openide/util/testimage.png", false); + assertNotNull("URL found", urlProperty); + assertNotNull("Should not be null", imageIcon); + assertNotNull("Should not be null", image); + assertEquals("URL obtained", urlProperty, image.getProperty("url", null)); + } + + /* These instances will no longer be the same; loadImage will now return a ToolTipImage, + while loadImageIcon will return a IconImageIcon. (Implementation detail only; could be + changed in the future.) */ + assertNotSame("Expected different instances in current implementation", + imageIcon, + image2Icon(image)); + + /* An Icon/Image loaded via loadImage can be freely passed through icon2Image/image2Icon + without a new instance being created. */ + assertEquals("Should be same instance", + image, + icon2Image(imageIcon)); + + assertEquals("Should be same instance", + icon2Image(imageIcon), + icon2Image(imageIcon)); + + if (!useExternalImage) { + /* In the useExternalImage case, the original instance will be converted to a + ToolTipImage, so we won't have the same instance here. */ + assertEquals("Should be same instance", + image, + icon2Image(image2Icon(image))); + } + + /* Again, loadImageIcon has to wrap its result in an IconImageIcon, so the instances below + won't be the same. (Implementation detail only; could be changed in the future.) */ + assertNotSame("Expected different instances in current implementation", + imageIcon, + image2Icon(icon2Image(imageIcon))); + + Icon iconFromImage2Icon = image2Icon(image); + assertEquals("Should be same instance", + iconFromImage2Icon, + image2Icon(icon2Image(iconFromImage2Icon))); + + Icon iconFromImageIconRoundabout = image2Icon(icon2Image(imageIcon)); + assertEquals("Should be same instance", + iconFromImageIconRoundabout, + image2Icon(icon2Image(iconFromImageIconRoundabout))); + + // An actual BufferedImage will return Image.UndefinedProperty rather than null. + assertEquals("Url is still there", + urlProperty != null ? urlProperty : Image.UndefinedProperty, + icon2Image(imageIcon).getProperty("url", null)); + assertEquals("Url is still there", urlProperty, icon2Image(iconFromImage2Icon).getProperty("url", null)); + assertEquals("Url is still there", urlProperty, icon2Image(iconFromImageIconRoundabout).getProperty("url", null)); + } + } + + public void testConvertNullImageIcon() { + // A corner case which occured during development. + ImageIcon imageIcon = new ImageIcon(); + Image image = ImageUtilities.icon2Image(imageIcon); + if (image == null) { + throw new AssertionError( + "icon2Image should work even with an ImageIcon for which the image is null"); + } + // Just ensure there are no NPEs. + image.getProperty("url", null); + } + + /** + * @param expectZeroedXY if the implementation is expected to paint the icon at (0,0) rather + * than on the supplied coordinates (e.g. because the paint happens on a cached + * backbuffer rather than directly on the original Graphics2D). + */ + private static void testLosslessCustomIconTransformations( + Function<CustomIcon, Icon> transformation, boolean expectZeroedXY) + { + /* Make sure that a custom Icon implementation that is passed through + the transformation is stilled called on to paint after the icon/image conversions. */ + final CustomIcon origIcon = new CustomIcon(); + Icon iconAgain = transformation.apply(origIcon); + origIcon.clear(); + BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = img.createGraphics(); + final int TEST_X = 45; + final int TEST_Y = 23; + // Also check that we don't unnecessarily crash if the Component parameter is null. + iconAgain.paintIcon(null, g, TEST_X, TEST_Y); + g.dispose(); + assertTrue(origIcon.wasPaintCalled); + assertNull(origIcon.lastObservedComponent); + if (expectZeroedXY) { + assertEquals(0.0, origIcon.lastSeenX); + assertEquals(0.0, origIcon.lastSeenY); + } else { + assertEquals((double) TEST_X, origIcon.lastSeenX); + assertEquals((double) TEST_Y, origIcon.lastSeenY); + } + } + + public void testCustomIconImplementationRetained() { + testLosslessCustomIconTransformations(new Function<CustomIcon, Icon>() { + @Override + public Icon apply(CustomIcon origIcon) { + Image image = ImageUtilities.icon2Image(origIcon); + assertTrue(origIcon.wasPaintCalled); + origIcon.clear(); + Icon iconAgain = ImageUtilities.image2Icon(image); + assertFalse(origIcon.wasPaintCalled); + return iconAgain; + } + }, false); + testLosslessCustomIconTransformations(new Function<CustomIcon, Icon>() { + @Override + public Icon apply(CustomIcon origIcon) { + return ImageUtilities.createDisabledIcon(origIcon); + } + }, true); + testLosslessCustomIconTransformations(new Function<CustomIcon, Icon>() { + @Override + public Icon apply(CustomIcon origIcon) { + BufferedImage otherImage = + new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB); + // The offset is for otherImage, so should not cause the test to fail. + return image2Icon( + ImageUtilities.mergeImages(icon2Image(origIcon), otherImage, 4, 12)); + } + }, true); + testLosslessCustomIconTransformations(new Function<CustomIcon, Icon>() { + @Override + public Icon apply(CustomIcon origIcon) { + BufferedImage otherImage = + new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB); + return image2Icon( + ImageUtilities.mergeImages(otherImage, icon2Image(origIcon), 0, 0)); + } + }, true); + } + + public void testCustomIconImplementationGetsValidComponent() + throws InterruptedException, InvocationTargetException + { + final CustomIcon origIcon = new CustomIcon(); + /* The dummy Component parameter that is fed to Icon.paintIcon is only guaranteed to be + initialized once the Event Dispatch Thread has had a chance to run. */ + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + assertFalse(origIcon.wasPaintCalled); + assertNull(origIcon.lastObservedComponent); + ImageUtilities.icon2Image(origIcon); + assertTrue(origIcon.wasPaintCalled); + assertNotNull(origIcon.lastObservedComponent); + } + }); + /* Once ImageUtilities has initialized its dummy Component, paintIcon should keep receiving + a valid instance no matter which thread it is running on. */ + final CustomIcon origIcon2 = new CustomIcon(); + ImageUtilities.icon2Image(origIcon2); + assertTrue(origIcon2.wasPaintCalled); + assertNotNull(origIcon2.lastObservedComponent); + } + + private static final class CustomIcon implements Icon { + public volatile Component lastObservedComponent; + public volatile boolean wasPaintCalled; + public volatile double lastSeenX; + public volatile double lastSeenY; + + private void clear() { + lastObservedComponent = null; + wasPaintCalled = false; + lastSeenX = Double.NaN; + lastSeenY = Double.NaN; + } - URL u = getClass().getResource("/org/openide/util/testimage.png"); - assertNotNull("URL found", u); - assertEquals("URL obtained", u, image.getProperty("url", null)); + public CustomIcon() { + clear(); + } - Icon icon2 = ImageUtilities.image2Icon(image); - Image image2 = ImageUtilities.icon2Image(icon); + @Override + public void paintIcon(Component c, Graphics g0, int x, int y) { + this.lastObservedComponent = c; + wasPaintCalled = true; + Graphics2D g = (Graphics2D) g0; + g.translate(x, y); + AffineTransform tx = g.getTransform(); + final int txType = tx.getType(); + assertTrue(txType == 0 || txType == AffineTransform.TYPE_TRANSLATION); + lastSeenX = tx.getTranslateX(); + lastSeenY = tx.getTranslateY(); + } - assertEquals("Should be same instance", icon, icon2); - assertEquals("Should be same instance", image, image2); + @Override + public int getIconWidth() { + return 16; + } - assertEquals("Url is still there", u, image2.getProperty("url", null)); + @Override + public int getIconHeight() { + return 16; + } } public void testLoadingNonExisting() { --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@netbeans.apache.org For additional commands, e-mail: commits-h...@netbeans.apache.org For further information about the NetBeans mailing lists, visit: https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists