// TreeBrowser.java
// $Id: TreeBrowser.java,v 1.14 2000/08/16 21:37:57 ylafon Exp $ */
// Authors: Jean-Michel.Leon@sophia.inria.fr,
// Yves.Lafon@w3.org :
// - Lines, insert/remove, awt 1.1 version
// Thierry.Kormann@sophia.inria.fr
// - Insert debug, horizontal scrollbar, javadoc,
// selection graphic customization, scrollbar policy,
// lightweight version.
package org.w3c.tools.widgets ;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Scrollbar;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Enumeration;
import java.util.EventObject;
import java.util.Stack;
import java.util.Vector;
/**
* The TreeBrowser class.
*
* This class is a generic framework to browser any hierachical structure.
*
* <p>Genericity is obtained through the use of 'handlers': the TreeBrowser
* itself does not perform any action in response to user events, but simply
* forward them as <b>notifications</b> to <b>handlers</b>. Each item inserted
* may have its own handler, but handlers may also (this is the most common
* case) be shared between handlers.
*
* <p>Any item added in the Tree is displayed with an icon and a label. When a
* handler receive a notification on a node, it may change this node, to modify
* or update its appearance.
*
* @author Jean-Michel.Leon@sophia.inria.fr
* @author Yves.Lafon@w3.org
* @author Thierry.Kormann@sophia.inria.fr
*/
public class TreeBrowser extends Canvas implements AdjustmentListener {
/**
* Specifies that the horizontal/vertical scrollbars should always be shown
* regardless of the respective sizes of the TreeBrowser.
*/
public static final int SCROLLBARS_ALWAYS = 0;
/**
* Specifies that horizontal/vertical scrollbars should be shown only when
* the size of the nodes exceeds the size of the TreeBrowser in the
* horizontal/vertical dimension.
*/
public static final int SCROLLBARS_ASNEEDED = 1;
/**
* This policy that lets just one node selected at the same time.
*/
public static final int SINGLE = 0;
/**
* The policy that enables a multiple selection of nodes.
*/
public static final int MULTIPLE = 1;
static final int HMARGIN = 5;
static final int VMARGIN = 5;
static final int HGAP = 10;
static final int DXLEVEL = HGAP*2;
/**
* The inner mouse listener in charge of all the node expansion
* selection and execution
*/
private class BrowserMouseListener extends MouseAdapter {
private void clickAt(TreeNode node, MouseEvent me) {
if(node == null)
return;
int x = me.getX() - HMARGIN;
if(node.handler == null)
return;
// node.handler.notifyExpand(this, node);
if((x >= node.level*DXLEVEL) &&
(x <= node.level*DXLEVEL + DXLEVEL)) {
// click on expand/collapse button
if(node.children != TreeNode.NOCHILD) {
node.handler.notifyCollapse(TreeBrowser.this, node);
}
else {
node.handler.notifyExpand(TreeBrowser.this, node);
}
}
else if(x > node.level*DXLEVEL + HGAP) {
// item selection
node.handler.notifySelect(TreeBrowser.this, node);
}
}
/**
* Handles events and send notifications ot handlers.
* is sent, depending on the node's current state.<br>
* on MOUSE_DOWN on a label, a <b>Select</b> notificaiton is sent.<br>
* on DOUBLE_CLICK on a label, an <b>Execute</b> notification is sent.
*/
public void mousePressed(MouseEvent me) {
int y = me.getY() - VMARGIN;
if(me.getClickCount() == 1) {
clickAt(itemAt(y), me);
}
}
public void mouseClicked(MouseEvent me) {
if(me.getClickCount() > 1) {
int y = me.getY() - VMARGIN;
TreeNode node = itemAt(y);
if((node != null) && (node.handler != null)) {
node.handler.notifyExecute(TreeBrowser.this, node);
}
}
}
}
private Scrollbar vscroll;
private Scrollbar hscroll;
private int maxwidth = 0;
private int startx = 0;
private Color selectColor = new Color(0, 0, 128);
private Color selectFontColor = Color.white;
private int scrollbarDisplayPolicy = SCROLLBARS_ASNEEDED;
private boolean hierarchyChanged = true;
protected Vector items;
protected Vector selection;
protected int topItem = 0;
protected int visibleItemCount = 20;
protected int selectionPolicy = SINGLE;
protected int fontHeight;
/**
* Builds a new browser instance
*
* @param root the root node for this hierarchy
* @param label the label that should be displayed for this item
* @param handler the handler for this node
* @param icon the icon that must be displayed for this item
*/
public TreeBrowser(Object root, String label,
NodeHandler handler, Image icon) {
this();
initialize(root, label, handler, icon);
}
protected TreeBrowser() {
selection = new Vector(1, 1);
items = new Vector();
topItem = 0;
addMouseListener(new BrowserMouseListener());
}
protected void initialize(Object item,String label,
NodeHandler handler, Image icon) {
items.addElement(new TreeNode(item,label, handler, icon, 0));
}
public Dimension getPreferredSize() {
return new Dimension(200, 400);
}
/**
* Sets the color of a selected node to the specified color.
* @param color the color used to paint a selected node
*/
public void setSelectionFontColor(Color color) {
this.selectFontColor = color;
}
/**
* Sets the background color of a selected node to the specified color.
* @param color the color used to paint the background of a selected node
*/
public void setSelectionBackgroudColor(Color color) {
this.selectColor = color;
}
/**
* Sets the scrollbars display policy to the specified policy. The default
* is SCROLLBARS_ALWAYS
* @param scrollbarDisplayPolicy SCROLLBARS_NEVER | SCROLLBARS_ASNEEDED |
* SCROLLBARS_ALWAYS
*/
public void setScrollbarDisplayPolicy(int scrollbarDisplayPolicy) {
this.scrollbarDisplayPolicy = scrollbarDisplayPolicy;
hierarchyChanged = false;
}
/**
* repaints the View.
*/
public void paint(Graphics g) {
fontHeight = g.getFontMetrics().getHeight();
int fontAscent = g.getFontMetrics().getAscent();
int itemCount = items.size();
Dimension dim = getSize();
int myHeight = dim.height-VMARGIN*2;
int myWidth = dim.width-HMARGIN*2;
g.clipRect(HMARGIN, VMARGIN, myWidth, myHeight);
g.translate(HMARGIN, VMARGIN);
int y = 0;
int dx, fatherIndex;
int level;
Stack indexStack = new Stack();
Graphics bg = g.create();
bg.setColor(selectColor);
g.setFont(getFont());
visibleItemCount = 0;
TreeNode node;
level = -1;
int labelwidth;
if (hierarchyChanged) {
maxwidth = 0;
}
// we push the indexes of the inner levels to speed up things
for(int i = 0; i < topItem; i++) {
node = (TreeNode) items.elementAt(i);
// hscroll
if (hierarchyChanged) {
dx = node.level * DXLEVEL;
labelwidth = g.getFontMetrics().stringWidth(node.label);
maxwidth = Math.max(maxwidth, dx + DXLEVEL + labelwidth);
}
if(node.level > level) {
indexStack.push(new Integer(i-1));
level = node.level;
}
if(node.level < level) {
for(int j=node.level; j<level; j++)
indexStack.pop();
level = node.level;
}
}
int nitems = myHeight/fontHeight;
int ditems = itemCount - topItem;
if (ditems < nitems) {
topItem = Math.max(0, topItem-(nitems-ditems));
}
if (myWidth >= maxwidth) {
startx = 0;
} else if (startx+myWidth > maxwidth) {
startx = (maxwidth - myWidth);
}
for(int i = topItem; i < itemCount ; i++) {
node = (TreeNode) items.elementAt(i);
if(node.level > level) {
indexStack.push(new Integer(i-1));
level = node.level;
}
if(node.level < level) {
for(int j=node.level; j<level; j++)
indexStack.pop();
level = node.level;
}
dx = (node.level * DXLEVEL)-startx;
if(y <= myHeight) {
if(node.selected) {
bg.fillRect(dx, y-1,
Math.max(myWidth-1, maxwidth-1), fontHeight);
g.setColor(selectFontColor);
g.drawImage(node.icon, dx, y, this);
g.drawString(node.label, dx + DXLEVEL, y+fontAscent);
g.setColor(getForeground());
} else {
g.setColor(getForeground());
g.drawImage(node.icon, dx, y, this);
g.drawString(node.label, dx + DXLEVEL, y+fontAscent);
}
fatherIndex = ((Integer) indexStack.peek()).intValue();
if( fatherIndex != -1) { // draw fancy lines
int fi = fatherIndex - topItem;
g.drawLine(dx - HGAP/2 , y + fontHeight/2,
dx - DXLEVEL + HGAP/2, y + fontHeight/2);
if(node.handler.isDirectory(this, node)) {
g.drawRect(dx - DXLEVEL + HGAP/2 -2,
y + fontHeight/2 - 2,
4, 4);
}
g.drawLine(dx-DXLEVEL + HGAP/2, y + fontHeight/2,
dx-DXLEVEL + HGAP/2, (fi+1)*fontHeight - 1);
}
visibleItemCount++;
} else { // draw the lines for invisible nodes.
fatherIndex = ((Integer)indexStack.peek()).intValue();
if(fatherIndex != -1) {
int fi = fatherIndex - topItem;
if((fi+1)*fontHeight -1 < myHeight)
g.drawLine(dx - DXLEVEL + HGAP/2, myHeight-1,
dx - DXLEVEL + HGAP/2, (fi+1)*fontHeight-1);
}
}
// hscroll
if (hierarchyChanged) {
dx = (node.level * DXLEVEL);
labelwidth = g.getFontMetrics().stringWidth(node.label);
maxwidth = Math.max(maxwidth, dx + DXLEVEL + labelwidth);
}
y += fontHeight;
}
// hscroll
if (hierarchyChanged) {
for (int i=itemCount; i < items.size(); ++i) {
node = (TreeNode) items.elementAt(i);
dx = (node.level * DXLEVEL);
labelwidth = g.getFontMetrics().stringWidth(node.label);
maxwidth = Math.max(maxwidth, dx + DXLEVEL + labelwidth);
}
}
hierarchyChanged = false;
updateScrollbars();
}
/**
* this should be private. having it protected is a present
* for dummy VM that doesn't know that an inner class can access
* private method of its parent class
*/
protected TreeNode itemAt(int y) {
for(int i = topItem; ((i < items.size()) && (y >0)); i++) {
if(y < fontHeight) {
return (TreeNode) items.elementAt(i);
}
y -= fontHeight;
}
return null;
}
public void update(Graphics pg) {
Rectangle r = pg.getClipBounds();
Graphics offgc;
Image offscreen = null;
Dimension d = getSize();
// create the offscreen buffer and associated Graphics
offscreen = ImageCache.getImage(this, d.width, d.height);
offgc = offscreen.getGraphics();
if(r != null) {
offgc.clipRect(r.x, r.y, r.width, r.height);
}
// clear the exposed area
offgc.setColor(getBackground());
offgc.fillRect(0, 0, d.width, d.height);
offgc.setColor(getForeground());
// do normal redraw
paint(offgc);
// transfer offscreen to window
pg.drawImage(offscreen, 0, 0, this);
}
/**
* Inserts new node.
*
* @param parent the parent node.
* @item the abstract object this node refers to. may be null.
* @handler the node handler, that will receive notifications for this node
* @label the label displayed in the list.
* @icon the icon displayed in the list.
*/
public void insert(TreeNode parent, Object item, NodeHandler handler,
String label, Image icon) {
boolean done;
int j;
if(parent == null) throw new IllegalArgumentException("null parent");
if((handler == null) && (label == null)) {
throw new IllegalArgumentException("non-null item required");
}
if(handler == null) {
handler = parent.handler;
}
if(label == null) {
label = handler.toString();
}
if(parent.children == TreeNode.NOCHILD) {
parent.children = 1;
} else {
parent.children += 1;
}
done = false;
TreeNode node = null;
int i = items.indexOf(parent)+parent.children;
for (; (i < items.size() &&
((TreeNode) items.elementAt(i)).level > parent.level);
i++) {}
items.insertElementAt(node=new TreeNode(item, label, handler, icon,
parent.level+1), i);
// hscroll
hierarchyChanged = true;
return;
}
/**
* Removes the specified node.
* This simply removes a node, without modifying its children if any. USE
* WITH CAUTION.
* @param node the node to remove
*/
public void remove(TreeNode node) {
int ind = items.indexOf(node);
TreeNode t = null;
while (ind>=0) {
t = (TreeNode) items.elementAt(ind);
if (t.level >= node.level)
ind--;
else {
t.children--;
break;
}
}
items.removeElement(node);
if(node.selected) {
unselect(node);
}
// hscroll
hierarchyChanged = true;
}
/**
* Removes the specified node and its children.
* NOTE: if two threads are doing adds and removes,
* this can lead to IndexOutOfBound exception.
* You will probably have to use locks to get rid of that problem
* @param node the node to remove
*/
public void removeBranch(TreeNode node) {
int ist, iend;
ist = items.indexOf(node)+1;
iend = items.size()-1;
for(int i = ist; i< iend; i++) {
if(((TreeNode)items.elementAt(ist)).level > node.level) {
remove((TreeNode)items.elementAt(ist));
} else
break;
}
remove(node);
// hscroll
hierarchyChanged = true;
}
/**
* Contracts the representation of the specified node.
* removes all the children nodes of 'item'. It is caller's
* responsibility to call repaint() afterwards.
* @param item the node to contracts
*/
public synchronized void collapse(TreeNode item) {
TreeNode node = (TreeNode)item;
if(node.children != TreeNode.NOCHILD) {
node.children = TreeNode.NOCHILD;
for(int j = items.indexOf(item)+1; j <items.size(); /*nothing*/) {
TreeNode child = (TreeNode)items.elementAt(j);
if(child.level > node.level) {
items.removeElementAt(j);
if(child.selected) {
unselect(child);
}
}
else {
// hscroll
hierarchyChanged = true;
// last children reached, exit
return;
}
}
}
}
/**
* Sets the selection policy.
* @param policy: SINGLE or MULTIPLE
*/
public void setSelectionPolicy(int policy) {
selectionPolicy = policy;
}
/**
* Gets the selection policy.
*/
public int getSelectionPolicy() {
return selectionPolicy;
}
/**
* Selects the specified node.
* Selects the given node. If selectionPolicy is SINGLE any previously
* selected node is unselected first. It is caller's responsibility to
* call repaint()
* @param node the node to select
*/
public void select(TreeNode node) {
if(node == null) return;
if(selectionPolicy == SINGLE) {
unselectAll();
}
selection.addElement(node);
node.selected = true;
}
/**
* Unselects the specified node.
* It is caller's responsibility to call repaint()
* @param node the node to unselect
*/
public void unselect(TreeNode node) {
if(node == null) return;
selection.removeElement(node);
node.selected = false;
}
/**
* Unselects all selected items.
*/
public void unselectAll() {
for(Enumeration e = selection.elements(); e.hasMoreElements(); ) {
TreeNode node = (TreeNode)e.nextElement();
node.selected = false;
}
}
/**
* Returns an Enumeraiton of selected items.
*/
public Enumeration selection() {
return selection.elements();
}
private void updateScrollbars() {
int max = items.size()+1;
if(items.size() > visibleItemCount) {
vscroll.setMaximum(max);
vscroll.setVisibleAmount(visibleItemCount);
vscroll.setVisible(true);
} else {
vscroll.setValue(0);
vscroll.setMaximum(max);
vscroll.setVisibleAmount(max);
if (scrollbarDisplayPolicy == SCROLLBARS_ASNEEDED) {
vscroll.setVisible(false);
}
}
int myWidth = getSize().width-HMARGIN*2;
hscroll.setMaximum(maxwidth);
hscroll.setVisibleAmount(myWidth);
if (maxwidth > myWidth) {
hscroll.setVisible(true);
} else {
if (scrollbarDisplayPolicy == SCROLLBARS_ASNEEDED) {
hscroll.setVisible(false);
}
}
}
/**
* Sets 'a' as vertical Scrollbar.
* The Browser becomes an AdjustmentListener of this scrollbar.
*/
public void setVerticalScrollbar(Scrollbar a) {
vscroll = a;
vscroll.addAdjustmentListener(this);
vscroll.setMaximum(visibleItemCount);
vscroll.setVisibleAmount(visibleItemCount);
vscroll.setBlockIncrement(visibleItemCount);
}
/**
* Sets 'a' as horizontal Scrollbar.
* The Browser becomes an AdjustmentListener of this scrollbar.
*/
public void setHorizontalScrollbar(Scrollbar a) {
hscroll = a;
hscroll.addAdjustmentListener(this);
int myWidth = getSize().width-HMARGIN*2;
hscroll.setMaximum(myWidth);
hscroll.setVisibleAmount(myWidth);
hscroll.setBlockIncrement(20);
}
/**
* Updates graphical appearance in response to a scroll.
*/
public void adjustmentValueChanged(AdjustmentEvent evt) {
if (evt.getSource() == vscroll) {
topItem = evt.getValue();
} else {
startx = evt.getValue();
}
repaint();
}
/**
* Returns the parent node of the specified node.
* If 'child' is a valid node belonging to the Tree and has a parent node,
* returns its parent. Returns null otherwise.
* @param child the child node you want to get its parent
*/
public TreeNode getParent(TreeNode child) {
int n = items.indexOf(child);
for(int i = n-1; i >= 0; i--) {
TreeNode node = (TreeNode)(items.elementAt(i));
if(node.level < child.level) {
return node;
}
}
return null;
}
/**
* Gets the node associated to the specified object, or null if any.
* @param obj the object related to a node
*/
public TreeNode getNode(Object obj) {
int imax = items.size();
for(int i=0; i < imax; i++) {
if(obj.equals(((TreeNode)(items.elementAt(i))).getItem()))
return (TreeNode)(items.elementAt(i));
}
return null;
}
}