Ticket #18694: hidpi-cursors-overlay.patch

File hidpi-cursors-overlay.patch, 26.2 KB (added by johsin18, 4 years ago)

complete patchset

  • src/org/openstreetmap/josm/tools/GuiSizesHelper.java

    diff --git a/src/org/openstreetmap/josm/tools/GuiSizesHelper.java b/src/org/openstreetmap/josm/tools/GuiSizesHelper.java
    index 2e4d38691..29f54e6d3 100644
    a b public final class GuiSizesHelper {  
    5656        return getScreenDPI() / 96f;
    5757    }
    5858
     59    /**
     60     * Sets coefficient of monitor pixel density.
     61     * @param pixelDensity coefficient of monitor pixel density to be set.
     62     */
     63    public static void setPixelDensity(float pixelDensity) {
     64        screenDPI = pixelDensity * 96f;
     65    }
     66
    5967    /**
    6068     * Check if a high DPI resolution is used
    6169     * @return <code>true</code> for HIDPI screens
  • src/org/openstreetmap/josm/tools/HiDPISupport.java

    diff --git a/src/org/openstreetmap/josm/tools/HiDPISupport.java b/src/org/openstreetmap/josm/tools/HiDPISupport.java
    index 169eea566..f54e3ff2e 100644
    a b public final class HiDPISupport {  
    3232    private static final Class<? extends Image> baseMultiResolutionImageClass;
    3333    private static final Constructor<? extends Image> baseMultiResolutionImageConstructor;
    3434    private static final Method resolutionVariantsMethod;
     35    private static final Method resolutionVariantMethod;
    3536
    3637    static {
    3738        baseMultiResolutionImageClass = initBaseMultiResolutionImageClass();
    3839        baseMultiResolutionImageConstructor = initBaseMultiResolutionImageConstructor();
    3940        resolutionVariantsMethod = initResolutionVariantsMethod();
     41        resolutionVariantMethod = initResolutionVariantMethod();
    4042    }
    4143
    4244    private HiDPISupport() {
    public final class HiDPISupport {  
    5658    public static Image getMultiResolutionImage(Image base, ImageResource ir) {
    5759        double uiScale = getHiDPIScale();
    5860        if (uiScale != 1.0 && baseMultiResolutionImageConstructor != null) {
    59             ImageIcon zoomed = ir.getImageIcon(new Dimension(
     61            ImageIcon zoomed = ir.getImageIconAlreadyScaled(new Dimension(
    6062                    (int) Math.round(base.getWidth(null) * uiScale),
    61                     (int) Math.round(base.getHeight(null) * uiScale)), false);
     63                    (int) Math.round(base.getHeight(null) * uiScale)), false, true);
    6264            Image mrImg = getMultiResolutionImage(Arrays.asList(base, zoomed.getImage()));
    6365            if (mrImg != null) return mrImg;
    6466        }
    public final class HiDPISupport {  
    137139        return Collections.singletonList(img);
    138140    }
    139141
     142    /**
     143     * Wrapper for method <code>java.awt.image.MultiResolutionImage#getResolutionVariant(double destImageWidth, double destImageHeight)</code>.
     144     * <p>
     145     * Will return the argument, in case it is not a multi-resolution image.
     146     * @param img the image
     147     * @return if <code>img</code> is a <code>java.awt.image.BaseMultiResolutionImage</code>,
     148     * then the result of the method <code>#getResolutionVariant(destImageWidth, destImageHeight)</code>,
     149     * otherwise the image itself
     150     */
     151    public static Image getResolutionVariant(Image img, double destImageWidth, double destImageHeight) {
     152        if (baseMultiResolutionImageClass == null || resolutionVariantsMethod == null) {
     153            return img;
     154        }
     155        if (baseMultiResolutionImageClass.isInstance(img)) {
     156            try {
     157                return (Image) resolutionVariantMethod.invoke(img, destImageWidth, destImageHeight);
     158            } catch (IllegalAccessException | InvocationTargetException ex) {
     159                Logging.error("Unexpected error while calling method: " + ex);
     160            }
     161        }
     162        return img;
     163    }
     164
    140165    /**
    141166     * Detect the GUI scale for HiDPI mode.
    142167     * <p>
    public final class HiDPISupport {  
    144169     * only take the default screen device into account.
    145170     * @return the GUI scale for HiDPI mode, a value of 1.0 means standard mode.
    146171     */
    147     private static double getHiDPIScale() {
     172    static double getHiDPIScale() {
    148173        if (GraphicsEnvironment.isHeadless())
    149174            return 1.0;
    150175        GraphicsConfiguration gc = GraphicsEnvironment
    public final class HiDPISupport {  
    236261            return null;
    237262        }
    238263    }
     264
     265    private static Method initResolutionVariantMethod() {
     266        try {
     267            return baseMultiResolutionImageClass != null
     268                    ? baseMultiResolutionImageClass.getMethod("getResolutionVariant", Double.TYPE, Double.TYPE)
     269                    : null;
     270        } catch (NoSuchMethodException ex) {
     271            Logging.error("Cannot find expected method: " + ex);
     272            return null;
     273        }
     274    }
    239275}
  • src/org/openstreetmap/josm/tools/ImageOverlay.java

    diff --git a/src/org/openstreetmap/josm/tools/ImageOverlay.java b/src/org/openstreetmap/josm/tools/ImageOverlay.java
    index 0bb61c68c..fd28eb770 100644
    a b  
    22package org.openstreetmap.josm.tools;
    33
    44import java.awt.Dimension;
     5import java.awt.Image;
    56import java.awt.image.BufferedImage;
     7import java.util.List;
    68
    79import javax.swing.ImageIcon;
    810
    public class ImageOverlay implements ImageProcessor {  
    6668     */
    6769    @Override
    6870    public BufferedImage process(BufferedImage ground) {
     71        return process(ground, false);
     72    }
     73
     74    /**
     75     * Handle overlay. The image passed as argument is modified!
     76     *
     77     * @param ground the base image for the overlay (gets modified!)
     78     * @param highResolution whether the high resolution variant should be used to overlay
     79     * @return the modified image (same as argument)
     80     */
     81    BufferedImage process(BufferedImage ground, boolean highResolution) {
    6982        /* get base dimensions for calculation */
    7083        int w = ground.getWidth();
    7184        int h = ground.getHeight();
    public class ImageOverlay implements ImageProcessor {  
    7790        if (offsetTop > 0 && offsetBottom > 0) {
    7891            height = (int) (h*(offsetBottom-offsetTop));
    7992        }
    80         ImageIcon overlay;
    8193        image = new ImageProvider(image).setMaxSize(new Dimension(width, height));
    82         overlay = image.get();
     94        ImageIcon overlay = image.get();
     95        if (highResolution) {
     96            List<Image> resolutionVariants = HiDPISupport.getResolutionVariants(overlay.getImage());
     97            if (resolutionVariants.size() >= 2) {
     98                overlay = new ImageIcon(resolutionVariants.get(1));
     99            }
     100        }
    83101        int x, y;
    84102        if (width == -1 && offsetLeft < 0) {
    85103            x = (int) (w*offsetRight) - overlay.getIconWidth();
  • src/org/openstreetmap/josm/tools/ImageProvider.java

    diff --git a/src/org/openstreetmap/josm/tools/ImageProvider.java b/src/org/openstreetmap/josm/tools/ImageProvider.java
    index 8b1882cb0..53ed29d45 100644
    a b public class ImageProvider {  
    13171317     * @return cursor with a given file name, optionally decorated with an overlay image
    13181318     */
    13191319    public static Cursor getCursor(String name, String overlay) {
    1320         ImageIcon img = get("cursor", name);
    1321         if (overlay != null) {
    1322             img = new ImageProvider("cursor", name).setMaxSize(ImageSizes.CURSOR)
    1323                 .addOverlay(new ImageOverlay(new ImageProvider("cursor/modifier/" + overlay)
    1324                     .setMaxSize(ImageSizes.CURSOROVERLAY))).get();
    1325         }
    13261320        if (GraphicsEnvironment.isHeadless()) {
    13271321            Logging.debug("Cursors are not available in headless mode. Returning null for ''{0}''", name);
    13281322            return null;
    13291323        }
    1330         return Toolkit.getDefaultToolkit().createCustomCursor(img.getImage(),
    1331                 "crosshair".equals(name) ? new Point(10, 10) : new Point(3, 2), "Cursor");
     1324
     1325        Point hotSpot = new Point();
     1326        Image image = getCursorImage(name, overlay, hotSpot);
     1327
     1328        return Toolkit.getDefaultToolkit().createCustomCursor(image, hotSpot, name);
     1329    }
     1330
     1331    /**
     1332     * Load a cursor image with a given file name, optionally decorated with an overlay image
     1333     *
     1334     * @param name the cursor image filename in "cursor" directory
     1335     * @param overlay optional overlay image
     1336     * @param hotSpot will be set to the properly scaled hotspot of the cursor
     1337     * @return cursor with a given file name, optionally decorated with an overlay image
     1338     */
     1339    static Image getCursorImage(String name, String overlay, /* out */ Point hotSpot) {
     1340        ImageProvider imageProvider = new ImageProvider("cursor", name);
     1341        if (overlay != null) {
     1342            imageProvider
     1343                .setMaxSize(ImageSizes.CURSOR)
     1344                .addOverlay(new ImageOverlay(new ImageProvider("cursor/modifier/" + overlay)
     1345                                                .setMaxSize(ImageSizes.CURSOROVERLAY)));
     1346        }
     1347        hotSpot.setLocation("crosshair".equals(name) ? new Point(10, 10) : new Point(3, 2));
     1348        ImageIcon imageIcon = imageProvider.get();
     1349        Image image = imageIcon.getImage();
     1350        int width = image.getWidth(null);
     1351        int height = image.getHeight(null);
     1352
     1353        // AWT will resize the cursor to bestCursorSize internally anyway, but miss to scale the hotspot as well
     1354        // (bug JDK-8238734).  So let's do this ourselves, and also scale the hotspot accordingly.
     1355        Dimension bestCursorSize = Toolkit.getDefaultToolkit().getBestCursorSize(width, height);
     1356        if (bestCursorSize.width != 0 && bestCursorSize.height != 0) {
     1357            // In principle, we could pass the MultiResolutionImage itself to AWT, but due to bug JDK-8240568,
     1358            // this results in bad alpha blending and thus jaggy edges.  So let's select the best variant ourselves.
     1359            image = HiDPISupport.getResolutionVariant(image, bestCursorSize.width, bestCursorSize.height);
     1360            if (bestCursorSize.width != image.getWidth(null) || bestCursorSize.height != image.getHeight(null)) {
     1361                image = image.getScaledInstance(bestCursorSize.width, bestCursorSize.height, Image.SCALE_DEFAULT);
     1362            }
     1363
     1364            hotSpot.x = hotSpot.x * bestCursorSize.width / width;
     1365            hotSpot.y = hotSpot.y * bestCursorSize.height / height;
     1366        }
     1367
     1368        return image;
    13321369    }
    13331370
    13341371    /** 90 degrees in radians units */
  • src/org/openstreetmap/josm/tools/ImageResource.java

    diff --git a/src/org/openstreetmap/josm/tools/ImageResource.java b/src/org/openstreetmap/josm/tools/ImageResource.java
    index 831b7c71b..4c00e0b79 100644
    a b public class ImageResource {  
    153153     * @return ImageIcon object for the image of this resource, scaled according to dim
    154154     * @since 12722
    155155     */
    156     public ImageIcon getImageIcon(Dimension dim, boolean multiResolution) {
     156    ImageIcon getImageIcon(Dimension dim, boolean multiResolution) {
     157        return getImageIconAlreadyScaled(GuiSizesHelper.getDimensionDpiAdjusted(dim), multiResolution, false);
     158    }
     159
     160    /**
     161     * Get an ImageIcon object for the image of this resource. A potential UI scaling is assumed
     162     * to be already taken care of, so dim is already scaled accordingly.
     163     * @param  dim The requested dimensions. Use (-1,-1) for the original size and (width, -1)
     164     *         to set the width, but otherwise scale the image proportionally.
     165     * @param  multiResolution If true, return a multi-resolution image
     166     * (java.awt.image.MultiResolutionImage in Java 9), otherwise a plain {@link BufferedImage}.
     167     * When running Java 8, this flag has no effect and a plain image will be returned in any case.
     168     * @param highResolution whether the high resolution variant should be used for overlays
     169     * @return ImageIcon object for the image of this resource, scaled according to dim
     170     */
     171    ImageIcon getImageIconAlreadyScaled(Dimension dim, boolean multiResolution, boolean highResolution) {
    157172        CheckParameterUtil.ensureThat((dim.width > 0 || dim.width == -1) && (dim.height > 0 || dim.height == -1),
    158173                () -> dim + " is invalid");
     174
    159175        BufferedImage img = imgCache.get(dim);
    160176        if (img == null) {
    161177            if (svg != null) {
    162                 Dimension realDim = GuiSizesHelper.getDimensionDpiAdjusted(dim);
    163                 img = ImageProvider.createImageFromSvg(svg, realDim);
     178                img = ImageProvider.createImageFromSvg(svg, dim);
    164179                if (img == null) {
    165180                    return null;
    166181                }
    167182            } else {
    168183                if (baseImage == null) throw new AssertionError();
    169184
    170                 int realWidth = GuiSizesHelper.getSizeDpiAdjusted(dim.width);
    171                 int realHeight = GuiSizesHelper.getSizeDpiAdjusted(dim.height);
    172185                ImageIcon icon = new ImageIcon(baseImage);
    173                 if (realWidth == -1 && realHeight == -1) {
    174                     realWidth = GuiSizesHelper.getSizeDpiAdjusted(icon.getIconWidth());
    175                     realHeight = GuiSizesHelper.getSizeDpiAdjusted(icon.getIconHeight());
    176                 } else if (realWidth == -1) {
    177                     realWidth = Math.max(1, icon.getIconWidth() * realHeight / icon.getIconHeight());
    178                 } else if (realHeight == -1) {
    179                     realHeight = Math.max(1, icon.getIconHeight() * realWidth / icon.getIconWidth());
     186                if (dim.width == -1 && dim.height == -1) {
     187                    dim.width = GuiSizesHelper.getSizeDpiAdjusted(icon.getIconWidth());
     188                    dim.height = GuiSizesHelper.getSizeDpiAdjusted(icon.getIconHeight());
     189                } else if (dim.width == -1) {
     190                    dim.width = Math.max(1, icon.getIconWidth() * dim.height / icon.getIconHeight());
     191                } else if (dim.height == -1) {
     192                    dim.height = Math.max(1, icon.getIconHeight() * dim.width / icon.getIconWidth());
    180193                }
    181                 Image i = icon.getImage().getScaledInstance(realWidth, realHeight, Image.SCALE_SMOOTH);
    182                 img = new BufferedImage(realWidth, realHeight, BufferedImage.TYPE_INT_ARGB);
     194                Image i = icon.getImage().getScaledInstance(dim.width, dim.height, Image.SCALE_SMOOTH);
     195                img = new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_ARGB);
    183196                img.getGraphics().drawImage(i, 0, 0, null);
    184197            }
    185198            if (overlayInfo != null) {
    186199                for (ImageOverlay o : overlayInfo) {
    187                     o.process(img);
     200                    o.process(img, highResolution);
    188201                }
    189202            }
    190203            if (isDisabled) {
  • test/unit/org/openstreetmap/josm/TestUtils.java

    diff --git a/test/unit/org/openstreetmap/josm/TestUtils.java b/test/unit/org/openstreetmap/josm/TestUtils.java
    index 3f8f100a4..61700f688 100644
    a b import org.openstreetmap.josm.gui.progress.ProgressTaskId;  
    5050import org.openstreetmap.josm.gui.util.GuiHelper;
    5151import org.openstreetmap.josm.io.Compression;
    5252import org.openstreetmap.josm.testutils.FakeGraphics;
     53import org.openstreetmap.josm.testutils.mockers.HeadlessToolkitMocker;
    5354import org.openstreetmap.josm.testutils.mockers.JOptionPaneSimpleMocker;
    5455import org.openstreetmap.josm.testutils.mockers.WindowMocker;
    5556import org.openstreetmap.josm.tools.JosmRuntimeException;
    public final class TestUtils {  
    464465            // Inspired by https://issues.apache.org/jira/browse/SOLR-11606
    465466            new WindowMocker();
    466467            new JOptionPaneSimpleMocker();
     468            new HeadlessToolkitMocker();
    467469        } catch (UnsupportedOperationException e) {
    468470            Assume.assumeNoException(e);
    469471        } finally {
  • new file test/unit/org/openstreetmap/josm/testutils/mockers/HeadlessToolkitMocker.java

    diff --git a/test/unit/org/openstreetmap/josm/testutils/mockers/HeadlessToolkitMocker.java b/test/unit/org/openstreetmap/josm/testutils/mockers/HeadlessToolkitMocker.java
    new file mode 100644
    index 000000000..59fec5e4e
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.testutils.mockers;
     3
     4import mockit.Mock;
     5import mockit.MockUp;
     6import sun.awt.HeadlessToolkit;
     7
     8import java.awt.Dimension;
     9import java.awt.HeadlessException;
     10import java.awt.Toolkit;
     11
     12/**
     13 * MockUp for a {@link Toolkit} that allows to mock getBestCursorSize
     14 */
     15public class HeadlessToolkitMocker extends MockUp<HeadlessToolkit> {
     16    public static Dimension bestCursorSize = new Dimension(32, 32);
     17
     18    @Mock
     19    public Dimension getBestCursorSize(int preferredWidth, int preferredHeight) throws HeadlessException {
     20        return bestCursorSize;
     21    }
     22}
  • test/unit/org/openstreetmap/josm/tools/ImageProviderTest.java

    diff --git a/test/unit/org/openstreetmap/josm/tools/ImageProviderTest.java b/test/unit/org/openstreetmap/josm/tools/ImageProviderTest.java
    index b96320301..20f056aee 100644
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.tools;
    33
     4import static java.awt.image.BufferedImage.TYPE_INT_ARGB;
    45import static org.junit.Assert.assertEquals;
    56import static org.junit.Assert.assertFalse;
    67import static org.junit.Assert.assertNotNull;
    78import static org.junit.Assert.assertNull;
     9import static org.junit.Assert.assertTrue;
    810
     11import java.awt.Color;
    912import java.awt.Dimension;
     13import java.awt.GraphicsEnvironment;
     14import java.awt.GridLayout;
     15import java.awt.Graphics;
     16import java.awt.Image;
     17import java.awt.Point;
     18import java.awt.Toolkit;
    1019import java.awt.Transparency;
     20import java.awt.event.MouseEvent;
     21import java.awt.event.MouseListener;
    1122import java.awt.image.BufferedImage;
    1223import java.io.File;
    1324import java.io.IOException;
    1425import java.util.EnumSet;
     26import java.util.List;
    1527import java.util.logging.Handler;
    1628import java.util.logging.LogRecord;
    1729import java.util.logging.Logger;
    1830
    1931import javax.swing.ImageIcon;
     32import javax.swing.JFrame;
     33import javax.swing.JPanel;
    2034
     35import mockit.integration.TestRunnerDecorator;
     36import org.junit.Before;
    2137import org.junit.BeforeClass;
     38import org.junit.Ignore;
    2239import org.junit.Rule;
    2340import org.junit.Test;
    2441import org.openstreetmap.josm.JOSMFixture;
    import org.openstreetmap.josm.TestUtils;  
    2643import org.openstreetmap.josm.data.osm.Node;
    2744import org.openstreetmap.josm.data.osm.OsmUtils;
    2845import org.openstreetmap.josm.testutils.JOSMTestRules;
     46import org.openstreetmap.josm.testutils.mockers.HeadlessToolkitMocker;
    2947import org.openstreetmap.josm.tools.ImageProvider.GetPaddedOptions;
    3048
    3149import com.kitfox.svg.SVGConst;
    public class ImageProviderTest {  
    7189        JOSMFixture.createUnitTestFixture().init();
    7290    }
    7391
     92    @Before
     93    public void resetPixelDensity() {
     94        GuiSizesHelper.setPixelDensity(1.0f);
     95    }
     96
    7497    /**
    7598     * Non-regression test for ticket <a href="https://josm.openstreetmap.de/ticket/9984">#9984</a>
    7699     * @throws IOException if an error occurs during reading
    public class ImageProviderTest {  
    148171        assertNotNull(ImageProvider.getPadded(OsmUtils.createPrimitive("way waterway=stream"), iconSize, noDefault));
    149172        assertNotNull(ImageProvider.getPadded(OsmUtils.createPrimitive("relation type=route route=railway"), iconSize, noDefault));
    150173    }
     174
     175    /**
     176     * Test getting a bounded icon given some UI scaling configured.
     177     */
     178    @Test
     179    public void testGetImageIconBounded() {
     180        int scale = 2;
     181        GuiSizesHelper.setPixelDensity(scale);
     182
     183        ImageProvider imageProvider = new ImageProvider("open").setOptional(true);
     184        ImageResource resource = imageProvider.getResource();
     185        Dimension iconDimension = ImageProvider.ImageSizes.SMALLICON.getImageDimension();
     186        ImageIcon icon = resource.getImageIconBounded(iconDimension);
     187        Image image = icon.getImage();
     188        List<Image> resolutionVariants = HiDPISupport.getResolutionVariants(image);
     189        if (resolutionVariants.size() > 1) {
     190            assertEquals(2, resolutionVariants.size());
     191            int expectedVirtualWidth = ImageProvider.ImageSizes.SMALLICON.getVirtualWidth();
     192            assertEquals(expectedVirtualWidth * scale, resolutionVariants.get(0).getWidth(null));
     193            assertEquals((int) Math.round(expectedVirtualWidth * scale * HiDPISupport.getHiDPIScale()),
     194                         resolutionVariants.get(1).getWidth(null));
     195        }
     196    }
     197
     198    public static final int ORIGINAL_CURSOR_SIZE = 32;
     199
     200    /**
     201     * Test getting an image for a crosshair cursor.
     202     */
     203    @Test
     204    public void testGetCursorImageForCrosshair() {
     205        Dimension[] bestCursorSizes = installHeadlessToolkitMock();
     206
     207        for (Dimension cursorSize: bestCursorSizes) {
     208            if (GraphicsEnvironment.isHeadless()) {
     209                HeadlessToolkitMocker.bestCursorSize = cursorSize;
     210            }
     211            Point hotSpot = new Point();
     212            Image image = ImageProvider.getCursorImage("crosshair", null, hotSpot);
     213            assertCursorDimensionsCorrect(new Point.Double(10.0, 10.0), image, hotSpot);
     214        }
     215
     216        TestRunnerDecorator.cleanUpAllMocks();
     217    }
     218
     219    /**
     220     * Test getting an image for a custom cursor with overlay.
     221     */
     222    @Test
     223    public void testGetCursorImageWithOverlay() {
     224        Dimension[] bestCursorSizes = installHeadlessToolkitMock();
     225
     226        for (Dimension cursorSize: bestCursorSizes) {
     227            if (GraphicsEnvironment.isHeadless()) {
     228                HeadlessToolkitMocker.bestCursorSize = cursorSize;
     229            }
     230            Point hotSpot = new Point();
     231            Image image = ImageProvider.getCursorImage("normal", "selection", hotSpot);
     232            assertCursorDimensionsCorrect(new Point.Double(3.0, 2.0), image, hotSpot);
     233            BufferedImage bufferedImage = new BufferedImage(image.getWidth(null), image.getWidth(null), TYPE_INT_ARGB);
     234            bufferedImage.getGraphics().drawImage(image, 0, 0, null);
     235
     236            // check that the square of 1/4 size right lower to the center has some non-emtpy pixels
     237            boolean nonEmptyPixelExistsRightLowerToCenter = false;
     238            for (int x = image.getWidth(null) / 2; x < image.getWidth(null) * 3 / 4; ++x) {
     239                for (int y = image.getHeight(null) / 2; y < image.getWidth(null) * 3 / 4; ++y) {
     240                    if (bufferedImage.getRGB(x, y) != 0)
     241                        nonEmptyPixelExistsRightLowerToCenter = true;
     242                }
     243            }
     244            assertTrue(nonEmptyPixelExistsRightLowerToCenter);
     245        }
     246        TestRunnerDecorator.cleanUpAllMocks();
     247    }
     248
     249    private Dimension[] installHeadlessToolkitMock() {
     250        if (GraphicsEnvironment.isHeadless()) {
     251            TestUtils.assumeWorkingJMockit();
     252            new HeadlessToolkitMocker();
     253            // in headless mode, let's test many reasonable values
     254            return new Dimension[]{new Dimension(32, 32), new Dimension(48, 48),
     255                                   new Dimension(64, 64), new Dimension(96, 96)};
     256        } else {
     257            // in normal mode, test the settings of the system run on
     258            return new Dimension[]{new Dimension(-1, -1)}; // values disregarded, but trigger a single test run
     259        }
     260    }
     261
     262    private void assertCursorDimensionsCorrect(Point.Double originalHotspot, Image image, Point hotSpot) {
     263        Dimension bestCursorSize = Toolkit.getDefaultToolkit().getBestCursorSize(ORIGINAL_CURSOR_SIZE, ORIGINAL_CURSOR_SIZE);
     264        Image bestCursorImage = HiDPISupport.getResolutionVariant(image, bestCursorSize.width, bestCursorSize.height);
     265        int bestCursorImageWidth = bestCursorImage.getWidth(null);
     266        assertEquals((int) Math.round(bestCursorSize.getWidth()), bestCursorImageWidth);
     267        int bestCursorImageHeight = bestCursorImage.getHeight(null);
     268        assertEquals((int) Math.round(bestCursorSize.getHeight()), bestCursorImageHeight);
     269        assertEquals(originalHotspot.x / ORIGINAL_CURSOR_SIZE * bestCursorImageWidth, hotSpot.x, 1 /* at worst one pixel off */);
     270        assertEquals(originalHotspot.y / ORIGINAL_CURSOR_SIZE * bestCursorImageHeight, hotSpot.y, 1 /* at worst one pixel off */);
     271    }
     272
     273
     274    /**
     275     * Test getting a cursor
     276     */
     277    @Ignore("manual execution only, as the look of the cursor cannot be checked automatedly")
     278    @Test
     279    public void testGetCursor() throws InterruptedException {
     280        JFrame frame = new JFrame();
     281        frame.setSize(500, 500);
     282        frame.setLayout(new GridLayout(2, 2));
     283        JPanel leftUpperPanel = new JPanel(), rightUpperPanel = new JPanel(), leftLowerPanel = new JPanel(), rightLowerPanel = new JPanel();
     284        leftUpperPanel.setBackground(Color.DARK_GRAY);
     285        rightUpperPanel.setBackground(Color.DARK_GRAY);
     286        leftLowerPanel.setBackground(Color.DARK_GRAY);
     287        rightLowerPanel.setBackground(Color.DARK_GRAY);
     288        frame.add(leftUpperPanel);
     289        frame.add(rightUpperPanel);
     290        frame.add(leftLowerPanel);
     291        frame.add(rightLowerPanel);
     292
     293        leftUpperPanel.setCursor(ImageProvider.getCursor("normal", "select_add")); // contains diagonal sensitive to alpha blending
     294        rightUpperPanel.setCursor(ImageProvider.getCursor("crosshair", "joinway")); // combination of overlay and hotspot not top left
     295        leftLowerPanel.setCursor(ImageProvider.getCursor("hand", "parallel_remove")); // reasonably nice bitmap cursor
     296        rightLowerPanel.setCursor(ImageProvider.getCursor("rotate", null)); // ugly bitmap cursor, cannot do much here
     297
     298        frame.setVisible(true);
     299
     300        // hover over the four quadrant to observe different cursors
     301
     302        // draw red dot at hotspot when clicking
     303        frame.addMouseListener(new MouseListener() {
     304            @Override
     305            public void mouseClicked(MouseEvent e) {
     306                Graphics graphics = frame.getGraphics();
     307                graphics.setColor(Color.RED);
     308                graphics.drawRect(e.getX(), e.getY(), 1, 1);
     309            }
     310
     311            @Override
     312            public void mousePressed(MouseEvent e) { }
     313
     314            @Override
     315            public void mouseReleased(MouseEvent e) { }
     316
     317            @Override
     318            public void mouseEntered(MouseEvent e) { }
     319
     320            @Override
     321            public void mouseExited(MouseEvent e) { }
     322        });
     323        Thread.sleep(9000); // test would time out after 10s
     324    }
    151325}