diff --git a/extras/src/main/java/org/piccolo2d/extras/pswing/PSwing.java b/extras/src/main/java/org/piccolo2d/extras/pswing/PSwing.java index a774014..76368ba 100644 --- a/extras/src/main/java/org/piccolo2d/extras/pswing/PSwing.java +++ b/extras/src/main/java/org/piccolo2d/extras/pswing/PSwing.java @@ -37,12 +37,9 @@ import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Stroke; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; import java.awt.event.ContainerAdapter; import java.awt.event.ContainerEvent; import java.awt.event.ContainerListener; -import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.IOException; @@ -60,7 +57,6 @@ import org.piccolo2d.util.PBounds; import org.piccolo2d.util.PPaintContext; - /* This message was sent to Sun on August 27, 1999 @@ -192,6 +188,7 @@ *

* * @author Sam R. Reid + * @author Chris Malley (cmalley@pixelzoom.com) * @author Benjamin B. Bederson * @author Lance E. Good * @@ -220,7 +217,7 @@ /** * Default stroke, new BasicStroke(). Cannot be made static - * because BasicStroke is not serializable. + * because BasicStroke is not serializable. Should not be null. */ private Stroke defaultStroke = new BasicStroke(); @@ -271,12 +268,6 @@ }; - private final PropertyChangeListener reshapeListener = new PropertyChangeListener() { - public void propertyChange(final PropertyChangeEvent evt) { - repaint(); - } - }; - /** * Listens to container nodes for changes to its contents. Any additions * will automatically have double buffering turned off. @@ -296,9 +287,10 @@ * I'm assuming that the intent of the is method is that it should be * called explicitly by anyone making changes to the hierarchy of the * Swing component graph. + * @param targetComponent the component for which double buffering should be removed */ private void disableDoubleBuffering(final JComponent targetComponent) { - targetComponent.setDoubleBuffered(false); + targetComponent.setDoubleBuffered( false ); for (int i = 0; i < targetComponent.getComponentCount(); i++) { final Component c = targetComponent.getComponent(i); if (c instanceof JComponent) { @@ -319,41 +311,56 @@ initializeComponent(component); component.revalidate(); - //TODO: this listener is suspicious, it's not listening for any specific property - component.addPropertyChangeListener(new PropertyChangeListener() { - /** {@inheritDoc} */ - public void propertyChange(final PropertyChangeEvent evt) { - updateBounds(); - } - }); - updateBounds(); listenForCanvas(this); } /** + * @deprecated by {@link #PSwing(JComponent)} + * + * @param swingCanvas canvas on which the PSwing node will be embedded + * @param component not used + */ + public PSwing(final PSwingCanvas swingCanvas, final JComponent component) { + this(component); + } + + /** * Ensures the bounds of the underlying component are accurate, and sets the * bounds of this PNode. */ public void updateBounds() { - // Avoid setBounds if it is unnecessary - // TODO: should we make sure this is called at least once - // TODO: does this sometimes need to be called when size already equals - // preferred size, to relayout/update things? + /* + * Need to explicitly set the component's bounds because + * the component's parent (PSwingCanvas.ChildWrapper) has no layout manager. + */ if (componentNeedsResizing()) { - component.setBounds(0, 0, component.getPreferredSize().width, component.getPreferredSize().height); + updateComponentSize(); } - setBounds(0, 0, component.getPreferredSize().width, component.getPreferredSize().height); - } - - private boolean componentNeedsResizing() { - return component.getWidth() != component.getPreferredSize().width - || component.getHeight() != component.getPreferredSize().height; + setBounds( 0, 0, component.getPreferredSize().width, component.getPreferredSize().height ); } /** - * Determines if the Swing component should be rendered normally or as a - * filled rectangle. + * Since the parent ChildWrapper has no layout manager, it is the responsibility of this PSwing + * to make sure the component has its bounds set properly, otherwise it will not be drawn properly. + * This method sets the bounds of the component to be equal to its preferred size. + */ + private void updateComponentSize() { + component.setBounds( 0, 0, component.getPreferredSize().width, component.getPreferredSize().height ); + } + + /** + * Determines whether the component should be resized, based on whether its actual width and height + * differ from its preferred width and height. + * @return true if the component should be resized. + */ + private boolean componentNeedsResizing() { + return component.getWidth() != component.getPreferredSize().width || component.getHeight() != component.getPreferredSize().height; + } + + /** + * Paints the PSwing on the specified renderContext. Also determines if + * the Swing component should be rendered normally or as a filled rectangle (greeking). *

* The transform, clip, and composite will be set appropriately when this * object is rendered. It is up to this object to restore the transform, @@ -365,25 +372,29 @@ * @param renderContext Contains information about current render. */ public void paint(final PPaintContext renderContext) { + if (componentNeedsResizing()) { + updateComponentSize(); + component.validate(); + } final Graphics2D g2 = renderContext.getGraphics(); - if (defaultStroke == null) { - defaultStroke = new BasicStroke(); - } + //Save Stroke and Font for restoring. + Stroke originalStroke = g2.getStroke(); + Font originalFont = g2.getFont(); g2.setStroke(defaultStroke); g2.setFont(DEFAULT_FONT); - - if (component.getParent() == null) { - component.revalidate(); - } - + if (shouldRenderGreek(renderContext)) { paintAsGreek(g2); } else { paint(g2); } + + //Restore the stroke and font on the Graphics2D + g2.setStroke(originalStroke); + g2.setFont(originalFont); } /** @@ -399,24 +410,26 @@ } /** - * Paints the Swing component as greek. + * Paints the Swing component as greek. This method assumes that the stroke has been set beforehand. * * @param g2 The graphics used to render the filled rectangle */ public void paintAsGreek(final Graphics2D g2) { - final Color background = component.getBackground(); - final Color foreground = component.getForeground(); - final Rectangle2D rect = getBounds(); + //Save original color for restoring painting as greek. + Color originalColor = g2.getColor(); - if (background != null) { - g2.setColor(background); + if (component.getBackground() != null) { + g2.setColor(component.getBackground()); } - g2.fill(rect); + g2.fill(getBounds()); - if (foreground != null) { - g2.setColor(foreground); + if (component.getForeground() != null) { + g2.setColor(component.getForeground()); } - g2.draw(rect); + g2.draw(getBounds()); + + //Restore original color on the Graphics2D + g2.setColor( originalColor ); } /** {@inheritDoc} */ @@ -504,20 +517,7 @@ if (c.getFont() != null) { minFontSize = Math.min(minFontSize, c.getFont().getSize()); } - c.addPropertyChangeListener("font", this); - - // Update shape when any property (such as text or font) changes. - c.addPropertyChangeListener(reshapeListener); - - c.addComponentListener(new ComponentAdapter() { - public void componentResized(final ComponentEvent e) { - updateBounds(); - } - - public void componentShown(final ComponentEvent e) { - updateBounds(); - } - }); + c.addPropertyChangeListener( "font", this ); if (c instanceof Container) { initializeChildren((Container) c); @@ -654,7 +654,7 @@ * threshold the Swing component is rendered as 'Greek' instead of painting * the Swing component. Defaults to {@link #DEFAULT_GREEK_THRESHOLD}. * - * @see PSwing#paintGreek(PPaintContext) + * @see PSwing#paintAsGreek(Graphics2D) * @return the current Greek threshold scale */ public double getGreekThreshold() { @@ -666,7 +666,7 @@ * scale will be below this threshold the Swing component is rendered as * 'Greek' instead of painting the Swing component.. * - * @see PSwing#paintGreek(PPaintContext) + * @see PSwing#paintAsGreek(Graphics2D) * @param greekThreshold Greek threshold in scale */ public void setGreekThreshold(final double greekThreshold) { diff --git a/extras/src/main/java/org/piccolo2d/extras/pswing/PSwingRepaintManager.java b/extras/src/main/java/org/piccolo2d/extras/pswing/PSwingRepaintManager.java index a19217b..035d39a 100644 --- a/extras/src/main/java/org/piccolo2d/extras/pswing/PSwingRepaintManager.java +++ b/extras/src/main/java/org/piccolo2d/extras/pswing/PSwingRepaintManager.java @@ -33,14 +33,13 @@ import javax.swing.JComponent; import javax.swing.RepaintManager; -import javax.swing.SwingUtilities; import org.piccolo2d.util.PBounds; - /** * This RepaintManager replaces the default Swing implementation, and is used to - * intercept and repaint dirty regions of PSwing components. + * repaint dirty regions of PSwing components and make sure the PSwings have + * the appropriate size. *

* This is an internal class used by Piccolo to support Swing components in * Piccolo. This should not be instantiated, though all the public methods of @@ -50,12 +49,12 @@ *

* PBasicRepaint Manager is an extension of RepaintManager that traps those * repaints called by the Swing components that have been added to the PCanvas - * and passes these repaints to the SwingVisualComponent rather than up the + * and passes these repaints to the PSwing rather than up the * component hierarchy as usually happens. *

*

- * Also traps revalidate calls made by the Swing components added to the PCanvas - * to reshape the applicable Visual Component. + * Also traps invalidate calls made by the Swing components added to the PCanvas + * to reshape the corresponding PSwing. *

*

* Also keeps a list of PSwings that are painting. This disables repaint until @@ -63,14 +62,14 @@ * by Swing's CellRendererPane which is itself a work-around. The problem is * that JTable's, JTree's, and JList's cell renderers need to be validated * before repaint. Since we have to repaint the entire Swing component hierarchy - * (in the case of a Swing component group used as a Piccolo visual component). - * This causes an infinite loop. So we introduce the restriction that no - * repaints can be triggered by a call to paint. + * (in the case of a PSwing), this causes an infinite loop. So we introduce the + * restriction that no repaints can be triggered by a call to paint. *

* * @author Benjamin B. Bederson * @author Lance E. Good * @author Sam R. Reid + * @author Chris Malley (cmalley@pixelzoom.com) */ public class PSwingRepaintManager extends RepaintManager { @@ -120,19 +119,15 @@ * @param width Width of the dirty region in the component * @param height Height of the dirty region in the component */ - public synchronized void addDirtyRegion(final JComponent component, final int x, final int y, final int width, - final int height) { + public synchronized void addDirtyRegion(final JComponent component, final int x, final int y, final int width, final int height) { boolean captureRepaint = false; JComponent childComponent = null; - int captureX = x; int captureY = y; - // We have to check to see if the PCanvas - // (ie. the SwingWrapper) is in the components ancestry. If so, - // we will want to capture that repaint. However, we also will - // need to translate the repaint request since the component may - // be offset inside another component. + // We have to check to see if the PCanvas (ie. the SwingWrapper) is in the components ancestry. If so, we will + // want to capture that repaint. However, we also will need to translate the repaint request since the component + // may be offset inside another component. for (Component comp = component; comp != null && comp.isLightweight(); comp = comp.getParent()) { if (comp.getParent() instanceof PSwingCanvas.ChildWrapper) { captureRepaint = true; @@ -146,14 +141,14 @@ } } - // Now we check to see if we should capture the repaint and act - // accordingly + // Now we check to see if we should capture the repaint and act accordingly if (captureRepaint) { if (!isPainting(childComponent)) { final double repaintW = Math.min(childComponent.getWidth() - captureX, width); final double repaintH = Math.min(childComponent.getHeight() - captureY, height); - dispatchRepaint(childComponent, new PBounds(captureX, captureY, repaintW, repaintH)); + //Schedule a repaint for the dirty part of the PSwing + getPSwing(childComponent).repaint( new PBounds( captureX, captureY, repaintW, repaintH ) ); } } else { @@ -161,19 +156,9 @@ } } - private void dispatchRepaint(final JComponent childComponent, final PBounds repaintBounds) { - final PSwing pSwing = (PSwing) childComponent.getClientProperty(PSwing.PSWING_PROPERTY); - - SwingUtilities.invokeLater(new Runnable() { - public void run() { - pSwing.repaint(repaintBounds); - } - }); - } - /** - * This is the method "revalidate" calls in the Swing components. Overridden - * to capture revalidate calls from those Swing components being used as + * This is the method "invalidate" calls in the Swing components. Overridden + * to capture invalidation calls from those Swing components being used as * Piccolo visual components and to update Piccolo's visual component * wrapper bounds (these are stored separately from the Swing component). * Otherwise, behaves like the superclass. @@ -181,20 +166,21 @@ * @param invalidComponent The Swing component that needs validation */ public synchronized void addInvalidComponent(final JComponent invalidComponent) { - final JComponent capturedComponent = invalidComponent; - - if (capturedComponent.getParent() == null - || !(capturedComponent.getParent() instanceof PSwingCanvas.ChildWrapper)) { + if (invalidComponent.getParent() == null || !(invalidComponent.getParent() instanceof PSwingCanvas.ChildWrapper)) { super.addInvalidComponent(invalidComponent); } else { - SwingUtilities.invokeLater(new Runnable() { - public void run() { - capturedComponent.validate(); - final PSwing pSwing = (PSwing) capturedComponent.getClientProperty(PSwing.PSWING_PROPERTY); - pSwing.updateBounds(); - } - }); + invalidComponent.validate(); + getPSwing(invalidComponent).updateBounds(); } } + + /** + * Obtains the PSwing associated with the specified component. + * @param component the component for which to return the associated PSwing + * @return the associated PSwing + */ + private PSwing getPSwing(JComponent component) { + return (PSwing) component.getClientProperty( PSwing.PSWING_PROPERTY ); + } } \ No newline at end of file diff --git a/extras/src/test/java/org/piccolo2d/extras/pswing/PSwingDynamicComponentExample.java b/extras/src/test/java/org/piccolo2d/extras/pswing/PSwingDynamicComponentExample.java new file mode 100644 index 0000000..44c181a --- /dev/null +++ b/extras/src/test/java/org/piccolo2d/extras/pswing/PSwingDynamicComponentExample.java @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2008-2010, Piccolo2D project, http://piccolo2d.org + * Copyright (c) 1998-2008, University of Maryland + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided + * that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions + * and the following disclaimer in the documentation and/or other materials provided with the + * distribution. + * + * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its + * contributors may be used to endorse or promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.piccolo2d.extras.pswing; + +import javax.swing.*; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.lang.reflect.InvocationTargetException; + +/** + * Demonstrates a PSwing problem with dynamic JComponents. + *

+ * This example shows 2 identical JPanels. + * The panel on the left uses PSwing. + * The panel on the right uses pure Swing. + *

+ * The JPanel contain various JComponents whose text can be updated by + * typing into JTextFields and pressing the "Update" button. + * The JPanel managed by PSwing is often rendered incorrectly. + *

+ * Please see piccolo2d issue 163 for more information about this problem and solution: + * http://code.google.com/p/piccolo2d/issues/detail?id=163 + * + * @author Chris Malley (cmalley@pixelzoom.com) + * @author Sam Reid + */ +public class PSwingDynamicComponentExample extends JFrame { + + private static final Dimension FRAME_SIZE = new Dimension( 800, 400 ); + private static final int TEXT_FIELD_COLUMNS = 30; + + private final ComponentPanel swingPanel, piccoloPanel; + private final JTextField labelTextField, checkBoxTextField, radioButtonTextField; + + public PSwingDynamicComponentExample() { + super( PSwingDynamicComponentExample.class.getName() ); + setSize( FRAME_SIZE ); + + // canvas + PSwingCanvas canvas = new PSwingCanvas(); + canvas.setBackground( Color.RED ); + canvas.removeInputEventListener( canvas.getZoomEventHandler() ); + canvas.removeInputEventListener( canvas.getPanEventHandler() ); + + // panel that we'll display using Piccolo + piccoloPanel = new ComponentPanel(); + final PSwing pswing = new PSwing( piccoloPanel ); + canvas.getLayer().addChild( pswing ); + pswing.setOffset( 10, 10 ); + + // panel that we're display using pure Swing + swingPanel = new ComponentPanel(); + JPanel jpanel = new JPanel(); + jpanel.setBorder( new LineBorder( Color.BLACK ) ); + jpanel.add( swingPanel ); + + // text fields, for specifying dynamic text + labelTextField = new JTextField( swingPanel.label.getText(), TEXT_FIELD_COLUMNS ); + checkBoxTextField = new JTextField( swingPanel.checkBox.getText(), TEXT_FIELD_COLUMNS ); + radioButtonTextField = new JTextField( swingPanel.radioButton.getText(), TEXT_FIELD_COLUMNS ); + + // Update button, for applying dynamic text + JButton updateButton = new JButton( "Update" ); + updateButton.addActionListener( new ActionListener() { + public void actionPerformed( ActionEvent e ) { + updatePanels(); + } + } ); + + // + JButton addComponentButton = new JButton( "add component" ); + addComponentButton.addActionListener( new ActionListener() { + + public void actionPerformed( ActionEvent e ) { + piccoloPanel.addComponent( new JLabel( "new" ) ); + swingPanel.addComponent( new JLabel( "new" ) ); + } + + }); + + // control panel + JPanel controlPanel = new JPanel(); + controlPanel.setBorder( new LineBorder( Color.BLACK ) ); + controlPanel.setLayout( new GridBagLayout() ); + GridBagConstraints c = new GridBagConstraints(); + // JLabel + c.gridx = 0; + c.gridy = 0; + c.anchor = GridBagConstraints.EAST; + controlPanel.add( new JLabel( "JLabel text:" ), c ); + c.gridx++; + c.anchor = GridBagConstraints.WEST; + controlPanel.add( labelTextField, c ); + // JCheckBox + c.gridx = 0; + c.gridy++; + c.anchor = GridBagConstraints.EAST; + controlPanel.add( new JLabel( "JCheckBox text:" ), c ); + c.gridx++; + c.anchor = GridBagConstraints.WEST; + controlPanel.add( checkBoxTextField, c ); + // JRadioButton + c.gridx = 0; + c.gridy++; + c.anchor = GridBagConstraints.EAST; + controlPanel.add( new JLabel( "JRadioButton text:" ), c ); + c.gridx++; + c.anchor = GridBagConstraints.WEST; + controlPanel.add( radioButtonTextField, c ); + // Update button + c.gridx = 1; + c.gridy++; + c.anchor = GridBagConstraints.WEST; + controlPanel.add( updateButton, c ); + // Add component buttons + c.gridx = 1; + c.gridy++; + c.anchor = GridBagConstraints.WEST; + controlPanel.add( addComponentButton, c ); + + + + // main panel + JPanel mainPanel = new JPanel( new BorderLayout() ); + mainPanel.add( canvas, BorderLayout.CENTER ); + mainPanel.add( jpanel, BorderLayout.EAST ); + mainPanel.add( controlPanel, BorderLayout.SOUTH ); + setContentPane( mainPanel ); + } + + // applies the text field values to the components in the panels + private void updatePanels() { + + // Piccolo (PSwing) panel + piccoloPanel.label.setText( labelTextField.getText() ); + piccoloPanel.checkBox.setText( checkBoxTextField.getText() ); + piccoloPanel.radioButton.setText( radioButtonTextField.getText() ); + + // Swing panel + swingPanel.label.setText( labelTextField.getText() ); + swingPanel.checkBox.setText( checkBoxTextField.getText() ); + swingPanel.radioButton.setText( radioButtonTextField.getText() ); + } + + // A panel with a few different types of JComponent. + private static class ComponentPanel extends JPanel { + + // allow public access to keep our example code short + public final JLabel label; + public final JCheckBox checkBox; + public final JRadioButton radioButton; + public final GridBagConstraints constraints; + + public ComponentPanel() { + setBorder( new CompoundBorder( new LineBorder( Color.BLACK, 1 ), new EmptyBorder( 5, 14, 5, 14 ) ) ); + setBackground( new Color( 180, 205, 255 ) ); + + // components + label = new JLabel( "JLabel" ); + checkBox = new JCheckBox( "JCheckBox" ); + radioButton = new JRadioButton( "JRadioButton" ); + + // layout + setLayout( new GridBagLayout() ); + constraints = new GridBagConstraints(); + constraints.anchor = GridBagConstraints.WEST; + constraints.gridx = 0; + constraints.gridy = GridBagConstraints.RELATIVE; + addComponent( label ); + addComponent( checkBox ); + addComponent( radioButton ); + } + + public void addComponent( JComponent c ) { + add( c, constraints ); + revalidate(); + } + } + + public static class SleepThread extends Thread { + + public SleepThread( long millis ) { + super( new Runnable() { + public void run() { + while ( true ) { + try { + SwingUtilities.invokeAndWait( new Runnable() { + public void run() { + try { + Thread.sleep( 1000 ); + } + catch ( InterruptedException e ) { + e.printStackTrace(); + } + } + } ); + } + catch ( InterruptedException e ) { + e.printStackTrace(); + } + catch ( InvocationTargetException e ) { + e.printStackTrace(); + } + } + } + } ); + } + } + + public static void main( String[] args ) { + // This thread serves to make the problem more noticeable. +// new SleepThread( 1000 ).start(); + SwingUtilities.invokeLater( new Runnable() { + public void run() { + JFrame frame = new PSwingDynamicComponentExample(); + frame.setDefaultCloseOperation( WindowConstants.EXIT_ON_CLOSE ); + frame.setVisible( true ); + } + } ); + } +}