001: // TreeBrowser.java
002: // $Id: TreeBrowser.java,v 1.14 2000/08/16 21:37:57 ylafon Exp $ */
003: // Authors: Jean-Michel.Leon@sophia.inria.fr,
004: // Yves.Lafon@w3.org :
005: // - Lines, insert/remove, awt 1.1 version
006: // Thierry.Kormann@sophia.inria.fr
007: // - Insert debug, horizontal scrollbar, javadoc,
008: // selection graphic customization, scrollbar policy,
009: // lightweight version.
010:
011: package org.w3c.tools.widgets;
012:
013: import java.awt.Canvas;
014: import java.awt.Color;
015: import java.awt.Component;
016: import java.awt.Dimension;
017: import java.awt.FontMetrics;
018: import java.awt.Graphics;
019: import java.awt.Image;
020: import java.awt.Rectangle;
021: import java.awt.Scrollbar;
022:
023: import java.awt.event.AdjustmentEvent;
024: import java.awt.event.AdjustmentListener;
025: import java.awt.event.MouseAdapter;
026: import java.awt.event.MouseEvent;
027:
028: import java.util.Enumeration;
029: import java.util.EventObject;
030: import java.util.Stack;
031: import java.util.Vector;
032:
033: /**
034: * The TreeBrowser class.
035: *
036: * This class is a generic framework to browser any hierachical structure.
037: *
038: * <p>Genericity is obtained through the use of 'handlers': the TreeBrowser
039: * itself does not perform any action in response to user events, but simply
040: * forward them as <b>notifications</b> to <b>handlers</b>. Each item inserted
041: * may have its own handler, but handlers may also (this is the most common
042: * case) be shared between handlers.
043: *
044: * <p>Any item added in the Tree is displayed with an icon and a label. When a
045: * handler receive a notification on a node, it may change this node, to modify
046: * or update its appearance.
047: *
048: * @author Jean-Michel.Leon@sophia.inria.fr
049: * @author Yves.Lafon@w3.org
050: * @author Thierry.Kormann@sophia.inria.fr
051: */
052: public class TreeBrowser extends Canvas implements AdjustmentListener {
053:
054: /**
055: * Specifies that the horizontal/vertical scrollbars should always be shown
056: * regardless of the respective sizes of the TreeBrowser.
057: */
058: public static final int SCROLLBARS_ALWAYS = 0;
059: /**
060: * Specifies that horizontal/vertical scrollbars should be shown only when
061: * the size of the nodes exceeds the size of the TreeBrowser in the
062: * horizontal/vertical dimension.
063: */
064: public static final int SCROLLBARS_ASNEEDED = 1;
065: /**
066: * This policy that lets just one node selected at the same time.
067: */
068: public static final int SINGLE = 0;
069: /**
070: * The policy that enables a multiple selection of nodes.
071: */
072: public static final int MULTIPLE = 1;
073:
074: static final int HMARGIN = 5;
075: static final int VMARGIN = 5;
076: static final int HGAP = 10;
077: static final int DXLEVEL = HGAP * 2;
078:
079: /**
080: * The inner mouse listener in charge of all the node expansion
081: * selection and execution
082: */
083: private class BrowserMouseListener extends MouseAdapter {
084:
085: private void clickAt(TreeNode node, MouseEvent me) {
086: if (node == null)
087: return;
088: int x = me.getX() - HMARGIN;
089: if (node.handler == null)
090: return;
091: // node.handler.notifyExpand(this, node);
092: if ((x >= node.level * DXLEVEL)
093: && (x <= node.level * DXLEVEL + DXLEVEL)) {
094: // click on expand/collapse button
095: if (node.children != TreeNode.NOCHILD) {
096: node.handler.notifyCollapse(TreeBrowser.this , node);
097: } else {
098: node.handler.notifyExpand(TreeBrowser.this , node);
099: }
100: } else if (x > node.level * DXLEVEL + HGAP) {
101: // item selection
102: node.handler.notifySelect(TreeBrowser.this , node);
103: }
104: }
105:
106: /**
107: * Handles events and send notifications ot handlers.
108: * is sent, depending on the node's current state.<br>
109: * on MOUSE_DOWN on a label, a <b>Select</b> notificaiton is sent.<br>
110: * on DOUBLE_CLICK on a label, an <b>Execute</b> notification is sent.
111: */
112: public void mousePressed(MouseEvent me) {
113: int y = me.getY() - VMARGIN;
114: if (me.getClickCount() == 1) {
115: clickAt(itemAt(y), me);
116: }
117: }
118:
119: public void mouseClicked(MouseEvent me) {
120: if (me.getClickCount() > 1) {
121: int y = me.getY() - VMARGIN;
122: TreeNode node = itemAt(y);
123: if ((node != null) && (node.handler != null)) {
124: node.handler.notifyExecute(TreeBrowser.this , node);
125: }
126: }
127: }
128: }
129:
130: private Scrollbar vscroll;
131: private Scrollbar hscroll;
132: private int maxwidth = 0;
133: private int startx = 0;
134: private Color selectColor = new Color(0, 0, 128);
135: private Color selectFontColor = Color.white;
136: private int scrollbarDisplayPolicy = SCROLLBARS_ASNEEDED;
137: private boolean hierarchyChanged = true;
138:
139: protected Vector items;
140: protected Vector selection;
141: protected int topItem = 0;
142: protected int visibleItemCount = 20;
143: protected int selectionPolicy = SINGLE;
144: protected int fontHeight;
145:
146: /**
147: * Builds a new browser instance
148: *
149: * @param root the root node for this hierarchy
150: * @param label the label that should be displayed for this item
151: * @param handler the handler for this node
152: * @param icon the icon that must be displayed for this item
153: */
154: public TreeBrowser(Object root, String label, NodeHandler handler,
155: Image icon) {
156: this ();
157: initialize(root, label, handler, icon);
158: }
159:
160: protected TreeBrowser() {
161: selection = new Vector(1, 1);
162: items = new Vector();
163: topItem = 0;
164: addMouseListener(new BrowserMouseListener());
165: }
166:
167: protected void initialize(Object item, String label,
168: NodeHandler handler, Image icon) {
169: items.addElement(new TreeNode(item, label, handler, icon, 0));
170: }
171:
172: public Dimension getPreferredSize() {
173: return new Dimension(200, 400);
174: }
175:
176: /**
177: * Sets the color of a selected node to the specified color.
178: * @param color the color used to paint a selected node
179: */
180: public void setSelectionFontColor(Color color) {
181: this .selectFontColor = color;
182: }
183:
184: /**
185: * Sets the background color of a selected node to the specified color.
186: * @param color the color used to paint the background of a selected node
187: */
188: public void setSelectionBackgroudColor(Color color) {
189: this .selectColor = color;
190: }
191:
192: /**
193: * Sets the scrollbars display policy to the specified policy. The default
194: * is SCROLLBARS_ALWAYS
195: * @param scrollbarDisplayPolicy SCROLLBARS_NEVER | SCROLLBARS_ASNEEDED |
196: * SCROLLBARS_ALWAYS
197: */
198: public void setScrollbarDisplayPolicy(int scrollbarDisplayPolicy) {
199: this .scrollbarDisplayPolicy = scrollbarDisplayPolicy;
200: hierarchyChanged = false;
201: }
202:
203: /**
204: * repaints the View.
205: */
206: public void paint(Graphics g) {
207: fontHeight = g.getFontMetrics().getHeight();
208: int fontAscent = g.getFontMetrics().getAscent();
209: int itemCount = items.size();
210:
211: Dimension dim = getSize();
212: int myHeight = dim.height - VMARGIN * 2;
213: int myWidth = dim.width - HMARGIN * 2;
214:
215: g.clipRect(HMARGIN, VMARGIN, myWidth, myHeight);
216: g.translate(HMARGIN, VMARGIN);
217:
218: int y = 0;
219: int dx, fatherIndex;
220: int level;
221:
222: Stack indexStack = new Stack();
223: Graphics bg = g.create();
224: bg.setColor(selectColor);
225: g.setFont(getFont());
226: visibleItemCount = 0;
227: TreeNode node;
228: level = -1;
229:
230: int labelwidth;
231: if (hierarchyChanged) {
232: maxwidth = 0;
233: }
234:
235: // we push the indexes of the inner levels to speed up things
236: for (int i = 0; i < topItem; i++) {
237: node = (TreeNode) items.elementAt(i);
238: // hscroll
239: if (hierarchyChanged) {
240: dx = node.level * DXLEVEL;
241: labelwidth = g.getFontMetrics().stringWidth(node.label);
242: maxwidth = Math
243: .max(maxwidth, dx + DXLEVEL + labelwidth);
244: }
245:
246: if (node.level > level) {
247: indexStack.push(new Integer(i - 1));
248: level = node.level;
249: }
250: if (node.level < level) {
251: for (int j = node.level; j < level; j++)
252: indexStack.pop();
253: level = node.level;
254: }
255: }
256:
257: int nitems = myHeight / fontHeight;
258: int ditems = itemCount - topItem;
259: if (ditems < nitems) {
260: topItem = Math.max(0, topItem - (nitems - ditems));
261: }
262: if (myWidth >= maxwidth) {
263: startx = 0;
264: } else if (startx + myWidth > maxwidth) {
265: startx = (maxwidth - myWidth);
266: }
267:
268: for (int i = topItem; i < itemCount; i++) {
269: node = (TreeNode) items.elementAt(i);
270: if (node.level > level) {
271: indexStack.push(new Integer(i - 1));
272: level = node.level;
273: }
274: if (node.level < level) {
275: for (int j = node.level; j < level; j++)
276: indexStack.pop();
277: level = node.level;
278: }
279:
280: dx = (node.level * DXLEVEL) - startx;
281: if (y <= myHeight) {
282: if (node.selected) {
283: bg.fillRect(dx, y - 1, Math.max(myWidth - 1,
284: maxwidth - 1), fontHeight);
285: g.setColor(selectFontColor);
286: g.drawImage(node.icon, dx, y, this );
287: g.drawString(node.label, dx + DXLEVEL, y
288: + fontAscent);
289: g.setColor(getForeground());
290: } else {
291: g.setColor(getForeground());
292: g.drawImage(node.icon, dx, y, this );
293: g.drawString(node.label, dx + DXLEVEL, y
294: + fontAscent);
295: }
296:
297: fatherIndex = ((Integer) indexStack.peek()).intValue();
298: if (fatherIndex != -1) { // draw fancy lines
299: int fi = fatherIndex - topItem;
300: g.drawLine(dx - HGAP / 2, y + fontHeight / 2, dx
301: - DXLEVEL + HGAP / 2, y + fontHeight / 2);
302:
303: if (node.handler.isDirectory(this , node)) {
304: g.drawRect(dx - DXLEVEL + HGAP / 2 - 2, y
305: + fontHeight / 2 - 2, 4, 4);
306: }
307: g.drawLine(dx - DXLEVEL + HGAP / 2, y + fontHeight
308: / 2, dx - DXLEVEL + HGAP / 2, (fi + 1)
309: * fontHeight - 1);
310: }
311: visibleItemCount++;
312: } else { // draw the lines for invisible nodes.
313: fatherIndex = ((Integer) indexStack.peek()).intValue();
314: if (fatherIndex != -1) {
315: int fi = fatherIndex - topItem;
316: if ((fi + 1) * fontHeight - 1 < myHeight)
317: g.drawLine(dx - DXLEVEL + HGAP / 2,
318: myHeight - 1, dx - DXLEVEL + HGAP / 2,
319: (fi + 1) * fontHeight - 1);
320: }
321: }
322: // hscroll
323: if (hierarchyChanged) {
324: dx = (node.level * DXLEVEL);
325: labelwidth = g.getFontMetrics().stringWidth(node.label);
326: maxwidth = Math
327: .max(maxwidth, dx + DXLEVEL + labelwidth);
328: }
329: y += fontHeight;
330: }
331:
332: // hscroll
333: if (hierarchyChanged) {
334: for (int i = itemCount; i < items.size(); ++i) {
335: node = (TreeNode) items.elementAt(i);
336: dx = (node.level * DXLEVEL);
337: labelwidth = g.getFontMetrics().stringWidth(node.label);
338: maxwidth = Math
339: .max(maxwidth, dx + DXLEVEL + labelwidth);
340: }
341: }
342: hierarchyChanged = false;
343: updateScrollbars();
344: }
345:
346: /**
347: * this should be private. having it protected is a present
348: * for dummy VM that doesn't know that an inner class can access
349: * private method of its parent class
350: */
351:
352: protected TreeNode itemAt(int y) {
353: for (int i = topItem; ((i < items.size()) && (y > 0)); i++) {
354: if (y < fontHeight) {
355: return (TreeNode) items.elementAt(i);
356: }
357: y -= fontHeight;
358: }
359: return null;
360: }
361:
362: public void update(Graphics pg) {
363: Rectangle r = pg.getClipBounds();
364: Graphics offgc;
365: Image offscreen = null;
366: Dimension d = getSize();
367:
368: // create the offscreen buffer and associated Graphics
369: offscreen = ImageCache.getImage(this , d.width, d.height);
370: offgc = offscreen.getGraphics();
371: if (r != null) {
372: offgc.clipRect(r.x, r.y, r.width, r.height);
373: }
374: // clear the exposed area
375: offgc.setColor(getBackground());
376: offgc.fillRect(0, 0, d.width, d.height);
377: offgc.setColor(getForeground());
378: // do normal redraw
379: paint(offgc);
380: // transfer offscreen to window
381: pg.drawImage(offscreen, 0, 0, this );
382:
383: }
384:
385: /**
386: * Inserts new node.
387: *
388: * @param parent the parent node.
389: * @item the abstract object this node refers to. may be null.
390: * @handler the node handler, that will receive notifications for this node
391: * @label the label displayed in the list.
392: * @icon the icon displayed in the list.
393: */
394: public void insert(TreeNode parent, Object item,
395: NodeHandler handler, String label, Image icon) {
396: boolean done;
397: int j;
398: if (parent == null)
399: throw new IllegalArgumentException("null parent");
400: if ((handler == null) && (label == null)) {
401: throw new IllegalArgumentException("non-null item required");
402: }
403: if (handler == null) {
404: handler = parent.handler;
405: }
406: if (label == null) {
407: label = handler.toString();
408: }
409: if (parent.children == TreeNode.NOCHILD) {
410: parent.children = 1;
411: } else {
412: parent.children += 1;
413: }
414: done = false;
415: TreeNode node = null;
416:
417: int i = items.indexOf(parent) + parent.children;
418: for (; (i < items.size() && ((TreeNode) items.elementAt(i)).level > parent.level); i++) {
419: }
420: items.insertElementAt(node = new TreeNode(item, label, handler,
421: icon, parent.level + 1), i);
422: // hscroll
423: hierarchyChanged = true;
424: return;
425: }
426:
427: /**
428: * Removes the specified node.
429: * This simply removes a node, without modifying its children if any. USE
430: * WITH CAUTION.
431: * @param node the node to remove
432: */
433: public void remove(TreeNode node) {
434: int ind = items.indexOf(node);
435: TreeNode t = null;
436:
437: while (ind >= 0) {
438: t = (TreeNode) items.elementAt(ind);
439: if (t.level >= node.level)
440: ind--;
441: else {
442: t.children--;
443: break;
444: }
445: }
446: items.removeElement(node);
447:
448: if (node.selected) {
449: unselect(node);
450: }
451: // hscroll
452: hierarchyChanged = true;
453: }
454:
455: /**
456: * Removes the specified node and its children.
457: * NOTE: if two threads are doing adds and removes,
458: * this can lead to IndexOutOfBound exception.
459: * You will probably have to use locks to get rid of that problem
460: * @param node the node to remove
461: */
462: public void removeBranch(TreeNode node) {
463: int ist, iend;
464:
465: ist = items.indexOf(node) + 1;
466: iend = items.size() - 1;
467:
468: for (int i = ist; i < iend; i++) {
469: if (((TreeNode) items.elementAt(ist)).level > node.level) {
470: remove((TreeNode) items.elementAt(ist));
471: } else
472: break;
473: }
474: remove(node);
475: // hscroll
476: hierarchyChanged = true;
477: }
478:
479: /**
480: * Contracts the representation of the specified node.
481: * removes all the children nodes of 'item'. It is caller's
482: * responsibility to call repaint() afterwards.
483: * @param item the node to contracts
484: */
485: public synchronized void collapse(TreeNode item) {
486: TreeNode node = (TreeNode) item;
487: if (node.children != TreeNode.NOCHILD) {
488: node.children = TreeNode.NOCHILD;
489: for (int j = items.indexOf(item) + 1; j < items.size(); /*nothing*/) {
490: TreeNode child = (TreeNode) items.elementAt(j);
491: if (child.level > node.level) {
492: items.removeElementAt(j);
493: if (child.selected) {
494: unselect(child);
495: }
496: } else {
497: // hscroll
498: hierarchyChanged = true;
499: // last children reached, exit
500: return;
501: }
502: }
503: }
504: }
505:
506: /**
507: * Sets the selection policy.
508: * @param policy: SINGLE or MULTIPLE
509: */
510: public void setSelectionPolicy(int policy) {
511: selectionPolicy = policy;
512: }
513:
514: /**
515: * Gets the selection policy.
516: */
517: public int getSelectionPolicy() {
518: return selectionPolicy;
519: }
520:
521: /**
522: * Selects the specified node.
523: * Selects the given node. If selectionPolicy is SINGLE any previously
524: * selected node is unselected first. It is caller's responsibility to
525: * call repaint()
526: * @param node the node to select
527: */
528: public void select(TreeNode node) {
529: if (node == null)
530: return;
531: if (selectionPolicy == SINGLE) {
532: unselectAll();
533: }
534: selection.addElement(node);
535: node.selected = true;
536: }
537:
538: /**
539: * Unselects the specified node.
540: * It is caller's responsibility to call repaint()
541: * @param node the node to unselect
542: */
543: public void unselect(TreeNode node) {
544: if (node == null)
545: return;
546: selection.removeElement(node);
547: node.selected = false;
548: }
549:
550: /**
551: * Unselects all selected items.
552: */
553: public void unselectAll() {
554: for (Enumeration e = selection.elements(); e.hasMoreElements();) {
555: TreeNode node = (TreeNode) e.nextElement();
556: node.selected = false;
557: }
558: }
559:
560: /**
561: * Returns an Enumeraiton of selected items.
562: */
563: public Enumeration selection() {
564: return selection.elements();
565: }
566:
567: private void updateScrollbars() {
568: int max = items.size() + 1;
569: if (items.size() > visibleItemCount) {
570: vscroll.setMaximum(max);
571: vscroll.setVisibleAmount(visibleItemCount);
572: vscroll.setVisible(true);
573: } else {
574: vscroll.setValue(0);
575: vscroll.setMaximum(max);
576: vscroll.setVisibleAmount(max);
577: if (scrollbarDisplayPolicy == SCROLLBARS_ASNEEDED) {
578: vscroll.setVisible(false);
579: }
580: }
581:
582: int myWidth = getSize().width - HMARGIN * 2;
583: hscroll.setMaximum(maxwidth);
584: hscroll.setVisibleAmount(myWidth);
585: if (maxwidth > myWidth) {
586: hscroll.setVisible(true);
587: } else {
588: if (scrollbarDisplayPolicy == SCROLLBARS_ASNEEDED) {
589: hscroll.setVisible(false);
590: }
591: }
592: }
593:
594: /**
595: * Sets 'a' as vertical Scrollbar.
596: * The Browser becomes an AdjustmentListener of this scrollbar.
597: */
598: public void setVerticalScrollbar(Scrollbar a) {
599: vscroll = a;
600: vscroll.addAdjustmentListener(this );
601: vscroll.setMaximum(visibleItemCount);
602: vscroll.setVisibleAmount(visibleItemCount);
603: vscroll.setBlockIncrement(visibleItemCount);
604: }
605:
606: /**
607: * Sets 'a' as horizontal Scrollbar.
608: * The Browser becomes an AdjustmentListener of this scrollbar.
609: */
610: public void setHorizontalScrollbar(Scrollbar a) {
611: hscroll = a;
612: hscroll.addAdjustmentListener(this );
613: int myWidth = getSize().width - HMARGIN * 2;
614: hscroll.setMaximum(myWidth);
615: hscroll.setVisibleAmount(myWidth);
616: hscroll.setBlockIncrement(20);
617: }
618:
619: /**
620: * Updates graphical appearance in response to a scroll.
621: */
622: public void adjustmentValueChanged(AdjustmentEvent evt) {
623: if (evt.getSource() == vscroll) {
624: topItem = evt.getValue();
625: } else {
626: startx = evt.getValue();
627: }
628: repaint();
629: }
630:
631: /**
632: * Returns the parent node of the specified node.
633: * If 'child' is a valid node belonging to the Tree and has a parent node,
634: * returns its parent. Returns null otherwise.
635: * @param child the child node you want to get its parent
636: */
637: public TreeNode getParent(TreeNode child) {
638: int n = items.indexOf(child);
639: for (int i = n - 1; i >= 0; i--) {
640: TreeNode node = (TreeNode) (items.elementAt(i));
641: if (node.level < child.level) {
642: return node;
643: }
644: }
645: return null;
646: }
647:
648: /**
649: * Gets the node associated to the specified object, or null if any.
650: * @param obj the object related to a node
651: */
652: public TreeNode getNode(Object obj) {
653: int imax = items.size();
654: for (int i = 0; i < imax; i++) {
655: if (obj.equals(((TreeNode) (items.elementAt(i))).getItem()))
656: return (TreeNode) (items.elementAt(i));
657: }
658: return null;
659: }
660: }
|