// Copyright 1996, Marimba Inc. All Rights Reserved.


// @(#)TreeNodeWidget.java, 1.37, 12/18/96





package marimba.gui;





import java.awt.*;


import java.util.*;





import marimba.persist.*;





/**


 * A node in a hierarchical list. This needs to be


 * generalized so that it can be customized easier.


 *


 * @author	Arthur van Hoff


 * @version 	1.37, 12/18/96


 */ 





public class TreeNodeWidget extends GroupWidget {


    static Color selectedColor = new Color(50, 50, 190);


    public static int INDENT1 = 8;


    public static int INDENT2 = 16;





    Image img;





    /**


     * The tree to which this node belongs.


     * @see #getTreeWidget


     * @see #setTreeWidget


     */


    private TreeWidget  tree = null;





    /**


     * The name of the source file for the image.


     * @see #getImage


     * @see #setImage


     */


    public String src;





    /**


     * The label of this node.


     * @see #getText


     * @see #setText


     */


    public String label;





    /**


     * True if the node is collapsed, the children are invisible.


     * @see #isCollapsed


     * @see #collapse


     * @see #expand


     * @see #collapseAll


     * @see #expandAll


     */


    public boolean collapsed;





    /**


     * True if this node is currently selected.


     * @see #isSelected


     * @see #select


     * @see #getSelected


     */


    public boolean selected;





    /**


     * Constructor


     */


    public TreeNodeWidget() {


	lineMode = NONE;


	fillMode = NONE;


	label = "node";


	src = "~builder/archiv_small.gif";


    }





    /**


     * Some handy constructors.


     */


    public TreeNodeWidget(String label) {


	lineMode = NONE;


	fillMode = NONE;


	this.label = label;


	collapsed = false;


	invalidate();


    }





    /**


     * Get the properties of this widget.


     */


    public void getProperties(PropertyList list) {


	super.getProperties(list);


	list.setBoolean("collapsed", collapsed, false);


	list.setString("label", label, null);


	list.setString("src", src, null);


    }





    /**


     * Set the properties of this widget.


     */


    public void setProperties(PropertyList list) {


	super.setProperties(list);


	collapsed = list.getBoolean("collapsed", false);


	label = list.getString("label", null);


	src = list.getString("src", null);


	img = null;


    }





    /**


     * Get the TreeWidget in which this treenode is embedded;


     * if the tree has not been set, a search all the way up


     * is performed by this method and the tree is set.


     * @see #setTreeWidget


     */


    public TreeWidget getTreeWidget() {


	if (tree == null) {


	    for (Widget w = parent ; w != null ; w = w.parent) {


		if (w instanceof TreeWidget) {


		    tree = (TreeWidget)w;


		    break;


		}


	    }


	}


	return tree;


    }





    /**


     * Set the TreeWidget in which this node is embedded,


     * by setting this the search for the tree all the way


     * up is not necessary anymore.


     * @see #getTreeWidget


     */


    public void setTreeWidget(TreeWidget tree) {


	this.tree = tree;


    }





    /**


     * Get root of the Tree.


     */


    public TreeNodeWidget getRoot() {


	return getTreeWidget().getRoot();


    }





    /**


     * Check whether this tree node is the root.


     */


    public boolean isRoot() {


	return getRoot() == this;


    }





    /**


     * Remove all the children of this node.


     */


    public void clear() {


	for (int i = nwidgets ; i-- > 0 ;) {


	    Widget node = widgets[i];


	    if ((node instanceof TreeNodeWidget)) {


		remove(node);


	    }


	}


    }





    /**


     * Return a vector of nodes that are visible, in linear order.


     */


    public Vector getNodes() {


	Vector v = new Vector();


	getNodes(v);


	return v;


    }





    /**


     * Get the nodes that are visible.


     */


    void getNodes(Vector v) {


	v.addElement(this);


	for (int i = 0 ; i < nwidgets ; i++) {


	    Widget node = widgets[i];


	    if ((node instanceof TreeNodeWidget) && node.isVisible()) {


		((TreeNodeWidget)node).getNodes(v);


	    }


	}


    }





    /**


     * Return a vector of all nodes, in linear order.


     */


    public Vector getAllNodes() {


	Vector v = new Vector();


	getAllNodes(v);


	return v;


    }





    /**


     * Get all nodes.


     */


    void getAllNodes(Vector v) {


	v.addElement(this);


	for (int i = 0 ; i < nwidgets ; i++) {


	    Widget node = widgets[i];


	    if (node instanceof TreeNodeWidget) {


		((TreeNodeWidget)node).getAllNodes(v);


	    }


	}


    }





    /**


     * True if the node has children.


     */


    public boolean hasChildren() {


	return nwidgets > 0;


    }





    /**


     * Returns a vector containing all the direct


     * children of this node, in linear order.


     */


    public Vector getChildren() {


	Vector  result = new Vector();


	for (int i = 0 ; i < nwidgets ; i++) {


	    Widget node = widgets[i];


	    if (node instanceof TreeNodeWidget) {


		result.addElement(node);


	    }


	}


	return result;


    }





    /**


     * True if the node is collapsed.


     * @see #collapsed


     */


    public boolean isCollapsed() {


	return collapsed;


    }





    /**


     * Collapse the node.


     * @see #collapsed


     */


    public void collapse() {


	// if currently selected item is below this node,


	// this node has to become the selected one.


	Vector  nodes = getNodes();


	TreeNodeWidget  sel = getSelected();


	if (nodes!=null && sel!=null && nodes.contains(sel)) {


	    select();


	    focus();


	    requestFocus();


	}


	collapse(true);


    }





    /**


     * Collapse the node.


     * @see #collapsed


     */


    public void collapse(boolean collapsed) {


	if (this.collapsed != collapsed) {


	    this.collapsed = collapsed;


	    for (int i = nwidgets ; i-- > 0 ; ) {


		widgets[i].show(!collapsed);


	    }


	    parent = this.parent;


	    if (parent != null) {


		parent.repaint();


		Vector  visible = getNodes();


		if (visible.size() > 0) {


		    getTreeWidget().focus((TreeNodeWidget)visible.lastElement());


		}


		focus();


	    }


	}


    }





    /**


     * Expand the node.


     * @see #collapsed


     */


    public void expand() {


	collapse(false);





	// Make sure all the parent nodes are expanded


	for (Widget w = parent ; w instanceof TreeNodeWidget ; w = w.parent) {


	    ((TreeNodeWidget)w).collapse(false);


	}


    }





    /**


     * Expand all children.


     * @see #collapsed


     */


    public void expandAll() {


	collapseAll(false);


    }





    /**


     * Collapse all children.


     * @see #collapsed


     */


    public void collapseAll() {


	collapseAll(true);


    }





    /**


     * Collapse all children.


     * @see #collapsed


     */


    public void collapseAll(boolean collapsed) {


	for (int i = nwidgets ; i-- > 0 ;) {


	    if (widgets[i] instanceof TreeNodeWidget) {


		TreeNodeWidget node = (TreeNodeWidget)widgets[i];


		node.collapse(collapsed);


		node.collapseAll(collapsed);


	    }


	}


    }





    /**


     * Focus on this widget.


     */


    public void focus() {


	TreeWidget tree = getTreeWidget();


	if (tree != null) {


	    tree.validate();


	    parent.validate();


	    tree.focus(this);


	}


    }





    /**


     * Add a child node sorted.


     */


    public void addSorted(TreeNodeWidget node) {


	String key = node.key();


	if (key != null) {


	    for (int i = nwidgets ; i-- > 0 ; ) {


		if (widgets[i] instanceof TreeNodeWidget) {


		    if (key.compareTo(((TreeNodeWidget)widgets[i]).key()) > 0) {


			content.add(i+1, node);


			return;


		    }


		}


	    }


	    add(0, node);


	} else {


	    add(node);


	}


    }





    /**


     * Add a node to the tree. This takes into account


     * whether the node is currently collapsed or expanded.


     */


    public void add(TreeNodeWidget node) {


	node.show(!isCollapsed());


	node.setTreeWidget(getTreeWidget());


	super.add(node);


    }





    /**


     * Check if this node is selected.


     * @see #selected


     */


    public boolean isSelected() {


	return selected;


    }


    


    /**


     * Select this widget. Unselect all other selected widgets


     * in this tree.


     * @see #selected


     */


    public void select() {


	select(true);


    }





    /**


     * Select this widget.


     * @see #selected


     */


    public void select(boolean selected) {


	if (this.selected != selected) {


	    if (selected) {


		TreeNodeWidget root = getRoot();


		TreeNodeWidget sel = root.getSelected();


		if ((sel != null) && (sel != this)) {


		    sel.select(false);


		}


	    }





	    this.selected = selected;


	    repaint();





	    TreeWidget tree = getTreeWidget();


	    if (tree != null) {


		tree.postEvent(new Event(tree, selected ? Event.LIST_SELECT : Event.LIST_DESELECT, getText()));


	    } else {


		postEvent(new Event(this, selected ? Event.LIST_SELECT : Event.LIST_DESELECT, getText()));


	    }


	}


    }





    /**


     * Get the selected widget in this subtree.


     * @see #selected


     */


    public TreeNodeWidget getSelected() {


	if (isSelected()) {


	    return this;


	}


	for (int i = nwidgets ; i-- > 0 ;) {


	    if (widgets[i] instanceof TreeNodeWidget) {


		TreeNodeWidget node = (TreeNodeWidget)widgets[i];


		TreeNodeWidget sel = node.getSelected();


		if (sel != null) {


		    return sel;


		}


	    }


	}


	return null;


    }





    /**


     * Get the label of this node


     * @see #label


     */


    public String getText() {


	return label;


    }





    /**


     * Set the label of this node


     * @see #label


     */


    public void setText(String label) {


	if (label == null) {


	    if (this.label != null) {


		label = null;


		invalidate();


		repaint();


	    }


	    return;


	}


	if (!label.equals(this.label)) {


	    this.label = label;


	    invalidate();


	    repaint();


	}


    }





    /**


     * Get the image for this node


     * @see #src


     */


    public String getImage() {


	return src;


    }





    /**


     * Set the image for this node


     * @see #src


     */


    public void setImage(String src) {


	this.src = src;


	img = null;


	if (isShowing()) {


	    start();


	    repaint();


	}


    }





    /**


     * The key by which this item is sorted.


     */


    public String key() {


	return getText();


    }





    /**


     * Invalidate this widget (and its parent)


     */


    public void invalidate() {


	if (valid) {


	    super.invalidate();


	    Widget parent = this.parent;


	    if (parent instanceof TreeNodeWidget) {


		parent.invalidate();


	    }


	}


    }





    /**


     * Layout the children of the node.


     */


    public void layout() {


	FontMetrics fm = getFontMetrics(font);


	int width = getLabelWidth();


	int y = getLabelHeight();





	if (collapsed) {


	    for (int i = 0 ; i < nwidgets ; i++) {


		widgets[i].hide();


	    }


	} else {


	    for (int i = 0 ; i < nwidgets ; i++) {


		Widget w = widgets[i];


		w.validate();


		w.move(INDENT2, y);


		y += w.height;


		width = Math.max(width, w.width + INDENT2);


	    }


	}


	resize(width, y);


    }





    /**


     * Start loading the image.


     */


    public void start() {


	if ((img == null) && (src != null)) {


	    Image newimg = getImage(src);


	    if (Toolkit.getDefaultToolkit().prepareImage(newimg, -1, -1, this)) {


		img = newimg;


		repaint();


	    }


	}


    }





    /**


     * Update the image.


     */


    public boolean imageUpdate(Image newimg, int flags, int x, int y, int w, int h) {


	if ((flags & ALLBITS) != 0) {


	    img = newimg;


	    repaint();


	    return false;


	}


	return (flags & ERROR) == 0;


    }





    /**


     * Compute the width of the Label


     */


    public int getLabelWidth() {


	return 25 + ((label != null) ? getFontMetrics(font).stringWidth(label) : 0);


    }





    /**


     * Compute the height of the Label


     */


    public int getLabelHeight() {


	return (label != null) ? getFontMetrics(font).getHeight() + 4 : 0;


    }


    


    /**


     * Paint the label.


     */


    public void paintIcon(Graphics g) {


	if (img != null) {


	    int w = img.getWidth(this);


	    int h = img.getHeight(this);


	    if ((w > 0) && (h > 0)) {


		g.drawImage(img, 0, (getLabelHeight() - h)/2, this);


	    }


	}


    }





    /**


     * Paint the label.


     */


    public void paintLabel(Graphics g) {


	if (label != null) {


	    FontMetrics fm = getFontMetrics(font);


	    int ht = getLabelHeight();


	    int x = (src != null) ? 20 : 0;





	    int strw = fm.stringWidth(label);


	    if (selected) {


		g.setColor(getSelBackground());


		g.fillRect(x-1, 1, strw+3, ht-2);


		g.setColor(getSelForeground());


	    } else {


		g.setColor(foreground);


	    }


	    g.setFont(font);


	    g.drawString(label, x, fm.getAscent()+1);





	    if (hasFocus()) {


		g.setColor(getSelFocusColor());


		Dash.drawFocusRect(g, x-1, 1, strw+3, ht-2);


	    }


	}


    }


    /**


     * Paint the node: the label, the lines to the child widgets.


     */


    public void paint(Graphics g, int x, int y, int width, int height) {


	//System.out.println(label + " -- " + g.getClipRect());


	int ny = getLabelHeight();





	if (y < ny) {


	    paintIcon(g);


	    paintLabel(g);


	}





	if (!collapsed) {


	    FontMetrics fm = getFontMetrics(font);


	    int ht = fm.getHeight() + 4;


	    int nx = INDENT1;


	    Dash d = Dash.getDash(2, 1, null, Color.gray);


	    if (ny > y + height) {


		return;


	    }


	    if (nwidgets > 0) {


		d.dashLine(g, nx, ny, INDENT1, widgets[nwidgets-1].y + ht/2);


	    }


	    for (int i = 0 ; i < nwidgets ; i++) {


		Widget w = widgets[i];


		ny = w.y + ht/2;


		if (ny + 5 < y) {


		    continue;


		}


		if (ny > y + height + 5) {


		    break;


		}





		d.dashLine(g, nx, ny, INDENT2, y);


		if (w instanceof TreeNodeWidget) {


		    TreeNodeWidget node = (TreeNodeWidget)w;


		    if (node.hasChildren()) {


			g.setColor(Color.gray);


			g.drawRect(nx - 4, ny - 4, 8, 8);


			g.setColor(hilite);


			g.fillRect(nx - 3, ny - 3, 7, 7);


			g.setColor(foreground);


			g.drawLine(nx - 2, ny, nx + 2, ny);


			if (node.isCollapsed()) {


			    g.drawLine(nx, ny - 2, nx, ny + 2);


			}


		    }


		}


	    }


	}


    }





    /**


     * Goto the next node, used in handleEvent.


     * @see #handleEvent


     */


    protected void gotoNextNode() {


	Vector v = getRoot().getNodes();


	int i = v.indexOf(this);


	if ((i >= 0) && (i < v.size()-1)) {


	    TreeNodeWidget node = (TreeNodeWidget)v.elementAt(i+1);


	    node.focus();


	    node.select();


	    node.requestFocus();


	}


    }





    /**


     * Goto the previous node, used in handleEvent.


     * @see #handleEvent


     */


    protected void gotoPreviousNode() {


	Vector v = getRoot().getNodes();


	int i = v.indexOf(this);


	if (i > 0) {


	    TreeNodeWidget node = (TreeNodeWidget)v.elementAt(i-1);


	    node.focus();


	    node.select();


	    node.requestFocus();


	}


    }





    /**


     * Handle mouse events.


     */


    public boolean handleEvent(Event evt) {


	switch (evt.id) {


	  case Event.GOT_FOCUS:


	    if (isRoot() && !getTreeWidget().showRoot) {


		gotoNextNode();


		return true;


	    }


	    break;





	  case Event.MOUSE_DOWN:


	    FontMetrics fm = getFontMetrics(font);


	    if (evt.y < getLabelHeight()) {


		select();


		requestFocus();


		if (evt.clickCount > 1) {


		    action();


		}


		return false;


	    }


	    if (evt.x < INDENT2) {


		for (int i = 0 ; i < nwidgets ; i++) {


		    Widget w = widgets[i];


		    if ((evt.y >= w.y) && (evt.y < w.y + w.height)) {


			if (w instanceof TreeNodeWidget) {


			    TreeNodeWidget node = (TreeNodeWidget)w;


			    if (node.hasChildren()) {


				if (node.isCollapsed()) {


				    node.expand();


				} else {


				    node.collapse();


				}


			    }


			}


			return false;


		    }


		}


	    }


	    return false;





	  case Event.KEY_PRESS:


	    switch (evt.key) {


	      case '\n':


		if (selected) {


		    action();


		}


		return true;


	    }


	    break;





	  case Event.KEY_ACTION:


	    switch (evt.key) {


	      case Event.UP: {


		gotoPreviousNode();


		return true;


	      }





	      case Event.DOWN: {


		gotoNextNode();


		return true;


	      }





	      case Event.LEFT:


		if (isCollapsed() || !hasChildren()) {


		    if (parent instanceof TreeNodeWidget) {


			TreeNodeWidget node = (TreeNodeWidget)parent;


			node.focus();


			node.select();


			node.requestFocus();


		    }


		} else {


		    collapse();


		}


		return true;





	      case Event.RIGHT:


		if (hasChildren()) {


		    if (isCollapsed()) {


			expand();


		    } else {


			for (int i = 0 ; i < nwidgets ; i++) {


			    if (widgets[i] instanceof TreeNodeWidget) {


				TreeNodeWidget node = (TreeNodeWidget)widgets[i];


				node.focus();


				node.select();


				node.requestFocus();


				break;


			    }


			}


		    }


		}


		return true;


	    }


	}


	return super.handleEvent(evt);


    }





    /**


     * The user double clicked this node.


     */


    public void action() {


	if (hasChildren()) {


	    collapse(!collapsed);


	} else {


	    super.action();


	}


    }





    /**


     * A treeNode is interested in the focus if it is selected.


     */


    public boolean focusInterest() {


	return selected;


    }





    /**


     * Debugging


     */


    public void paramString(StringBuffer buf) {


	super.paramString(buf);


	if (label != null) {


	    buf.append(",label=");


	    buf.append(label);


	}


	if (src != null) {


	    buf.append(",src=");


	    buf.append(src);


	}


	if (hasChildren()) {


	    buf.append(",children");


	}


	if (isCollapsed()) {


	    buf.append(",collapsed");


	}


    }


}


