diff --git a/examples/src/main/java/edu/umd/cs/piccolo/examples/SwingLayoutExample.java b/examples/src/main/java/edu/umd/cs/piccolo/examples/SwingLayoutExample.java new file mode 100644 index 0000000..5876634 --- /dev/null +++ b/examples/src/main/java/edu/umd/cs/piccolo/examples/SwingLayoutExample.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2008-2009, 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 edu.umd.cs.piccolo.examples; + +import java.awt.BasicStroke; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Rectangle2D; + +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSlider; +import javax.swing.JTextField; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import edu.umd.cs.piccolo.PCanvas; +import edu.umd.cs.piccolo.PNode; +import edu.umd.cs.piccolo.event.PBasicInputEventHandler; +import edu.umd.cs.piccolo.event.PInputEvent; +import edu.umd.cs.piccolo.nodes.PHtmlView; +import edu.umd.cs.piccolo.nodes.PPath; +import edu.umd.cs.piccolo.nodes.PText; +import edu.umd.cs.piccolox.pswing.PSwing; +import edu.umd.cs.piccolox.pswing.PSwingCanvas; +import edu.umd.cs.piccolox.swing.SwingLayoutNode; +import edu.umd.cs.piccolox.swing.SwingLayoutNode.Anchor; + +public class SwingLayoutExample { + + public static class MyPPath extends PPath { + public MyPPath(final Shape shape, final Color color, final Stroke stroke, final Color strokeColor) { + super(shape, stroke); + setPaint(color); + setStrokePaint(strokeColor); + } + } + + public static void main(final String[] args) { + + final Dimension canvasSize = new Dimension(800, 600); + final PCanvas canvas = new PSwingCanvas(); + canvas.setPreferredSize(canvasSize); + + final PNode rootNode = new PNode(); + canvas.getLayer().addChild(rootNode); + rootNode.addInputEventListener(new PBasicInputEventHandler() { + // Shift+Drag up/down will scale the node up/down + public void mouseDragged(final PInputEvent event) { + super.mouseDragged(event); + if (event.isShiftDown()) { + event.getPickedNode().scale(event.getCanvasDelta().height > 0 ? 0.98 : 1.02); + } + } + }); + + final BorderLayout borderLayout = new BorderLayout(); + borderLayout.setHgap(10); + borderLayout.setVgap(5); + final SwingLayoutNode borderLayoutNode = new SwingLayoutNode(borderLayout); + borderLayoutNode.addChild(new PText("North"), BorderLayout.NORTH); + borderLayoutNode.setAnchor(Anchor.CENTER); + borderLayoutNode.addChild(new PText("South"), BorderLayout.SOUTH); + borderLayoutNode.setAnchor(Anchor.WEST); + borderLayoutNode.addChild(new PText("East"), BorderLayout.EAST); + borderLayoutNode.addChild(new PText("West"), BorderLayout.WEST); + borderLayoutNode.addChild(new PText("CENTER"), BorderLayout.CENTER); + borderLayoutNode.setOffset(100, 100); + rootNode.addChild(borderLayoutNode); + + final SwingLayoutNode flowLayoutNode = new SwingLayoutNode(new FlowLayout()); + flowLayoutNode.addChild(new PText("1+1")); + flowLayoutNode.addChild(new PText("2+2")); + flowLayoutNode.setOffset(200, 200); + rootNode.addChild(flowLayoutNode); + + final SwingLayoutNode gridBagLayoutNode = new SwingLayoutNode(new GridBagLayout()); + final GridBagConstraints gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = GridBagConstraints.RELATIVE; + gridBagLayoutNode.addChild(new PText("FirstNode"), gridBagConstraints); + gridBagLayoutNode.addChild(new PText("SecondNode"), gridBagConstraints); + gridBagConstraints.insets = new Insets(50, 50, 50, 50); + gridBagLayoutNode.addChild(new PText("ThirdNode"), gridBagConstraints); + gridBagLayoutNode.setOffset(400, 250); + rootNode.addChild(gridBagLayoutNode); + + final SwingLayoutNode boxLayoutNode = new SwingLayoutNode(); + boxLayoutNode.setLayout(new BoxLayout(boxLayoutNode.getContainer(), BoxLayout.Y_AXIS)); + boxLayoutNode.addChild(new MyPPath(new Rectangle2D.Double(0, 0, 50, 50), Color.yellow, new BasicStroke(2), + Color.red)); + boxLayoutNode.addChild(new MyPPath(new Rectangle2D.Double(0, 0, 100, 50), Color.orange, new BasicStroke(2), + Color.blue)); + final SwingLayoutNode innerNode = new SwingLayoutNode(); // nested + // layout + innerNode.addChild(new PSwing(new JLabel("foo"))); + innerNode.addChild(new PSwing(new JLabel("bar"))); + boxLayoutNode.addChild(innerNode, Anchor.CENTER); + boxLayoutNode.setOffset(300, 300); + rootNode.addChild(boxLayoutNode); + + final SwingLayoutNode horizontalLayoutNode = new SwingLayoutNode(new GridBagLayout()); + horizontalLayoutNode.addChild(new PSwing(new JButton("Zero"))); + horizontalLayoutNode.addChild(new PSwing(new JButton("One"))); + horizontalLayoutNode.addChild(new PSwing(new JButton("Two"))); + horizontalLayoutNode.addChild(new PSwing(new JLabel("Three"))); + horizontalLayoutNode.addChild(new PSwing(new JSlider())); + horizontalLayoutNode.addChild(new PSwing(new JTextField("Four"))); + final PHtmlView htmlNode = new PHtmlView("Five", new JLabel().getFont().deriveFont(15f), + Color.blue); + htmlNode.scale(3); + horizontalLayoutNode.addChild(htmlNode); + horizontalLayoutNode.setOffset(100, 450); + rootNode.addChild(horizontalLayoutNode); + + // 3x2 grid of values, shapes and labels (similar to a layout in + // acid-base-solutions) + final SwingLayoutNode gridNode = new SwingLayoutNode(new GridBagLayout()); + final GridBagConstraints constraints = new GridBagConstraints(); + constraints.insets = new Insets(10, 10, 10, 10); + /*---- column of values, right justified ---*/ + constraints.gridy = 0; // row + constraints.gridx = 0; // column + constraints.anchor = GridBagConstraints.EAST; + final PText dynamicNode = new PText("0"); // will be controlled by + // dynamicSlider + gridNode.addChild(dynamicNode, constraints); + constraints.gridy++; + gridNode.addChild(new PText("0"), constraints); + /*---- column of shapes, center justified ---*/ + constraints.gridy = 0; // row + constraints.gridx++; // column + constraints.anchor = GridBagConstraints.CENTER; + final PPath redCircle = new PPath(new Ellipse2D.Double(0, 0, 25, 25)); + redCircle.setPaint(Color.RED); + gridNode.addChild(redCircle, constraints); + constraints.gridy++; + final PPath greenCircle = new PPath(new Ellipse2D.Double(0, 0, 25, 25)); + greenCircle.setPaint(Color.GREEN); + gridNode.addChild(greenCircle, constraints); + /*---- column of labels, left justified ---*/ + constraints.gridy = 0; // row + constraints.gridx++; // column + constraints.anchor = GridBagConstraints.WEST; + gridNode.addChild(new PHtmlView("H2O"), constraints); + constraints.gridy++; + gridNode.addChild(new PHtmlView("H3O+"), constraints); + gridNode.scale(2.0); + gridNode.setOffset(400, 50); + rootNode.addChild(gridNode); + + final JPanel controlPanel = new JPanel(); + controlPanel.setLayout(new BoxLayout(controlPanel, BoxLayout.Y_AXIS)); + final JSlider dynamicSlider = new JSlider(0, 1000, 0); // controls + // dynamicNode + dynamicSlider.setMajorTickSpacing(dynamicSlider.getMaximum()); + dynamicSlider.setPaintTicks(true); + dynamicSlider.setPaintLabels(true); + dynamicSlider.addChangeListener(new ChangeListener() { + + public void stateChanged(final ChangeEvent e) { + dynamicNode.setText(String.valueOf(dynamicSlider.getValue())); + } + }); + controlPanel.add(dynamicSlider); + + final JPanel appPanel = new JPanel(new BorderLayout()); + appPanel.add(canvas, BorderLayout.CENTER); + appPanel.add(controlPanel, BorderLayout.EAST); + + final JFrame frame = new JFrame(); + frame.setContentPane(appPanel); + frame.pack(); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setVisible(true); + } +} diff --git a/extras/src/main/java/edu/umd/cs/piccolox/swing/SwingLayoutNode.java b/extras/src/main/java/edu/umd/cs/piccolox/swing/SwingLayoutNode.java new file mode 100644 index 0000000..5369b31 --- /dev/null +++ b/extras/src/main/java/edu/umd/cs/piccolox/swing/SwingLayoutNode.java @@ -0,0 +1,657 @@ +/* + * Copyright (c) 2008-2009, 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 edu.umd.cs.piccolox.swing; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.LayoutManager; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Collection; +import java.util.Iterator; + +import javax.swing.JComponent; +import javax.swing.JPanel; + +import edu.umd.cs.piccolo.PNode; + +/** + * Uses Swing layout managers to position PNodes. + * + * @author Sam Reid + * @author Chris Malley (cmalley@pixelzoom.com) + */ +public class SwingLayoutNode extends PNode { + + /** + * + */ + private static final long serialVersionUID = 1L; + /* + * How the space allocated by the Swing layout manager is used differs + * depending on Swing component type. The behavior of a default JLabel + * (Anchors.WEST) seems to make the most sense for PNodes. + */ + private static final Anchor DEFAULT_ANCHOR = Anchor.WEST; + /** Container for ProxyComponents. */ + private final JPanel container; + + private final PropertyChangeListener propertyChangeListener; + private Anchor anchor; + + /** + * Construct a SwingLayoutNode that uses FlowLayout. + */ + public SwingLayoutNode() { + this(new FlowLayout()); + } + + /** + * Constructs a SwingLayoutNode that uses the provided LayoutManager to + * layout its children. + * + * @param layoutManager LayoutManager to use for laying out children. Must + * not be null. + */ + public SwingLayoutNode(final LayoutManager layoutManager) { + container = new JPanel(layoutManager); + propertyChangeListener = new PropertyChangeListener() { + public void propertyChange(final PropertyChangeEvent event) { + final String propertyName = event.getPropertyName(); + if (isLayoutProperty(propertyName)) { + updateContainerLayout(); + } + } + }; + anchor = DEFAULT_ANCHOR; + } + + /** + * Sets the default anchor. If no anchor is specified when a node is added, + * then the default anchor determines where the node is positioned in the + * space allocated by the Swing layout manager. + * + * @param anchor + */ + public void setAnchor(final Anchor anchor) { + this.anchor = anchor; + } + + /** + * Returns the anchor being used by this LayoutManager. + * + * @return anchor currently being used when laying out children. + */ + public Anchor getAnchor() { + return anchor; + } + + /** + * Some Swing layout managers (like BoxLayout) require a reference to the + * proxy Container. + * + * For example: + * SwingLayoutNode layoutNode = new SwingLayoutNode(); + * layoutNode.setLayout( new BoxLayout( layoutNode.getContainer(), BoxLayout.Y_AXIS ) ); + * + * + * @return container in which children will logically be laid out in + */ + public Container getContainer() { + return container; + } + + /** + * Sets the layout manager. + *

+ * It's recommended that you avoid using this method, it's a can of worms. + * Like Swing, if you call this after adding nodes, the results may be + * unpredictable. You'll also have problems if the constraints that you + * specified to addChild aren't compatible with the new layout manager, or + * if the new layout manager requires constraints. + * + * @param layoutManager + */ + public void setLayout(final LayoutManager layoutManager) { + container.setLayout(layoutManager); + updateContainerLayout(); + } + + /** + * Adds a child at the specified index. Like Swing, bad things can happen if + * the type of the constraints isn't compatible with the layout manager. + * + * @param index 0 based index at which to add the child + * @param child child to be added + * @param constraints constraints the layout manager uses when laying out + * the child + * @param anchor specifies the location from which layout takes place + */ + public void addChild(final int index, final PNode child, final Object constraints, final Anchor anchor) { + /* + * NOTE: This must be the only super.addChild call that we make in our + * entire implementation, because all PNode.addChild methods are + * implemented in terms of this one. Calling other variants of + * super.addChild will incorrectly invoke our overrides, resulting in + * StackOverflowException. + */ + super.addChild(index, child); + addProxyComponent(child, constraints, anchor); + } + + /** {@inheritDoc} */ + public void addChild(final int index, final PNode child) { + addChild(index, child, null, anchor); + } + + /** + * Adds a child at the specified index. Like Swing, bad things can happen if + * the type of the constraints isn't compatible with the layout manager. + * + * @param index 0 based index at which to add the child + * @param child child to be added + * @param constraints constraints the layout manager uses when laying out + * the child + */ + public void addChild(final int index, final PNode child, final Object constraints) { + addChild(index, child, constraints, anchor); + } + + /** + * Adds a child at the specified index. + * + * @param index 0 based index at which to add the child + * @param child child to be added + * @param anchor specifies the location from which layout takes place + */ + public void addChild(final int index, final PNode child, final Anchor anchor) { + addChild(index, child, null, anchor); + } + + /** + * Adds a child to the end of the node list. + * + * @param child child to be added + * @param constraints constraints the layout manager uses when laying out + * the child + * @param anchor specifies the location from which layout takes place + */ + public void addChild(final PNode child, final Object constraints, final Anchor anchor) { + // NOTE: since PNode.addChild(PNode) is implemented in terms of + // PNode.addChild(int index), we must do the same. + int index = getChildrenCount(); + // workaround a flaw in PNode.addChild(PNode), they should have handled + // this in PNode.addChild(int index). + if (child.getParent() == this) { + index--; + } + addChild(index, child, constraints, anchor); + } + + /** + * Adds a child to the end of the node list. + * + * @param child child to be added + */ + public void addChild(final PNode child) { + addChild(child, null, anchor); + } + + /** + * Adds a child to the end of the node list and specifies the given + * constraints. + * + * @param child child to be added + * @param constraints constraints the layout manager uses when laying out + * the child + */ + public void addChild(final PNode child, final Object constraints) { + addChild(child, constraints, anchor); + } + + /** + * Adds a child to the end of the node list. + * + * @param child child to be added + * @param anchor specifies the location from which layout takes place + */ + public void addChild(final PNode child, final Anchor anchor) { + addChild(child, null, anchor); + } + + /** + * Adds a collection of nodes to the end of the list. + * + * @param nodes nodes to add to the end of the list + * @param constraints constraints the layout manager uses when laying out + * the child + * @param anchor specifies the location from which layout takes place + */ + public void addChildren(final Collection nodes, final Object constraints, final Anchor anchor) { + final Iterator i = nodes.iterator(); + while (i.hasNext()) { + final PNode each = (PNode) i.next(); + addChild(each, constraints, anchor); + } + } + + /** {@inheritDoc} */ + public void addChildren(final Collection nodes) { + addChildren(nodes, null, anchor); + } + + /** + * Adds a collection of nodes to the end of the list. + * + * @param nodes nodes to add to the end of the list + * @param constraints constraints the layout manager uses when laying out + * the child + */ + public void addChildren(final Collection nodes, final Object constraints) { + addChildren(nodes, constraints, anchor); + } + + /** + * Adds a collection of nodes to the end of the list. + * + * @param nodes nodes to add to the end of the list + * @param anchor specifies the location from which layout takes place + */ + public void addChildren(final Collection nodes, final Anchor anchor) { + addChildren(nodes, null, anchor); + } + + /** + * Removes a node at a specified index. + * + * @param index 0 based index of the child to be removed + */ + public PNode removeChild(final int index) { + /* + * NOTE: This must be the only super.removeChild call that we make in + * our entire implementation, because all PNode.removeChild methods are + * implemented in terms of this one. Calling other variants of + * super.removeChild will incorrectly invoke our overrides, resulting in + * StackOverflowException. + */ + final PNode node = super.removeChild(index); + removeProxyComponent(node); + return node; + } + + /* + * NOTE We don't need to override removeChild(PNode) or removeChildren, + * because they call removeChild(int index). If their implementation ever + * changes, then we'll need to override them. + */ + + /** + * PNode.removeAllChildren does not call removeChild, it manipulates an + * internal data structure. So we must override this in a more careful (and + * less efficient) manner. + */ + public void removeAllChildren() { + final Iterator i = getChildrenIterator(); + while (i.hasNext()) { + removeChild((PNode) i.next()); + } + } + + /** + * Adds a proxy component for a node. + * + * @param node node for which to add the proxy component + * @param constraints Constraints to apply when laying out the component + * @param anchor relative anchor point of the underyling proxy component on + * its container + */ + private void addProxyComponent(final PNode node, final Object constraints, final Anchor anchor) { + final ProxyComponent component = new ProxyComponent(node, anchor); + container.add(component, constraints); + node.addPropertyChangeListener(propertyChangeListener); + updateContainerLayout(); + } + + /** + * Removes a proxy component for a node. Does nothing if the node is not a + * child of the layout. + * + * @param node node from which the proxy container should be removed from. + */ + private void removeProxyComponent(final PNode node) { + if (node != null) { + final ProxyComponent component = getComponentForNode(node); + if (component != null) { + container.remove(component); + node.removePropertyChangeListener(propertyChangeListener); + updateContainerLayout(); + } + } + } + + /** + * Finds the component that is serving as the proxy for a specific node. + * Returns null if not found. + */ + private ProxyComponent getComponentForNode(final PNode node) { + ProxyComponent nodeComponent = null; + final Component[] components = container.getComponents(); + if (components != null) { + for (int i = 0; i < components.length && nodeComponent == null; i++) { + if (components[i] instanceof ProxyComponent) { + final ProxyComponent n = (ProxyComponent) components[i]; + if (n.getNode() == node) { + nodeComponent = n; + } + } + } + } + return nodeComponent; + } + + /** + * Helper to figure out if the given property name relates to layout. + * + * @param propertyName name of property being tested + * + * @return true property name relates to layout. + */ + private boolean isLayoutProperty(final String propertyName) { + return propertyName.equals(PNode.PROPERTY_VISIBLE) || propertyName.equals(PNode.PROPERTY_FULL_BOUNDS) || + propertyName.equals(PNode.PROPERTY_BOUNDS) || propertyName.equals(PNode.PROPERTY_TRANSFORM); + } + + /** + * Updates the Proxy Container's layout. + */ + private void updateContainerLayout() { + container.invalidate(); // necessary for layouts like BoxLayout that + // would otherwise use stale state + container.setSize(container.getPreferredSize()); + container.doLayout(); + } + + /** + * JComponent that acts as a proxy for a PNode. Provides the PNode's bounds + * info for all bounds-related requests. + */ + private static class ProxyComponent extends JComponent { + private static final long serialVersionUID = 1L; + private final PNode node; + private final Anchor anchor; + + public ProxyComponent(final PNode node, final Anchor anchor) { + this.node = node; + this.anchor = anchor; + } + + /** + * Returns the associated PNode. + * + * @return associated PNode + */ + public PNode getNode() { + return node; + } + + /** + * Report the node's dimensions as the ProxyComponent's preferred size. + */ + public Dimension getPreferredSize() { + // Round up fractional part instead of rounding down; better to + // include the whole node than to chop off part. + final double w = node.getFullBoundsReference().getWidth(); + final double h = node.getFullBoundsReference().getHeight(); + return new Dimension(roundUp(w), roundUp(h)); + } + + private int roundUp(final double val) { + return (int) Math.ceil(val); + } + + /** + * Return the PNode size as the minimum dimension; required by layouts + * such as BoxLayout. + * + * @return the minimum size for this component + */ + public Dimension getMinimumSize() { + return getPreferredSize(); + } + + /** + * Sets the bounds of the ProxyComponent and positions the node in the + * area (x,y,w,h) allocated by the layout manager. + */ + public void setBounds(final int x, final int y, final int w, final int h) { + // important to check that the bounds have really changed, or we'll + // cause StackOverflowException + if (x != getX() || y != getY() || w != getWidth() || h != getHeight()) { + super.setBounds(x, y, w, h); + anchor.positionNode(node, x, y, w, h); + } + } + } + + /** + * Determines where nodes are anchored in the area allocated by the Swing + * layout manager. Predefined anchor names are similar to GridBagConstraint + * anchors and have the same semantics. + */ + public interface Anchor { + + /** + * Positions the node in the bounds defined. + * + * @param node node to be laid out + * @param x left of bounds + * @param y top of bounds + * @param width width of bounds + * @param height height of bounds + */ + void positionNode(PNode node, double x, double y, double width, double height); + + /** + * Base class that provides utilities for computing common anchor + * points. + */ + + /** Anchors the node's center as the point used when laying it out. */ + public static final Anchor CENTER = new AbstractAnchor() { + /** {@inheritDoc} */ + public void positionNode(final PNode node, final double x, final double y, final double w, final double h) { + node.setOffset(centerX(node, x, w), centerY(node, y, h)); + } + }; + + /** Anchors the node's top center as the point used when laying it out. */ + public static final Anchor NORTH = new AbstractAnchor() { + /** {@inheritDoc} */ + public void positionNode(final PNode node, final double x, final double y, final double w, final double h) { + node.setOffset(centerX(node, x, w), north(node, y, h)); + } + }; + + /** Anchors the node's top right as the point used when laying it out. */ + public static final Anchor NORTHEAST = new AbstractAnchor() { + /** {@inheritDoc} */ + public void positionNode(final PNode node, final double x, final double y, final double w, final double h) { + node.setOffset(east(node, x, w), north(node, y, h)); + } + }; + + /** + * Anchors the node's middle right as the point used when laying it out. + */ + public static final Anchor EAST = new AbstractAnchor() { + /** {@inheritDoc} */ + public void positionNode(final PNode node, final double x, final double y, final double w, final double h) { + node.setOffset(east(node, x, w), centerY(node, y, h)); + } + }; + + /** + * Anchors the node's bottom right as the point used when laying it out. + */ + public static final Anchor SOUTHEAST = new AbstractAnchor() { + /** {@inheritDoc} */ + public void positionNode(final PNode node, final double x, final double y, final double w, final double h) { + node.setOffset(east(node, x, w), south(node, y, h)); + } + }; + + /** + * Anchors the node's center bottom as the point used when laying it + * out. + */ + public static final Anchor SOUTH = new AbstractAnchor() { + /** {@inheritDoc} */ + public void positionNode(final PNode node, final double x, final double y, final double w, final double h) { + node.setOffset(centerX(node, x, w), south(node, y, h)); + } + }; + + /** Anchors the node's bottom left as the point used when laying it out. */ + public static final Anchor SOUTHWEST = new AbstractAnchor() { + /** {@inheritDoc} */ + public void positionNode(final PNode node, final double x, final double y, final double w, final double h) { + node.setOffset(west(node, x, w), south(node, y, h)); + } + }; + + /** Anchors the node's middle left as the point used when laying it out. */ + public static final Anchor WEST = new AbstractAnchor() { + /** {@inheritDoc} */ + public void positionNode(final PNode node, final double x, final double y, final double w, final double h) { + node.setOffset(west(node, x, w), centerY(node, y, h)); + } + }; + + /** Anchors the node's top left as the point used when laying it out. */ + public static final Anchor NORTHWEST = new AbstractAnchor() { + /** {@inheritDoc} */ + public void positionNode(final PNode node, final double x, final double y, final double w, final double h) { + node.setOffset(west(node, x, w), north(node, y, h)); + } + }; + + public static abstract class AbstractAnchor implements Anchor { + /** + * Returns the x at which the given node would need to be placed so + * that its center was in the middle of the horizontal segment + * defined by x and width. + * + * @param node node which is being analyzed + * @param x x component of horizontal line segment + * @param width width of horizontal line segment + * @return x at which node would need to be placed so that its + * center matched the center of the line segment + */ + protected static double centerX(final PNode node, final double x, final double width) { + return x + (width - node.getFullBoundsReference().getWidth()) / 2; + } + + /** + * Returns the y at which the given node would need to be placed so + * that its center was in the middle of the vertical segment defined + * by y and h. + * + * @param node node which is being analyzed + * @param y y component of horizontal line segment + * @param height height of vertical line segment + * @return y at which node would need to be placed so that its + * center matched the center of the line segment + */ + protected static double centerY(final PNode node, final double y, final double height) { + return y + (height - node.getFullBoundsReference().getHeight()) / 2; + } + + /** + * Returns the y at which the given node would need to be placed so + * that its top was against the top of the vertical segment defined. + * + * @param node node which is being analyzed + * @param y y component of horizontal line segment + * @param height height of vertical line segment + * @return y at which node would need to be placed so that its top + * matched the start of the line segment (y) + */ + protected static double north(final PNode node, final double y, final double height) { + return y; + } + + /** + * Returns the y at which the given node would need to be placed so + * that its bottom was against the bottom of the vertical range + * defined. + * + * @param node node which is being analyzed + * @param y y component of vertical range + * @param height height of vertical range + * @return y at which node would need to be placed so that its + * bottom matched the bottom of the range + */ + protected static double south(final PNode node, final double y, final double height) { + return y + height - node.getFullBoundsReference().getHeight(); + } + + /** + * Returns the x at which the given node would need to be placed so + * that its right side was against the right side of the horizontal + * range defined. + * + * @param node node which is being analyzed + * @param x x component of horizontal range + * @param width width of horizontal range + * @return x at which node would need to be placed so that its right + * side touched the right side of the range defined. + */ + protected static double east(final PNode node, final double x, final double w) { + return x + w - node.getFullBoundsReference().getWidth(); + } + + /** + * Returns the x at which the given node would need to be placed so + * that its left side was against the left side of the horizontal + * range defined. + * + * @param node node which is being analyzed + * @param x x component of horizontal range + * @param width width of horizontal range + * @return x at which node would need to be placed so that its left + * side touched the left side of the range defined (x) + */ + protected static double west(final PNode node, final double x, final double w) { + return x; + } + }; + } +} \ No newline at end of file