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 ); + } + } ); + } +}