// 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