// TreeBrowser.java
// $Id: TreeBrowser.java,v 1.5 1997/07/07 15:42:03 ylafon Exp $
// Author: Jean-Michel.Leon@sophia.inria.fr, Yves.Lafon@w3.org
// (c) COPYRIGHT MIT and INRIA, 1997.
// Please first read the full copyright statement in file COPYRIGHT.html

package w3c.tools.widgets;

import java.awt.*;
import java.awt.event.*;
import java.util.*;


/**
 * The TreeBrowser class.
 *
 * This class is a generic framework to browser any hierachical structure.
 *
 * 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
 * ots own handler, but handlers may also (this is the most common case) be
 * shared between handlers.
 *
 * 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
 * Lines, insert/remove, awt 1.1 version: ylafon@w3.org
 * @see w3c.jigadm.gui.DirectoryBrowser
 * @see w3c.jigadm.gui.TreeNode
 * */

public class TreeBrowser extends Canvas implements AdjustmentListener {

    static public final int SINGLE = 1;
    static public final int MULTIPLE = 1;
    
    static private java.awt.Color selectColor = java.awt.Color.magenta;
    
    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;

    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));
    }


   /**
    * Redraws the current context, using double buffering.
    */    
    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);

    }

   /**
    * repaints the View.
    */
    public void paint(Graphics g) {
	fontHeight = g.getFontMetrics().getHeight();
	int fontAscent = g.getFontMetrics().getAscent();
	int itemCount = items.size();
		
	int myHeight = getSize().height-VMARGIN*2;
	int myWidth = getSize().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);
	visibleItemCount = 0;
	TreeNode node;
	level = -1;

	// we push the indexes of the inner levels to speed up things
	for(int i = 0; i < topItem; 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;
	    }
	}

	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);
	    if(y <= myHeight) {
		if(node.selected) {
		    bg.fillRect(dx, y, myWidth-1, fontHeight-1);
		}
		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);
		}
	    }
	    y += fontHeight;
	}
	updateScrollbars();
    }


    private 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;
    }

   /**
    * 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(label == null) {
	    label = handler.toString();
	}
	if(handler == null) {
	    handler = parent.handler;
	}
	if(parent.children == TreeNode.NOCHILD) {
	    parent.children = 1;
	}
	else {
	    parent.children += 1;
	}
	done = false;
	for(int i=items.indexOf(parent)+parent.children; 
	    i< items.size();
	    i++) {
	    if(((TreeNode)items.elementAt(i)).level <= parent.level) {
		items.insertElementAt(new TreeNode(item,label,handler,icon,
						   parent.level+1),
				      i);
		done = true;
		break;
	    }
	} 
    	if(!done) {
	    items.insertElementAt(new TreeNode(item,label,handler,icon,
					       parent.level+1),
				  items.indexOf(parent)+parent.children);
	}
	return;
    }


   /**
    * Removes a node.
    *
    * This simply removes a node, without modifying its children if any. USE
    * WITH CAUTION.
    */
    public void remove(TreeNode node) {
	items.removeElement(node);
	if(node.selected) {
	    unselect(node);
	}
    }

    /**
     * Removes a 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
     */

    public void removeBranch(TreeNode node) {
	int ist, iend;
	
	ist  = items.indexOf(node);
	iend = items.size()-1;
	remove(node);

	for(int i = ist; i< iend; i++) {
	    if(((TreeNode)items.elementAt(ist)).level > node.level) {
		remove((TreeNode)items.elementAt(ist));
	    } else
		break;
	}
    }

   /**
    * contracts the representation of this node.
    *
    * removes all the children nodes of 'item'. It is caller's
    * responsibility to call repaint() afterwards.
    */
    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 {
		    // 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 a node.
    *
    * Selects the given node. If selectionPolicy is SINGLE any previously
    * selected node is unselected first.
    *
    * It is caller's responsibility to call repaint()
    */    
    public void select(TreeNode node) {
	if(node == null) return;
	if(selectionPolicy == SINGLE) {
	    unselectAll();
	}
	selection.addElement(node);
	node.selected = true;
    }


   /**
    * Selects a node.
    *
    * Unselects the given node.
    *
    * It is caller's responsibility to call repaint()
    */  
    public void unselect(TreeNode node) {
	if(node == null) return;
	selection.removeElement(node);
	node.selected = false;
    }


   /**
    * Unselects any selected item.
    */    
    public void unselectAll() {
	for(Enumeration e = selection.elements(); e.hasMoreElements(); ) {
	    TreeNode node = (TreeNode)e.nextElement();
	    node.selected = false;
	}
    }

   /**
    * return an Enumeraiton of selected items.
    */
    public Enumeration selection() {
	return selection.elements();
    }


    private void updateScrollbars() {
	int max = items.size() + 1;
	if(items.size() > visibleItemCount) {
	    if(max != vscroll.getMaximum()) {
		vscroll.setMaximum(max);
		vscroll.setVisibleAmount(visibleItemCount);
	    }
	    vscroll.setVisible(true);
	}
	else {
	    vscroll.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);
    }


   /**
    * Updates graphical appearance in response to a scroll.
    */    
    public void adjustmentValueChanged(AdjustmentEvent evt) {
	topItem = evt.getValue();
	repaint();
    }


   /**
    * Returns parent node.
    *
    * If 'child' is a valid node belonging to the Tree and has a parent node,
    * returns its parent. Returns null otherwise.
    */
    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;
    }

    public TreeNode getNode(Object o) {
	int imax = items.size();
	for(int i=0; i < imax; i++) {
	    if(o.equals(((TreeNode)(items.elementAt(i))).getItem())) 
		return (TreeNode)(items.elementAt(i));
	}
	return null;
    }
}
    
