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.setDelegateRepaintManager
wasn'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)SolutionApproach.ILLEGAL
: Same asSolutionApproach.FLICKERING
but without the flickering.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 theTestPanel
(or any possible children) requests a repaint the correct area of theJLayer
should be repainted.
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.
FallbackTransformRepaintManger
extendRepaintManager
instead ofDelegateRepaintManager
. However this would make using the component a destructive operation as any other customRepaintManager
set previously would be overwritten. – Atlantic