/* * Copyright (c) 2008-2011, 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 Container container; private final PropertyChangeListener propertyChangeListener; /** Anchor to use when adding child nodes and they don't specify one. */ private Anchor defaultAnchor; /** * 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) { this(new JPanel(layoutManager)); } /** * Constructs a SwingLayoutNode that lays out its children as though they * were children of the provided Container. * * Whatever LayoutManager is being used by the container will be used when * laying out nodes. * * @param container Container in which child nodes will effectively be laid * out */ public SwingLayoutNode(Container container) { this.container = container; propertyChangeListener = new PropertyChangeListener() { public void propertyChange(final PropertyChangeEvent event) { final String propertyName = event.getPropertyName(); if (isLayoutProperty(propertyName)) { updateContainerLayout(); } } }; defaultAnchor = 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 anchor to use when a node is added but its anchor is not * specified */ public void setAnchor(final Anchor anchor) { this.defaultAnchor = anchor; } /** * Returns the anchor being used by this LayoutManager. * * @return anchor currently being used when laying out children. */ public Anchor getAnchor() { return defaultAnchor; } /** * Some Swing layout managers (like BoxLayout) require a reference to the * proxy Container. * * For example: <code> * SwingLayoutNode layoutNode = new SwingLayoutNode(); * layoutNode.setLayout( new BoxLayout( layoutNode.getContainer(), BoxLayout.Y_AXIS ) ); * </code> * * @return container in which children will logically be laid out in */ public Container getContainer() { return container; } /** * 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 childAnchor specifies the location from which layout takes place */ public void addChild(final int index, final PNode child, final Object constraints, final Anchor childAnchor) { /* * 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, childAnchor); } /** {@inheritDoc} */ public void addChild(final int index, final PNode child) { addChild(index, child, null, defaultAnchor); } /** * 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, defaultAnchor); } /** * 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, defaultAnchor); } /** * 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, defaultAnchor); } /** * 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, defaultAnchor); } /** * 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, defaultAnchor); } /** * 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. */ 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. */ 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. */ 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. */ 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. */ 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. */ 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. */ 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. */ 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. */ 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)); } }; 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 width) { return x + width - 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 width) { return x; } }; } }