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