Java 9 replacement for SwingUtilities3.setDelegateRepaintManager
Asked Answered
A

0

10

While trying to port swing code to be compliant with the Java module system I got stuck trying to replace SwingUtilities3.setDelegateRepaintManager.

I have a component, which when any of its children requests a repaint then I need to transform the region (in particular this is trying to port the code of org.pbjar.jxlayer.plaf.ext.TransformUI for anyone who knows of this). Currently this is done by setting the delegate repaint manager of the component and intercepting the calls to addDirtyRegion.

Now with Java 9 the method to do so isn't available anymore as public api. The original code provided an alternative method which was originally used for older versions of Java where SwingUtilities3.setDelegateRepaintManagerwasn't available, which simply replaced the global RepaintManager with a delegating implementation. It checks each call if the component is contained in the actual component which needs the transformation. This solution however throws away all internal data of the RepaintManager and results in heavy flickering while the frame is resized.

Here an abridged version of the code currently used:

SwingUtilities3.setDelegateRepaintManager(component, new TransformRepaintManger());

...

class TransformRepaintManager extends RepaintManager {

    @Override
    public void addDirtyRegion(JComponent c, int x, int y, int w, int h) {
        if (c.isShowing()) {
            Point point = c.getLocationOnScreen();
            SwingUtilities.convertPointFromScreen(point, c);
            Rectangle transformPortRegion = transform(new Rectangle(x + point.x, y + point.y, w, h), c);
            RepaintManager.currentManager(c)
                .addDirtyRegion(c,
                    transformPortRegion.x, transformPortRegion.y,
                    transformPortRegion.width, transformPortRegion.height);
        }
    }
}

and the alternative approach which causes flickering (DelegateRepaintManager simply takes the original RepaintManager and forwards all calls to it):

class TransformRPMFallBack extends DelegateRepaintManager {

    @Override
    public void addDirtyRegion(JComponent aComponent,int x, int y, int w, int h) {
        if (aComponent.isShowing()) {
            JComponent targetParent = findTargetParent(aComponent);
            if (targetParent != null) {
                Point point = aComponent.getLocationOnScreen();
                SwingUtilities.convertPointFromScreen(point, targetParent);
                Rectangle transformPortRegion = transform(new Rectangle(x + point.x, y + point.y, w, h));
                addDirtyRegion(targetParent,
                               transformPortRegion.x, transformPortRegion.y,
                               transformPortRegion.width, transformPortRegion.height);
                return;
            }
        }
        super.addDirtyRegion(aComponent, x, y, w, h);
    }
}

I know one option would be to simply add --add-exports java.desktop/com.sun.java.swing=<module name> to the startup parameters, but as this is intended for a library forcing everyone using it to do so isn't the best option in my opinion.

Update: Here is an example which demonstrates the two approaches above. It consists of a panel which is rotated 90 degree in the JLayer. Toggling the checkboxes paints the left (visually top) or right (visually bottom) part of the component in a different color. The different approaches can be changed by setting the static vairable in TransformLayerUI. One can observe the following behaviours:

  • SolutionApproach.FLICKERING: Clicking the checkboxes behaves as expected. Resizing the window results in flickering (for such a small example it isn't very bas but for larger applications it gets much worse) flickering solution
  • SolutionApproach.ILLEGAL: Same as SolutionApproach.FLICKERING but without the flickering. illegal solution
  • SolutionApproach.NONE: Clicking the checkboxes only repaints a quarter of the area that should change. This is the problem that needs to be solved. If the TestPanel (or any possible children) requests a repaint the correct area of the JLayer should be repainted. none solution
public class TransformTest {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("Transform Test");

            JPanel contentPanel = new JPanel(new BorderLayout());
            TestPanel testPanel = new TestPanel();
            JLayer<TestPanel> testLayer = new JLayer<>(testPanel, new TransformLayerUI());
            contentPanel.add(testLayer);
            frame.getContentPane().add(contentPanel, BorderLayout.CENTER);

            JCheckBox leftCheck = new JCheckBox("Left active");
            leftCheck.addActionListener(e -> testPanel.setLeftActive(leftCheck.isSelected()));
            JCheckBox rightCheck = new JCheckBox("Right active");
            rightCheck.addActionListener(e -> testPanel.setRightActive(rightCheck.isSelected()));
            JComponent buttonPanel = Box.createHorizontalBox();
            buttonPanel.add(leftCheck);
            buttonPanel.add(rightCheck);
            frame.getContentPane().add(buttonPanel, BorderLayout.SOUTH);

            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }

    @SuppressWarnings("unchecked")
    static class TransformLayerUI extends LayerUI<TestPanel> {

        enum SolutionApproach {
            ILLEGAL,
            FLICKERING,
            NONE
        }

        static SolutionApproach approach = SolutionApproach.ILLEGAL;

        @Override
        public void installUI(JComponent c) {
            super.installUI(c);
            switch (approach) {
                case ILLEGAL:
                    SwingUtilities3.setDelegateRepaintManager(((JLayer<? extends JComponent>) c).getView(),
                            new TransformRepaintManager());
                    break;
                case FLICKERING:
                    if (!(RepaintManager.currentManager(c) instanceof FallbackTransformRepaintManger)) {
                        RepaintManager.setCurrentManager(
                                new FallbackTransformRepaintManger(RepaintManager.currentManager(c)));
                    }
                    break;
                case NONE:
                    break;
            }
        }

        private AffineTransform calcTransform(Dimension size) {
            AffineTransform at = new AffineTransform();
            Point2D center = new Point2D.Double(size.getWidth() / 2f, size.getHeight() / 2f);
            at.translate(center.getX(), center.getY());
            at.quadrantRotate(1);
            at.translate(-center.getX(), -center.getY());
            return at;
        }

        private Rectangle transform(Dimension size, final Rectangle rect) {
            Area area = new Area(rect);
            area.transform(calcTransform(size));
            return area.getBounds();
        }

        @Override
        public void paint(Graphics g, JComponent c) {
            ((Graphics2D) g).transform(calcTransform(c.getSize()));
            super.paint(g, c);
        }

        @Override
        public void doLayout(JLayer<? extends TestPanel> l) {
            l.getView().setBounds(transform(l.getSize(), new Rectangle(l.getSize())));
        }

        @Override
        public Dimension getPreferredSize(JComponent c) {
            return transform(((JLayer<? extends JComponent>) c).getView().getPreferredSize());
        }

        @Override
        public Dimension getMaximumSize(JComponent c) {
            return transform(((JLayer<? extends JComponent>) c).getView().getMaximumSize());
        }

        @Override
        public Dimension getMinimumSize(JComponent c) {
            return transform(((JLayer<? extends JComponent>) c).getView().getMinimumSize());
        }

        private Dimension transform(final Dimension size) {
            Area area = new Area(new Rectangle2D.Double(0, 0, size.getWidth(), size.getHeight()));
            area.transform(calcTransform(size));
            Rectangle2D bounds = area.getBounds2D();
            size.setSize(bounds.getWidth(), bounds.getHeight());
            return size;
        }

        class TransformRepaintManager extends RepaintManager {
            @Override
            public void addInvalidComponent(JComponent invalidComponent) {
                Container layer = SwingUtilities.getAncestorOfClass(JLayer.class, invalidComponent);
                RepaintManager.currentManager(layer).addInvalidComponent((JComponent) layer);
            }

            @Override
            public void addDirtyRegion(JComponent comp, int x, int y, int w, int h) {
                if (comp.isShowing()) {
                    Container layer = SwingUtilities.getAncestorOfClass(JLayer.class, comp);
                    dispatchRepaint(comp, layer, TransformLayerUI.this, new Rectangle(x, y, w, h));
                }
            }
        }

        static void dispatchRepaint(Component comp, Component layer, TransformLayerUI ui, Rectangle rect) {
            Point point = comp.getLocationOnScreen();
            SwingUtilities.convertPointFromScreen(point, layer);
            Rectangle transformPortRegion =
                    ui.transform(layer.getSize(),
                            new Rectangle(rect.x + point.x, rect.y + point.y, rect.width, rect.height));
            RepaintManager.currentManager(layer).addDirtyRegion((JComponent) layer,
                    transformPortRegion.x, transformPortRegion.y,
                    transformPortRegion.width, transformPortRegion.height);
        }

        static class FallbackTransformRepaintManger extends DelegateRepaintManager {

            FallbackTransformRepaintManger(RepaintManager delegate) {
                super(delegate);
            }

            @Override
            public void addDirtyRegion(JComponent aComponent, int x, int y, int w, int h) {
                if (aComponent.isShowing()) {
                    JLayer<?> layer = (JLayer<?>) SwingUtilities.getAncestorOfClass(JLayer.class, aComponent);
                    if (layer != null) {
                        LayerUI<?> layerUI = layer.getUI();
                        if (layerUI instanceof TransformLayerUI) {
                            TransformLayerUI ui = (TransformLayerUI) layerUI;
                            dispatchRepaint(aComponent, layer, ui, new Rectangle(x, y, w, h));
                            return;
                        }
                    }
                }
                super.addDirtyRegion(aComponent, x, y, w, h);
            }
        }

        static class DelegateRepaintManager extends RepaintManager {
            private final RepaintManager delegate;

            DelegateRepaintManager(RepaintManager delegate) {
                this.delegate = delegate;
            }

            @Override
            public void addInvalidComponent(JComponent invalidComponent) {
                delegate.addInvalidComponent(invalidComponent);
            }

            @Override
            public void removeInvalidComponent(JComponent component) {
                delegate.removeInvalidComponent(component);
            }

            @Override
            public void addDirtyRegion(JComponent c, int x, int y, int w, int h) {
                delegate.addDirtyRegion(c, x, y, w, h);
            }

            @Override
            public void addDirtyRegion(Window window, int x, int y, int w, int h) {
                delegate.addDirtyRegion(window, x, y, w, h);
            }

            @Override
            @Deprecated
            public void addDirtyRegion(Applet applet, int x, int y, int w, int h) {
                delegate.addDirtyRegion(applet, x, y, w, h);
            }

            @Override
            public Rectangle getDirtyRegion(final JComponent c) {
                return delegate.getDirtyRegion(c);
            }

            @Override
            public void markCompletelyDirty(final JComponent c) {
                delegate.markCompletelyDirty(c);
            }

            @Override
            public boolean isCompletelyDirty(final JComponent c) {
                return delegate.isCompletelyDirty(c);
            }

            @Override
            public Dimension getDoubleBufferMaximumSize() {
                return delegate.getDoubleBufferMaximumSize();
            }

            @Override
            public void markCompletelyClean(final JComponent c) {
                delegate.markCompletelyClean(c);
            }

            public RepaintManager getDelegateManager() {
                return delegate;
            }

            @Override
            public void setDoubleBufferMaximumSize(final Dimension d) {
                delegate.setDoubleBufferMaximumSize(d);
            }

            @Override
            public void validateInvalidComponents() {
                delegate.validateInvalidComponents();
            }

            @Override
            public void paintDirtyRegions() {
                delegate.paintDirtyRegions();
            }

            @Override
            public Image getOffscreenBuffer(Component c, int proposedWidth, int proposedHeight) {
                return delegate.getOffscreenBuffer(c, proposedWidth, proposedHeight);
            }

            @Override
            public Image getVolatileOffscreenBuffer(Component c, int proposedWidth, int proposedHeight) {
                return delegate.getVolatileOffscreenBuffer(c, proposedWidth, proposedHeight);
            }

            @Override
            public boolean isDoubleBufferingEnabled() {
                return delegate.isDoubleBufferingEnabled();
            }

            @Override
            public void setDoubleBufferingEnabled(final boolean flag) {
                delegate.setDoubleBufferingEnabled(flag);
            }
        }
    }

    static class TestPanel extends JPanel {

        private boolean leftActive;
        private boolean rightActive;

        TestPanel() {
            setPreferredSize(new Dimension(600, 300));
            setMinimumSize(new Dimension(600, 300));
            setMaximumSize(new Dimension(600, 300));
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (leftActive) {
                g.setColor(Color.RED);
                g.fillRect(0, 0, getWidth() / 2, getHeight());
            }
            if (rightActive) {
                g.setColor(Color.GREEN);
                g.fillRect(getWidth() / 2, 0, getWidth() / 2, getHeight());
            }
            g.setColor(Color.BLACK);
            g.drawString("Left", (getWidth() - 50) / 4, getHeight() / 2 + 10);
            g.drawString("Right", getWidth() / 2 + (getWidth() - 50) / 4, getHeight() / 2 + 10);
        }

        public void setLeftActive(boolean leftActive) {
            this.leftActive = leftActive;
            repaint(0, 0, getWidth() / 2, getHeight());
        }

        public void setRightActive(boolean rightActive) {
            this.rightActive = rightActive;
            repaint(getWidth() / 2, 0, getWidth() / 2, getHeight());
        }
    }
}

Update 2: Having FallbackTransformRepaintManger extend RepaintManager instead of DelegateRepaintManager. However this would make using the component a destructive operation as any other custom RepaintManager set previously would be overwritten.

Atlantic answered 20/7, 2021 at 12:8 Comment(4)
Can you post a minimal reproducible example for us to test?Cowpox
Of course. Added an example, with the approaches described approaches.Atlantic
@Atlantic can you please to test my RepaintManager, maybe thats enviroment/gpu sensitiveMortar
@Mortar I can confirm that the flickering issue goes away when I make FallbackTransformRepaintManger extend RepaintManager instead of DelegateRepaintManager. However this would make using the component a destructive operation as any other custom RepaintManager set previously would be overwritten.Atlantic

© 2022 - 2024 — McMap. All rights reserved.