Search code examples
javamacosjava-11retina-displayjava-2d

Swing Java2D on Java 11 and a modern iMac


A while back, I had written and had working a Zoom to Mouse Swing panel that handled highlighting, pan, mouse zoom, selection, etc. It was very nice.

I went to play with it some more today, and it wasn't working. I was flummoxed. I knew I had a working example -- somewhere. But scouring my drive, none of my experiments worked. I started trying to make it work again.

Eventually I found out the problem. It's some combination of the JDK 11 and my new iMac. Last time I worked on this, was on my older Mac (where I may or may not have been using JDK 11, I don't recall).

This is what my app looks like with JDK 11 enter image description here

This is what it looks like with JDK 8 (this is what it should look like) enter image description here

If you play with the code, you'll see that there is apparently some kind of (2X?) scaling going on under JDK 11, but not JDK 8. I don't know if it's trying to compensate with for the large display on my machine, or what's going on.

But you try zooming or panning under JDK 11, and see it doesn't stay centered on the mouse, and how the tracking highlight is wrong, etc.

How can I make this work properly under JDK 11? Is it a Mac only thing? Or a Mac with "Drive in theater size monitors" thing?

Below is the code:

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;

public class TestPanel {

    public static void main(String[] args) throws Exception {
        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                createAndShowGUI();
            }
        });
    }

    public static void createAndShowGUI() {
        JFrame f = new JFrame("Test Zoom");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setLayout(new BorderLayout());
        TestPanelZoom p = new TestPanelZoom();
        f.add(p, BorderLayout.CENTER);
        f.setPreferredSize(new Dimension(400, 500));
        f.pack();
        f.setVisible(true);
    }

    private static class TestPanelZoom extends JPanel {

        boolean hilighted = false;
        private int hiliteX = -1;
        private int hiliteY = -1;
        boolean selected = false;
        private int selectX = -1;
        private int selectY = -1;

        private AffineTransform at = new AffineTransform();

        public TestPanelZoom() {
            setBackground(Color.WHITE);
            setForeground(Color.BLACK);
            addMouseAdapter();
            at.setToIdentity();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(400, 500);
        }

        protected void paintComponent(Graphics g) {
            super.paintComponent(g);

            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setTransform(at);
            paintPanel(g2d);
            g2d.dispose();
        }

        private void paintPanel(Graphics2D g2) {
            g2.setRenderingHint(
                    RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
            g2.setRenderingHint(
                    RenderingHints.KEY_TEXT_ANTIALIASING,
                    RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
            Color c = g2.getColor();
            g2.setColor(Color.WHITE);
            g2.fill(getBounds());
            g2.setColor(c);

            for (int i = 0; i < 400; i += 50) {
                Line2D line = new Line2D.Double(i, 0, i, 350);
                g2.draw(line);
                line = new Line2D.Double(0, i, 350, i);
                g2.draw(line);
            }
            if (hilighted) {
                Rectangle2D rect = new Rectangle2D.Double(hiliteX * 50 + 5, hiliteY * 50 + 5, 40, 40);
                g2.setColor(Color.GREEN);
                g2.fill(rect);
            }
            if (selected) {
                Rectangle2D rect = new Rectangle2D.Double(selectX * 50 + 10, selectY * 50 + 10, 30, 30);
                g2.setColor(Color.RED);
                g2.fill(rect);
            }
        }

        private void addMouseAdapter() {
            MouseAdapter ma = new MouseAdapter() {
                int lx, ly;

                @Override
                public void mouseWheelMoved(MouseWheelEvent e) {
                    AffineTransform t = new AffineTransform();
                    double delta = 1 + 0.05f * e.getPreciseWheelRotation();
                    int x = e.getX();
                    int y = e.getY();
                    t.translate(x, y);
                    t.scale(delta, delta);
                    t.translate(-x, -y);
                    t.concatenate(at);
                    at = t;
                    revalidate();
                    repaint();
                }

                @Override
                public void mousePressed(MouseEvent e) {
                    lx = e.getX();
                    ly = e.getY();
                }

                @Override
                public void mouseDragged(MouseEvent e) {
                    update(e);
                }

                @Override
                public void mouseReleased(MouseEvent e) {
                    update(e);
                }

                @Override
                public void mouseClicked(MouseEvent e) {
                    Point2D srcPt = getSrcPoint(e);
                    selected = true;
                    selectX = (int) (srcPt.getX() / 50);
                    selectY = (int) (srcPt.getY() / 50);
                    revalidate();
                    repaint();
                }

                @Override
                public void mouseMoved(MouseEvent e) {
                    Point2D srcPt = getSrcPoint(e);
                    hilighted = true;
                    hiliteX = (int) (srcPt.getX() / 50);
                    hiliteY = (int) (srcPt.getY() / 50);
                    revalidate();
                    repaint();
                }

                public void update(MouseEvent e) {
                    Point2D srcPt = new Point2D.Double(e.getX(), e.getY());
                    Point2D lastPt = new Point2D.Double(lx, ly);
                    try {
                        at.inverseTransform(srcPt, srcPt);
                        at.inverseTransform(lastPt, lastPt);
                    } catch (NoninvertibleTransformException noninvertibleTransformException) {
                        throw new RuntimeException(noninvertibleTransformException);
                    }
                    double dx = srcPt.getX() - lastPt.getX();
                    double dy = srcPt.getY() - lastPt.getY();
                    at.translate(dx, dy);
                    lx = e.getX();
                    ly = e.getY();
                    revalidate();
                    repaint();
                }

                public Point2D getSrcPoint(MouseEvent e) {
                    Point2D srcPt = new Point2D.Double(e.getX(), e.getY());
                    try {
                        at.inverseTransform(srcPt, srcPt);
                    } catch (NoninvertibleTransformException ex) {
                        throw new RuntimeException(ex);
                    }
                    return srcPt;
                }

            };
            addMouseListener(ma);
            addMouseMotionListener(ma);
            addMouseWheelListener(ma);
        }

    }
}

EDIT:

After some more poking around, for whatever reason under JDK 11, the default transform for the Graphics2D is scaled by 2.

In this code, if you print out the default transform, on JDK 11 (in my environment) you get:

AffineTransform[[2.0, 0.0, 0.0], [0.0, 2.0, 0.0]]

On JDK 8, you get the Identity.

AffineTransform[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);

            Graphics2D g2d = (Graphics2D) g.create();
            System.out.println(g2d.getTransform());
            g2d.setTransform(at);
            paintPanel(g2d);
            g2d.dispose();
        }

I initially force it to identiy by using my own original AffineTransform. On JDK 8, this is a NOP, JDK 11, not so much.

I tried this, where I set the default transform (at) to the "default" of the graphics context the first time (using its fundamental world view vs forcing my own). That's make the grid the proper size, but it's messing with the cursor locations.

        // I remove setting `at` up above, and setting it to identity also.
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);

            Graphics2D g2d = (Graphics2D) g.create();
            if (at == null) {
                at = g2d.getTransform();
            }
            g2d.setTransform(at);
            paintPanel(g2d);
            g2d.dispose();
        }

So that's not a solution.

Maybe there's a way to make it work by leveraging the default transform for the context, but I haven't figure that out yet. I have to assume that the default transform is fluid, not just JDK to JDK, but machine to machine. It may even change in a multi-monitor situation.

EDIT:

Continuing on.

So, the window created and displayed on the screen is 2x the size I ask for in Java. This I check by creating a screenshot of the Window.

In JDK 8, the mechanic that makes a 100 pixel line in Java become a 200 pixel line on the display is not made apparent to my code.

In JDK 11, one can see them trying to move that up by using the AffineTransform directly.

Similarly, in JDK 8, the mouse coordinates are scaled back in to the Java coordinate system. So if you put a 400x400 box on the screen and move the mouse from one corner to another, even though the box is 800x800 in actual pixels, the mouse just ranges from 0-400.

The problem in JDK 8 is that because of the Transform, the screen coordinates no long match the model coordinates. While you drew a 400x400 box, what was drawn on the screen is 400x400 time 2 (due to the transform). But the mouse coordinates are not in screen coordinates. The raw mouse is in the 0-400 range rather than 0-800 range. So you can no longer use the graphics transform to convert the mouse coordinates back in to model coordinates. There's shenanigans and skullduggery happening hidden under the covers. Simply, the transform isn't canonical. That's why my mousing gets all messed up.

Also, I ran the sample on another Mac with a lower resolution display, and there's no problem at all. The default transform under JDK 11 is identity.


Solution

  • I didn't see any difference between JDK 8 and 11, but the issue is that the Graphics2D context is already transformed to match the screen configuration, so where you do g2d.setTransform(at) you need to do g2d.transform(at). Then everything will work as you expect. (I ran with JDK 17 on OS X 11.4)

    So paintComponent should be (I've also added some logging):

    protected void paintComponent(Graphics g) {
                super.paintComponent(g);
    
                Graphics2D g2d = (Graphics2D) g.create();
                AffineTransform t = g2d.getTransform();
                System.out.println("Base G2D:" + t);
                System.out.println("Ours:" + at);
                g2d.transform(at);
                System.out.println("Final:" + g2d.getTransform());
    
                paintPanel(g2d);
                g2d.dispose();
            }
    

    I have tested this on OpenJDK Runtime Environment 18.9 (build 11.0.2+9) and Java(TM) SE Runtime Environment (build 1.8.0_201-b09).

    I've now also tested with 11.0.11:

    $ rm *.class
    $ java -version
    openjdk version "11.0.11" 2021-04-20
    OpenJDK Runtime Environment AdoptOpenJDK-11.0.11+9 (build 11.0.11+9)
    OpenJDK 64-Bit Server VM AdoptOpenJDK-11.0.11+9 (build 11.0.11+9, mixed mode)
    $ javac TestPanel.java
    $ java TestPanel
    Base G2D:AffineTransform[[2.0, 0.0, 0.0], [0.0, 2.0, 0.0]]
    Ours:AffineTransform[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
    Final:AffineTransform[[2.0, 0.0, 0.0], [0.0, 2.0, 0.0]]