// Inlay.java import java.util.*; import javax.swing.*; import javax.swing.border.*; import java.awt.*; import java.awt.event.*; /** * An Inlay object is a container similar to javax.swing.Box in that it * lays out added components along a given axis. What it adds are visual * borders, alternating color backgrounds for nested Inlay components, * a selection model, and a tree traversal method. * * The selection model works as follows: Each mouse click event sent to an * Inlay component normally causes it to swap its background color with a * highlighting color. If the selected instance is transparent and has another * Inlay instance as its parent, then given mouse events are dispatched * directly to that parent. The purpose of dispatching mouse click events to * the parent is to allow transparent Inlay instances to be used as convienient * containers to position visual components within a parent, and have them all * behave as if they were simply part of that parent's display. * * Note: Users should not use any JPanel editing methods other than the add * and remove methods defined here, otherwise it may display incorrectly. * * @author Melinda Green */ public class Inlay extends JPanel { private static final int PAD_PIXELS = 10; private static final Color HIGHLIGHT_COLOR = Color.yellow.darker(); private static final Color backgroundColors[] = { Color.white, Color.gray, }; private int layoutDirection; private boolean isHighlighted = false; private boolean highlightable = true; private boolean contrastWithParent = true; private int normalBackground = 0; /** the list of highlight listeners */ private HashSet highlightListeners = new HashSet(); /** returns highlighted state. */ public boolean isHighlighted() { return isHighlighted; } public void setHighlightable(boolean highlightable) { this.highlightable = highlightable; } /** returns layout direction. one of the javax.swing.BoxLayout constants */ public int getLayoutDirection() { return layoutDirection; } public String toString() { return "Inlay"; } /** * Constructs an Inlay object which lays out its children in the * given direction. Note: it is an error to attempt to change the * layout manager on Inlay objects. * * @param direction is one of the BoxLayout direction constants, * either BoxLayout.X_AXIS or BoxLayout.Y_AXIS. * @param constrastParent specifies whether the instance should attempt to * match the background color of any parent Inlay, or to contrast with it. * @param borderWidth specifies the width of the internal border padding in pixels. * @param borderHeight specifies the height of the internal border padding in pixels. */ public Inlay(int direction, boolean contrastParent, int borderWidth, int borderHeight) { layoutDirection = direction; contrastWithParent = contrastParent; super.setLayout(new BoxLayout(this, layoutDirection)); setBorder(new EmptyBorder(borderHeight, borderWidth, borderHeight, borderWidth)); setBackground(backgroundColors[normalBackground]); setAlignmentX(Component.CENTER_ALIGNMENT); setAlignmentY(Component.CENTER_ALIGNMENT); addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent me) { if( ! highlightable) return; Container parent = getParent(); // always just toggle when not transparent // or when transparent but no containing Inlay parent to dispatch to. if (isOpaque() || (! (parent instanceof Inlay))) { toggleHighlighted(); return; } // this message is not for us. translate into parent's // coordinate system and dispatch the event to it. // not sure if "source" field needs to change too. Point loc_in_parent = getLocation(); me.translatePoint(loc_in_parent.x, loc_in_parent.y); parent.dispatchEvent(me); } }); } /** * Same as the four argument constructor but assumes the default * border width. */ public Inlay(int direction, boolean contrastParent) { this(direction, contrastParent, PAD_PIXELS, PAD_PIXELS); } /** * Same as the two argument constructor but assumes contrasting background. */ public Inlay(int direction) { this(direction, true); } /** * just calls super.paint(g) and when highlighted, * draws a one pixel wide border in the normal background color * so this instance can be distinguished from an also highlighted * parent. */ public void paint(Graphics g) { super.paint(g); if( ! isHighlighted) return; g.setColor(backgroundColors[normalBackground]); g.drawRect(0, 0, getSize().width-1, getSize().height-1); } /** * Called from parent Inlay containers to inform children of a change * in the parent's background color. this is needed for children who * must maintain a contrasting color with that of their parent. */ private void parentBackgroundColorIs(int parentBackground) { // first, save our new color normalBackground = contrastWithParent ? oppositeColor(parentBackground) : parentBackground; if( ! isHighlighted) // tell swing setBackground(backgroundColors[normalBackground]); // finaly, tell the kids Component kids[] = getComponents(); for(int i=0; i<kids.length; i++) { if(kids[i] instanceof Inlay) ((Inlay)kids[i]).parentBackgroundColorIs(normalBackground); } } private static int oppositeColor(int color) { return color == 0 ? 1 : 0; } ////////////////////// // EDITING CONTROLS // ////////////////////// /** * Overrides the protected method called by all add methods. * NOTE: For each component added after the first one, this * implementation adds two objects for each one given. * This could screw up code which counts, changes background color, * or removes elements it didn't add, etc. * Perhaps this should be a subclass of Container rather than JPanel * which would deligate to an internal JPanel? Tricky to do solidly * either way. */ protected void addImpl(Component comp, Object constraints, int index) { if(comp instanceof JComponent) ((JComponent)comp).setAlignmentX(Component.CENTER_ALIGNMENT); if(getComponentCount() > 0) super.addImpl(space(), null, index); // adds spacer between previous super.addImpl(comp, constraints, index); // adds given component if(comp instanceof Inlay) { // set child's color to be opposite of ours Inlay inlay = (Inlay)comp; inlay.parentBackgroundColorIs(normalBackground); inlay.addHighlightListener(nanny); // nanny reports child highlight events inlay.fireHighlightChanged(this); // workaround for swing bug? } invalidate(); } /** * Don't know why java.awt.Container doesn't include this useful method, * so its implemented here. */ public int indexOf(Component comp) { Component kids[] = getComponents(); for(int i=0; i<kids.length; i++) { if(kids[i] == comp) return i; } return -1; } /** * returns the number of Inlay children. */ public int getKidCount() { Component kids[] = getComponents(); int kidCount = 0; for(int i=0; i<kids.length; i++) { if(kids[i] instanceof Inlay) kidCount++; } return kidCount; } /** * returns an array containing all the Inlay children. */ public Inlay[] getKids() { Component kids[] = (Component[])getComponents().clone(); Inlay inlays[] = new Inlay[getKidCount()]; int nkids = 0; for(int i=0; i<kids.length; i++) if(kids[i] instanceof Inlay) inlays[nkids++] = (Inlay)kids[i]; return inlays; } /** * overrides the remove methods to account for spacers added. */ public void remove(Component comp) { remove(indexOf(comp)); } /** * removes the component at the given index * as well as any associated spacer. */ public void remove(int index) { super.remove(index); if(index > 0) super.remove(index-1); // remove preceeding spacer else // given index == 0 if(getComponentCount() > 1) // there are following components super.remove(0); // remove the spacer that *was* following (but now at 0) } /////////////////////////// // HIGHLIGHTING CONTROLS // /////////////////////////// /** * Listeners notified on highlight changes. */ public interface HighlightListener { /** * called when the highlighting state is changed on the Inlay * to which a listener is added. The method is also called if * the highlightin state of any of its descendants changes. * In all cases, the given Inlay is the one who's state has changed. * Just be aware that it may not be the same Inlay to which the * listener was added. */ public void highlightChanged(Inlay whose); /** * returning <code>true</code> causes attached Inlay instances to * report highlighting events of all nested Inlay instances as well * as those of the attached Inlay. returning <code>false</code> * causes notification of highlighting events of the attached Inlay * instances only. */ public boolean doRecurse(); } /** * Swaps the background color between normal color and highlight color. * @param doNotify specifies whether attached HighlightListeners should * be notified of the change. */ protected void toggleHighlighted(boolean doNotify) { setHighlighted(!isHighlighted, doNotify); } /** * Same as the two argument version but with listeners always notified. */ public void toggleHighlighted() { toggleHighlighted(true); } /** * Sets the highlighted state. * @param doNotify specifies whether attached HighlightListeners should * be notified if the highlighted state changes. */ public void setHighlighted(boolean on, boolean doNotify) { if (isHighlighted == on) { //System.out.println("highlighting already " + (on ? "on" : "off")); return; } isHighlighted = on; setBackground(isHighlighted ? HIGHLIGHT_COLOR : backgroundColors[normalBackground]); if(doNotify) fireHighlightChanged(this); } /** * Same as the two argument version but with listeners always notified. */ public void setHighlighted(boolean on) { setHighlighted(on, true); } /** * adds a listener which will be called any time the highlighting state * is changed on the called instance <i>or any of its descendants</i>. */ public void addHighlightListener(HighlightListener hl) { highlightListeners.add(hl); } /** * partial implementation of HighlightListener which always recurses. */ public static abstract class HighlightAdapter implements HighlightListener { public boolean doRecurse() { return true; } } /** * removes a previously added highlight listener. */ public void removeHighlightListener(HighlightListener hl) { highlightListeners.remove(hl); // remove user's listener Inlay kids[] = getKids(); for(int i=0; i<kids.length; i++) // make nanny stop watching the kids kids[i].highlightListeners.remove(nanny); // done forcefully. oh well. } private void fireHighlightChanged(Inlay whose) { for(Iterator it = highlightListeners.iterator(); it.hasNext(); ) { HighlightListener listener = (HighlightListener)it.next(); if(whose == this || listener.doRecurse()) listener.highlightChanged(whose); } } /** * returns an array containing all the highlighted Inlay components * from the called instance, downward, optionally including all * highlighted descendants. * @param recurse specifies whether to include descendants other than * just the direct children. */ public Inlay[] getHighlighted(boolean recurse) { final ArrayList highlighted = new ArrayList(); walkTree(new TreeWalker() { public void visit(Inlay node, int level) { if(node.isHighlighted()) highlighted.add(node); } }, recurse); return (Inlay[])highlighted.toArray(new Inlay[0]); } /** * used internally to listen to child Inlays and report their changes * to listeners of their parent (i.e. listeners to "this"). */ private HighlightListener nanny = new HighlightAdapter() { public void highlightChanged(Inlay whose) { fireHighlightChanged(whose); } }; ///////////////////// // UTILITY METHODS // ///////////////////// /** * returns a component which attempts to spread to fill available space * along the horizontal axis, and takes up a finite space (thickness) * along the vertical axis. Essentially a combinition of a strut * in one direction and glue in the other. * Note: do not use in scroll panels or they will expand indefinitely. * @param thickness gives the size of the component's "strutness" * along the vertical axis. */ public static Component createHorizontalSpreaderBar(int thickness) { return new Box.Filler( new Dimension(0, thickness), // minimum size new Dimension(0, thickness), // preferred size new Dimension(Short.MAX_VALUE, thickness) { // maximum size public String toString() { return "horizontal spreader"; } }); } /** * returns a component which attempts to spread to fill available space * along the vertical axis, and takes up a finite space (thickness) * along the horizontal axis. Essentially a combinition of a strut * in one direction and glue in the other. * Note: do not use in scroll panels or they will expand indefinitely. * @param thickness gives the size of the component's "strutness" * along the horizontal axis. */ public static Component createVerticalSpreaderBar(int thickness) { return new Box.Filler( new Dimension(thickness, 0), // minimum size new Dimension(thickness, 0), // preferred size new Dimension(thickness, Short.MAX_VALUE) { // maximum size public String toString() { return "vertical spreader"; } }); } /** * like HorizontalSpreaderBar but which has an unlimited preferred size * along the horizontal axis. In this sense it's very similar to * glue but stronger since glue only has an unlimited maximum size * whereas this one has an unlimited <i>preferred</i> size. * When competing with normal glue, super glue wins. That makes it possible * to use super glue to "take up the slack" in areas where normal glue * has caused components to expand too far into unoccupied space. * Note: do not use in scroll panels or they will expand indefinitely. */ public static Component createHorizontalSuperGlue() { return new Box.Filler( new Dimension(0, 0), // minimum size new Dimension(Short.MAX_VALUE, 0), // preferred size new Dimension(Short.MAX_VALUE, 0) { // maximum size public String toString() { return "horizontal superglue"; } }); } /** * like VerticalSpreaderBar but which has an unlimited preferred size * along the vertical axis. In this sense it's very similar to * glue but stronger since glue only has an unlimited maximum size * whereas this one has an unlimited <i>preferred</i> size. * When competing with normal glue, super glue wins. That makes it possible * to use super glue to "take up the slack" in areas where normal glue * has caused components to expand too far into unoccupied space. * Note: do not use in scroll panels or they will expand indefinitely. */ public static Component createVerticalSuperGlue() { return new Box.Filler( new Dimension(0, 0), // minimum size new Dimension(0, Short.MAX_VALUE), // preferred size new Dimension(0, Short.MAX_VALUE) { // maximum size public String toString() { return "vertical superglue"; } }); } /** * returns a component which takes up PAD_PIXELS in the layout direction. */ private Component space() { // Implementation uses a spreader bar rather than a simple strut or // rigid area in order to expand the containers to fill available space // in the non layout direction. If that is later decided to not be // desired, then simply use the commented out use of rigid area below. return layoutDirection == BoxLayout.X_AXIS ? Inlay.createVerticalSpreaderBar(PAD_PIXELS) : Inlay.createHorizontalSpreaderBar(PAD_PIXELS); //return Box.createRigidArea(new Dimension(PAD_PIXELS, PAD_PIXELS)); } /** * static utility tells whether all given Inlays have the same Inlay parent. */ public static boolean haveCommonInlayParent(Inlay inlays[]) { if(inlays.length < 2) return true; if( ! (inlays[0].getParent() instanceof Inlay)) return false; Inlay parent0 = (Inlay)inlays[0].getParent(); for(int i=0; i<inlays.length; i++) { if(inlays[i].getParent() != parent0) return false; // not all highlighted inlays have a common parent } return true; } //////////////////// // TREE TRAVERSAL // //////////////////// /** * for use by walkTree method. * objects implementing this interface and passed to walkTree * will have their visit method called for each Inlay in a * (possibly nested) Inlay tree. every Inlay node encountered * will be visited in depth-first order. */ public interface TreeWalker { /** * callback method which reports an Inlay instance in a tree. * @param node is the Inlay being reported. * @param level is the depth of the node being reported * where zero denotes the root (i.e. the original Inlay instance * the walkTree method was invoked on.) */ public void visit(Inlay node, int level); } /** * walks this Inlay object and all its Inlay children. * the walker's visit method is called for each Inlay node * in the tree rooted at the instance this method is called on. * nodes are visited in depth-first order. The walker object * may safely edit the tree during traversal, but callers should * be aware that changes to the node containing a node that is * currently being walked will not be noticed by the current * walkTree invocation, though changes to its descendants will. */ public void walkTree(TreeWalker walker, boolean recurse) { walkTree(walker, recurse, 0); } /** * convienience version which always recurses. */ public void walkTree(TreeWalker walker) { walkTree(walker, true, 0); } /** * the private recursive version called from the public version * to initiate tree traversal. * the current Inlay plus each of its immediate children are always * visited, but if recurse==true, all children's descenedants are * also visited. */ private void walkTree(TreeWalker walker, boolean recurse, int level) { walker.visit(this, level); Inlay kids[] = getKids(); for(int i=0; i<kids.length; i++) kids[i].walkTree(walker, recurse, level+1); } /** * A simple example of using Inlays. */ private static void main(String args[]) { Inlay mainpanel = new Inlay(BoxLayout.Y_AXIS); Inlay subpanel = new Inlay(BoxLayout.Y_AXIS); subpanel.add(new JComboBox()); subpanel.add(new JComboBox()); mainpanel.add(subpanel); JPanel tmp = new JPanel(); tmp.setOpaque(false); tmp.add(new JComboBox(new String[] { "AND", "OR", })); tmp.setMaximumSize(new Dimension(8888, 30)); mainpanel.add(tmp); JLabel text = new JLabel("Click Me To Toggle Selection"); Inlay labelinlay = new Inlay(BoxLayout.Y_AXIS); text.setForeground(Color.black); labelinlay.add(text); mainpanel.add(labelinlay); mainpanel.addHighlightListener(new Inlay.HighlightAdapter() { public void highlightChanged(Inlay whose) { System.out.println("Inlay with " + whose.getKidCount() + " sub Inlays"); } }); JFrame frame = new JFrame("Inlay Example"); frame.getContentPane().add(mainpanel); frame.pack(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } } // end class Inlay