Java/GWT/Tree Table

Материал из Java эксперт
Перейти к: навигация, поиск

A Tree Table

/*
 * Copyright 2006 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
 
package com.jexp.gwt.client;
import java.util.Iterator;
import java.util.Vector;
import java.util.EventListener;
import java.util.List;
import java.util.ArrayList;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.KeyboardListener;
import com.google.gwt.user.client.ui.KeyboardListenerCollection;
import com.google.gwt.user.client.ui.MouseListener;
import com.google.gwt.user.client.ui.MouseListenerCollection;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.Widget;

import com.google.gwt.user.client.ui.HasFocus;
import com.google.gwt.user.client.ui.HasHTML;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.Tree;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.RootPanel;

public class GWTClient implements EntryPoint{
  /**
   * This is the entry point method.
   */
  public void onModuleLoad() {
    HorizontalPanel p = new HorizontalPanel();
    TreeTable fileTreeTable = createFileTreeTable();
    p.add(fileTreeTable);
    TreeTable treeTable = createToDoTreeTable();
    p.add(treeTable);
    treeTable = createSimpleTreeTable();
    p.add(treeTable);
    RootPanel.get("slot1").add(p);
    // Adds a few items after display
    fileTreeTable.addItem(new File("File E", "1 TB", "Jan 1, 2005"));
    fileTreeTable.getItem(0).addItem(new File("File E", "1 TB", "Jan 1, 2005"));
  }
  
  /**
   * Creates an example tree using the default renderer. Wigdets are 
   * rendered, objects are rendered with toString(), and array values
   * are inserted across the table. 
   */
  public TreeTable createSimpleTreeTable() {
    TreeTable treeTable = new TreeTable();
    
    TreeItem item = treeTable.addItem("I"m text");
    item.addItem("I"m <b>HTML</b>");
    item.setState(true);
    item = treeTable.addItem(new CheckBox("I"m a Widget!!!"));
    item.addItem("Child");
    item = treeTable.addItem("Parent");
    item.addItem("Child");
    treeTable.addItem(new Object[] {new CheckBox("I"m in an array"), "1", "2", new CheckBox("3")});
    
    return treeTable;
  }
  
  /**
   * Creates an example tree using a custom renderer. File objects are 
   * added as user objects and the renderer displays values. 
   * @return
   */
  public TreeTable createFileTreeTable() {
    TreeTable treeTable = new TreeTable();
    treeTable.setBorderWidth(1);
    treeTable.setCellPadding(3);
    treeTable.setCellSpacing(0);
    
    TreeItem item = treeTable.addItem(new File("Folder A", "-", "Apr 4, 2007"));
    item.addItem(new File("File AA", "128 kb", "Apr 4, 2007"));
    item.addItem(new File("File AB", "64 kb", "Apr 1, 2007"));
    item = treeTable.addItem(new File("Folder B", "-", "Jan 21, 2006"));
    item.addItem(new File("File BA", "256 kb", "Jan 18, 2006"));
    item = item.addItem(new File("Folder BB", "-", "Jan 21, 2006"));
    item.addItem(new File("File BBA", "256 kb", "Jan 18, 2006"));
    item.addItem(new File("File BBB", "256 kb", "Jan 18, 2006"));
    item.addItem(new File("File BBC", "256 kb", "Jan 18, 2006"));
    item.addItem(new File("File BBD", "256 kb", "Jan 21, 2006"));
    treeTable.addItem(new File("File C", "256 kb", "Jan 18, 2006"));
    treeTable.addItem(new File("File D", "256 kb", "Jan 18, 2006"));
    
    treeTable.setRenderer(new ExampleRenderer());
    
    return treeTable;
  }
  
  /**
   * Creates an example tree using a custom renderer. ToDo objects
   * are added as the user objects of TreeItems. The renderer turns
   * them into Widgets. 
   * @return
   */
  public TreeTable createToDoTreeTable() {
    TreeTable treeTable = new TreeTable();
    TreeItem grp1 = treeTable.addItem("Group 1");
    grp1.addItem(new ToDo("Garbage", "3 days", "Matt"));
    grp1.addItem(new ToDo("Dishes", "1 day", "Matt"));
    grp1.addItem(new ToDo("Laundry", "2 days", "Matt"));
    TreeItem grp2 = treeTable.addItem("Group 2");
    grp2.addItem(new ToDo("Task 1", "2 days", "Unassigned"));
    grp2.addItem(new ToDo("Task 2", "3 day", "Unassigned"));
    grp2.addItem(new ToDo("Task 3", "7 days", "Unassigned"));
    
    treeTable.setRenderer(new ExampleRenderer());
    
    return treeTable;
  }
  
  class ExampleRenderer implements TreeTableRenderer {
    public void renderTreeItem(TreeTable table, TreeItem item, int row) {
      Object obj = item.getUserObject();
      if (obj instanceof ToDo) {
        ToDo todo = (ToDo) obj;
        item.setWidget(new CheckBox(todo.name));
        table.setText(row, 1, todo.due);
        table.setText(row, 2, todo.who);
      } else if (obj instanceof File) {
        File f = (File) obj;
        item.setText(f.name);
        table.setText(row, 1, f.size);
        table.setText(row, 2, f.date);
      } else if (obj != null) {
        item.setText(obj.toString());
      }
    }
  }
  
  public class File {
    public String name;
    public String size;
    public String date;
    public File(String n, String s, String d) {
      name = n;
      size = s;
      date = d;
    }
    
    public String toString() {
      return name;
    }
  }
  
  public class ToDo {
    public String name;
    public String due;
    public String who;
    public ToDo(String n, String d, String w) {
      name = n;
      due = d;
      who = w;
    }
    
    public String toString() {
      return name;
    }
  }
}

/**
 * Shameless copy of com.google.gwt.user.client.ui.TreeItem. GWT"s TreeItem does
 * not expose enough to allow a simple subclass. If that changes, this class
 * will be greatly simplified.
 * 
 * Changes:
 * <li>Removed the DOM hierarchy of tree nodes. Each node is
 * independent and therefore placable is a table cell.</li>
 * <li>Changed subclass to Widget so the TreeItem could be added to a table.</li> 
 * <li>Changed parent Tree to TreeTable.</li>
 * <li>Worked around package scope methods from the GWT ui package.</li>
 * <li>Removed ContentPanel.</li>
 * <li>Added row index cache.</li>
 * </ul>
 * 
 * @author Matt Boyd (modifications to GWT"s classes)
 */
class TreeItem extends Widget implements HasHTML {
  private Vector children = new Vector();
  private Element itemTable, contentElem, imgElem;
  private boolean open;
  private TreeItem parentItem;
  private boolean selected;
  private Object userObject;
  private TreeTable table;
  private int row;
  
  private Widget widget;
  /**
   * Creates an empty tree item.
   */
  public TreeItem() {
    setElement(DOM.createDiv());
    itemTable = DOM.createTable();
    contentElem = DOM.createSpan();
    imgElem = DOM.createImg();
    // Uses the following Element hierarchy:
    // <div (handle)>
    // <table (itemElem)>
    // <tr>
    // <td><img (imgElem)/></td>
    // <td><span (contents)/></td>
    // </tr>
    // </table>
    // </div>
    Element tbody = DOM.createTBody(), tr = DOM.createTR();
    Element tdImg = DOM.createTD(), tdContent = DOM.createTD();
    DOM.appendChild(itemTable, tbody);
    DOM.appendChild(tbody, tr);
    DOM.appendChild(tr, tdImg);
    DOM.appendChild(tr, tdContent);
    DOM.setStyleAttribute(tdImg, "verticalAlign", "middle");
    DOM.setStyleAttribute(tdContent, "verticalAlign", "middle");
    DOM.appendChild(getElement(), itemTable);
    DOM.appendChild(tdImg, imgElem);
    DOM.appendChild(tdContent, contentElem);
    DOM.setAttribute(getElement(), "position", "relative");
    DOM.setStyleAttribute(contentElem, "display", "inline");
    DOM.setStyleAttribute(getElement(), "whiteSpace", "nowrap");
    DOM.setAttribute(itemTable, "whiteSpace", "nowrap");
    setStyleName(contentElem, "gwt-TreeItem", true);
  }
  
  public TreeItem(Object userObj) {
    this();
    setUserObject(userObj);
  }
  /**
   * Adds a child tree item containing the specified text.
   * 
   * @param itemText
   *            the text to be added
   * @return the item that was added
   */
  public TreeItem addItem(String itemText) {
    TreeItem ret = new TreeItem(itemText);
    addItem(ret);
    return ret;
  }
  
  public TreeItem addItem(Object userObj) {
    TreeItem ret = new TreeItem(userObj);
    addItem(ret);
    return ret;
  }
  
  /**
   * Adds another item as a child to this one.
   * 
   * @param item
   *            the item to be added
   */
  public void addItem(TreeItem item) {
    // If this element already belongs to a tree or tree item, it should be
    // removed.
    if ((item.getParentItem() != null) || (item.getTreeTable() != null)) {
      item.remove();
    }
    item.setTreeTable(table);
    item.setParentItem(this);
    children.add(item);
    int d = item.getDepth();
    if (d != 0) {
      DOM.setStyleAttribute(item.getElement(), "marginLeft", (d * 16) + "px");
    }
    if (table != null) {
      table.insertItem(item, getRow() + getChildCount());
      table.updateRowCache();
      table.updateVisibility(item);
    }
    if (children.size() == 1) {
      updateState();
    }
  }
  public int getRow() {
    return row;
  }
  void setRow(int r) {
    row = r;
  }
  /**
   * Returns the depth of this item. Depth of root child is 0.
   * 
   * @return
   */
  public int getDepth() {
    if (parentItem == null) {
      return 0;
    }
    return parentItem.getDepth() + 1;
  }
  /**
   * Returns the count of all descendents; includes this item in the count.
   * 
   * @return
   */
  int getDescendentCount() {
    int d = 1;
    for (int i = getChildCount() - 1; i >= 0; i--) {
      d += getChild(i).getDescendentCount();
    }
    return d;
  }
  /**
   * Adds a child tree item containing the specified widget.
   * 
   * @param widget
   *            the widget to be added
   * @return the item that was added
   */
  public TreeItem addItem(Widget widget) {
    TreeItem ret = new TreeItem(widget);
    addItem(ret);
    return ret;
  }
  /**
   * Gets the child at the specified index.
   * 
   * @param index
   *            the index to be retrieved
   * @return the item at that index
   */
  public TreeItem getChild(int index) {
    if ((index < 0) || (index >= children.size())) {
      return null;
    }
    return (TreeItem) children.get(index);
  }
  /**
   * Gets the number of children contained in this item.
   * 
   * @return this item"s child count.
   */
  public int getChildCount() {
    return children.size();
  }
  /**
   * Gets the index of the specified child item.
   * 
   * @param child
   *            the child item to be found
   * @return the child"s index, or <code>-1</code> if none is found
   */
  public int getChildIndex(TreeItem child) {
    return children.indexOf(child);
  }
  public String getHTML() {
    return DOM.getInnerHTML(contentElem);
  }
  /**
   * Gets this item"s parent.
   * 
   * @return the parent item
   */
  public TreeItem getParentItem() {
    return parentItem;
  }
  /**
   * Gets whether this item"s children are displayed.
   * 
   * @return <code>true</code> if the item is open
   */
  public boolean getState() {
    return open;
  }
  public boolean isOpen() {
    return getState();
  }
  public String getText() {
    return DOM.getInnerText(contentElem);
  }
  /**
   * Gets the tree that contains this item.
   * 
   * @return the containing tree
   */
  public TreeTable getTreeTable() {
    return table;
  }
  /**
   * Gets the user-defined object associated with this item.
   * 
   * @return the item"s user-defined object
   */
  public Object getUserObject() {
    return userObject;
  }
  /**
   * Gets the <code>Widget</code> associated with this tree item.
   * 
   * @return the widget
   */
  public Widget getWidget() {
    return widget;
  }
  /**
   * Determines whether this item is currently selected.
   * 
   * @return <code>true</code> if it is selected
   */
  public boolean isSelected() {
    return selected;
  }
  /**
   * Removes this item from its tree.
   */
  public void remove() {
    if (parentItem != null) {
      // If this item has a parent, remove self from it.
      parentItem.removeItem(this);
    } else if (table != null) {
      // If the item has no parent, but is in the Tree, it must be a
      // top-level
      // element.
      table.removeItem(this);
    }
  }
  /**
   * Removes one of this item"s children.
   * 
   * @param item
   *            the item to be removed
   */
  public void removeItem(TreeItem item) {
    if (!children.contains(item)) {
      return;
    }
    // Update Item state.
    item.setTreeTable(null);
    item.setParentItem(null);
    children.remove(item);
    if (table != null) {
      table.removeItemFromTable(item);
    }
    if (children.size() == 0) {
      updateState();
    }
  }
  /**
   * Removes all of this item"s children.
   */
  public void removeItems() {
    while (getChildCount() > 0) {
      removeItem(getChild(0));
    }
  }
  public void setHTML(String html) {
    DOM.setInnerHTML(contentElem, html);
//    if (widget != null) {
//      DOM.removeChild(contentElem, widget.getElement());
//      widget = null;
//    }
  }
  /**
   * Selects or deselects this item.
   * 
   * @param selected
   *            <code>true</code> to select the item, <code>false</code>
   *            to deselect it
   */
  public void setSelected(boolean selected) {
    if (this.selected == selected) {
      return;
    }
    this.selected = selected;
    setStyleName(contentElem, "gwt-TreeItem-selected", selected);
  }
  /**
   * Sets whether this item"s children are displayed.
   * 
   * @param open
   *            whether the item is open
   */
  public void setState(boolean open) {
    setState(open, true);
  }
  /**
   * Sets whether this item"s children are displayed.
   * 
   * @param open
   *            whether the item is open
   * @param fireEvents
   *            <code>true</code> to allow open/close events to be fired
   */
  public void setState(boolean open, boolean fireEvents) {
    if (open && children.size() == 0) {
      return;
    }
    this.open = open;
    if (open) {
      table.showChildren(this);
    } else {
      table.hideChildren(this);
    }
    updateState();
    if (fireEvents) {
      table.fireStateChanged(this);
    }
  }
  public void setText(String text) {
    DOM.setInnerText(contentElem, text);
//    if (widget != null) {
//      DOM.removeChild(contentElem, widget.getElement());
//      widget = null;
//    }
  }
  /**
   * Sets the user-defined object associated with this item.
   * 
   * @param userObj
   *            the item"s user-defined object
   */
  public void setUserObject(Object userObj) {
    userObject = userObj;
  }
  /**
   * Sets the current widget. Any existing child widget will be removed.
   * 
   * @param widget
   *            Widget to set
   */
  public void setWidget(Widget w) {
    if (widget != null) {
      DOM.removeChild(contentElem, widget.getElement());
//      widget.setParent(null);
    }
    
    if (w != null) {
      widget = w;
      DOM.setInnerText(contentElem, null);
      DOM.appendChild(contentElem, w.getElement());
//      widget.setParent(this);
    }
  }
  /**
   * Returns the widget, if any, that should be focused on if this TreeItem is
   * selected.
   * 
   * @return widget to be focused.
   */
  protected HasFocus getFocusableWidget() {
    Widget widget = getWidget();
    if (widget instanceof HasFocus) {
      return (HasFocus) widget;
    } else {
      return null;
    }
  }
  void addTreeItems(List accum) {
    for (int i = 0; i < children.size(); i++) {
      TreeItem item = (TreeItem) children.get(i);
      accum.add(item);
      item.addTreeItems(accum);
    }
  }
  Vector getChildren() {
    return children;
  }
  Element getContentElem() {
    return contentElem;
  }
  int getContentHeight() {
    return DOM.getIntAttribute(itemTable, "offsetHeight");
  }
  Element getImageElement() {
    return imgElem;
  }
  int getTreeTop() {
    TreeItem item = this;
    int ret = 0;
    while (item != null) {
      ret += DOM.getIntAttribute(item.getElement(), "offsetTop");
      item = item.getParentItem();
    }
    return ret;
  }
  String imgSrc(String img) {
    if (table == null) {
      return img;
    }
    return table.getImageBase() + img;
  }
  void setParentItem(TreeItem parent) {
    this.parentItem = parent;
  }
  void setTreeTable(TreeTable table) {
    if (this.table == table) {
      return;
    }
    if (this.table != null) {
      if (this.table.getSelectedItem() == this) {
        this.table.setSelectedItem(null);
      }
    }
    this.table = table;
    for (int i = 0, n = children.size(); i < n; ++i) {
      ((TreeItem) children.get(i)).setTreeTable(table);
    }
    updateState();
  }
  void updateState() {
    if (children.size() == 0) {
      // UIObject.setVisible(childSpanElem, false);
      DOM.setAttribute(imgElem, "src", imgSrc("tree_white.gif"));
      return;
    }
    // We must use "display" rather than "visibility" here,
    // or the children will always take up space.
    if (open) {
      // UIObject.setVisible(childSpanElem, true);
      DOM.setAttribute(imgElem, "src", imgSrc("tree_open.gif"));
    } else {
      // UIObject.setVisible(childSpanElem, false);
      DOM.setAttribute(imgElem, "src", imgSrc("tree_closed.gif"));
    }
    
//    if (getParentItem() != null) {
//      table.updateVisibility(getParentItem());
//    }
  }
  void updateStateRecursive() {
    updateState();
    for (int i = 0, n = children.size(); i < n; ++i) {
      ((TreeItem) children.get(i)).updateStateRecursive();
    }
  }
}

/**
 * Shameless copy of com.google.gwt.user.client.ui.TreeListener. 
 * Changed to replace GWT"s TreeItem with the altered TreeItem. 
 * 
 * Event listener interface for tree events.
 */
public interface TreeTableListener extends EventListener {
  /**
   * Fired when a tree item is selected.
   * 
   * @param item the item being selected.
   */
  void onTreeItemSelected(TreeItem item);
  /**
   * Fired when a tree item is opened or closed.
   * 
   * @param item the item whose state is changing.
   */
  void onTreeItemStateChanged(TreeItem item);
}
/**
 * Shameless copy of com.google.gwt.user.client.ui.TreeListenerCollection. 
 * Changed to replace TreeListener with TreeTableListener. 
 * 
 * A helper class for implementers of the SourcesClickEvents interface. This
 * subclass of Vector assumes that all objects added to it will be of type
 * {@link com.google.gwt.user.client.ui.ClickListener}.
 */
 class TreeTableListenerCollection extends Vector {
  /**
   * Fires a "tree item selected" event to all listeners.
   * 
   * @param item the tree item being selected.
   */
  public void fireItemSelected(TreeItem item) {
    for (Iterator it = iterator(); it.hasNext();) {
      TreeTableListener listener = (TreeTableListener) it.next();
      listener.onTreeItemSelected(item);
    }
  }
  /**
   * Fires a "tree item state changed" event to all listeners.
   * 
   * @param item the tree item whose state has changed.
   */
  public void fireItemStateChanged(TreeItem item) {
    for (Iterator it = iterator(); it.hasNext();) {
      TreeTableListener listener = (TreeTableListener) it.next();
      listener.onTreeItemStateChanged(item);
    }
  }
}

interface TreeTableRenderer {
  /**
   * Called to render a tree item row. 
   * @param table
   * @param item
   * @param row
   */
  void renderTreeItem(TreeTable table, TreeItem item, int row);
}
/*
 * Copyright 2006 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

/**
 * Shameless copy of com.google.gwt.user.client.ui.Tree. Extension of FlexTable
 * adding a tree in one column. Uses a TreeItem model and row based rendering of
 * table cells. 
 * 
 * Changes: 
 * <ul>
 * <li>Removed focus functionality from Tree code. It was causing problems with IE. 
 * Not sure how applicable it is with FlexTable as the base class. It may have
 * problems playing well with GWT because of package scope work arounds. Seems
 * to work ok without the code, minus drawing an outline.</li>
 * <li>Made TreeItem a Widget to be added to a table cell. Removed ContentPanel
 * handling from the Tree code. 
 * <li>Disabled some Widget add/remove code. This may cause strange bugs. Again, 
 * package scope issues. This needs a work around.</li>
 * <li>Streamlined findItemByChain() and modified elementClicked() to search the
 * table. This should probably be rewritten to leverage FlexTable. 
 * </ul>
 * 
 * Notes:
 * <ul>
 * <li>If anyone has a firm understanding of "focus" in GWT I could use the help
 * cleaning this up.</li>
 * </ul>
 * 
 * @author Matt Boyd (modifications to GWT"s classes)
 */
class TreeTable extends FlexTable {
  private Element headElem;
  private TreeItem curSelection;
//  private final Element focusable;
//  private FocusListenerCollection focusListeners;
  private String imageBase = GWT.getModuleBaseURL();
  private KeyboardListenerCollection keyboardListeners;
  private TreeTableListenerCollection listeners;
  private MouseListenerCollection mouseListeners = null;
  private final TreeItem root;
  
  private TreeTableRenderer renderer;
  /**
   * Keeps track of the last event type seen. We do this to determine if we
   * have a duplicate key down.
   */
  private int lastEventType;
  /**
   * Needed local instance. GWT"s is hidden in package scope. 
   */
//  private FocusImpl impl = (FocusImpl) GWT.create(FocusImpl.class);
  public class Renderer {
    public void renderRow(TreeTable tree, TreeItem item, int row) {
    }
  }
  /**
   * Constructs an empty tree.
   */
  public TreeTable() {
    Element tableElem = getElement();
    headElem = DOM.createElement("thead");
    Element tr = DOM.createTR();
    DOM.appendChild(headElem, tr);
    DOM.insertChild(tableElem, headElem, 0);
    DOM.setStyleAttribute(getElement(), "position", "relative");
//    focusable = impl.createFocusable();
//    DOM.setStyleAttribute(focusable, "fontSize", "0");
//    DOM.setStyleAttribute(focusable, "position", "absolute");
//    DOM.setIntStyleAttribute(focusable, "zIndex", -1);
//    DOM.appendChild(getElement(), focusable);
    sinkEvents(Event.MOUSEEVENTS | Event.ONCLICK | Event.KEYEVENTS);
//    DOM.sinkEvents(focusable, Event.FOCUSEVENTS | Event.KEYEVENTS | DOM.getEventsSunk(focusable));
    // The "root" item is invisible and serves only as a container
    // for all top-level items.
    root = new TreeItem() {
      public void addItem(TreeItem item) {
        // If this element already belongs to a tree or tree item,
        // remove it.
        if ((item.getParentItem() != null) || (item.getTreeTable() != null)) {
          item.remove();
        }
        item.setTreeTable(this.getTreeTable());
        // Explicitly set top-level items" parents to null.
        item.setParentItem(null);
        getChildren().add(item);
        // Use no margin on top-most items.
        DOM.setIntStyleAttribute(item.getElement(), "marginLeft", 0);
      }
      public void removeItem(TreeItem item) {
        if (!getChildren().contains(item)) {
          return;
        }
        // Update Item state.
        item.setTreeTable(null);
        item.setParentItem(null);
        getChildren().remove(item);
      }
    };
    root.setTreeTable(this);
    setStyleName("gwt-TreeTable");
  }
  /**
   * Adds the widget as a root tree item.
   * 
   * @see com.google.gwt.user.client.ui.HasWidgets#add(com.google.gwt.user.client.ui.Widget)
   * @param widget
   *            widget to add.
   */
  public void add(Widget widget) {
    addItem(widget);
  }
  
  /**
   * Adds a new tree item containing the specified widget.
   * 
   * @param widget
   *            the widget to be added
   */
  public TreeItem addItem(Widget widget) {
    TreeItem item = new TreeItem(widget);
    addItem(item);
    return item;
  }
  /**
   * Adds a simple tree item containing the specified text.
   * 
   * @param itemText
   *            the text of the item to be added
   * @return the item that was added
   */
  public TreeItem addItem(String itemText) {
    TreeItem ret = new TreeItem(itemText);
    addItem(ret);
    return ret;
  }
  
  public TreeItem addItem(Object userObj) {
    TreeItem ret = new TreeItem(userObj);
    addItem(ret);
    return ret;
  }
  /**
   * Adds an item to the root level of this tree.
   * 
   * @param item
   *            the item to be added
   */
  public void addItem(TreeItem item) {
    root.addItem(item);
    // Adds the item to the proper row
    insertItem(item, getRowCount());
    updateRowCache();
    updateVisibility(item);
  }
  /**
   * Updates table rows to include children.
   * 
   * @param item
   */
  void insertItem(TreeItem item, int r) {
    // inserts this item into the tree
    insertRow(r);
    setWidget(r, getTreeColumn(), item);
    item.setRow(r);
    render(item);
    Vector chlds = item.getChildren();
    for (int i = 0, s = chlds.size(); i < s; i++) {
      TreeItem chld = (TreeItem) chlds.get(i);
      insertItem(chld, r + 1);
    }
    
    TreeItem p = item.getParentItem();
    if (p != null) {
      if (!p.isOpen()) {
        setVisible(false, item.getRow());
        setChildrenVisible(item, false);
      }
    }
  }
  /**
   * Removes an item from the root level of this tree.
   * 
   * @param item
   *            the item to be removed
   */
  public void removeItem(TreeItem item) {
    root.removeItem(item);
    removeItemFromTable(item);
  }
  void removeItemFromTable(TreeItem item) {
    int r = item.getRow();
    int rs = item.getDescendentCount();
    for (int i = 0; i < rs; i++) {
      removeRow(r);
    }
    updateRowCache();
  }
  /**
   * Removes all items from the root level of this tree.
   */
  public void removeItems() {
    while (getItemCount() > 0) {
      removeItem(getItem(0));
    }
  }
  /**
   * Updates the cached row index for each tree item. TODO - Optomize with
   * start item.
   */
  void updateRowCache() {
    updateRowCache(root, -1);
  }
  int updateRowCache(TreeItem item, int r) {
    item.setRow(r);
    Vector chlds = item.getChildren();
    for (int i = 0, s = chlds.size(); i < s; i++) {
      TreeItem chld = (TreeItem) chlds.get(i);
      r++;
      r = updateRowCache(chld, r);
    }
    return r;
  }
  protected int getTreeColumn() {
    return 0;
  }
  public void addKeyboardListener(KeyboardListener listener) {
    if (keyboardListeners == null) {
      keyboardListeners = new KeyboardListenerCollection();
    }
    keyboardListeners.add(listener);
  }
  public void addMouseListener(MouseListener listener) {
    if (mouseListeners == null) {
      mouseListeners = new MouseListenerCollection();
    }
    mouseListeners.add(listener);
  }
  public void addTreeTableListener(TreeTableListener listener) {
    if (listeners == null) {
      listeners = new TreeTableListenerCollection();
    }
    listeners.add(listener);
  }
  /**
   * Clears all tree items from the current tree.
   */
  public void clear() {
    int size = root.getChildCount();
    for (int i = size - 1; i >= 0; i--) {
      root.getChild(i).remove();
    }
  }
  /**
   * Ensures that the currently-selected item is visible, opening its parents
   * and scrolling the tree as necessary.
   */
  public void ensureSelectedItemVisible() {
    if (curSelection == null) {
      return;
    }
    TreeItem parent = curSelection.getParentItem();
    while (parent != null) {
      parent.setState(true);
      parent = parent.getParentItem();
    }
  }
  /**
   * Gets this tree"s default image package.
   * 
   * @return the tree"s image package
   * @see #setImageBase
   */
  public String getImageBase() {
    return imageBase;
  }
  /**
   * Gets the top-level tree item at the specified index.
   * 
   * @param index
   *            the index to be retrieved
   * @return the item at that index
   */
  public TreeItem getItem(int index) {
    return root.getChild(index);
  }
  /**
   * Gets the number of items contained at the root of this tree.
   * 
   * @return this tree"s item count
   */
  public int getItemCount() {
    return root.getChildCount();
  }
  /**
   * Gets the currently selected item.
   * 
   * @return the selected item
   */
  public TreeItem getSelectedItem() {
    return curSelection;
  }
  public void onBrowserEvent(Event event) {
    int eventType = DOM.eventGetType(event);
    switch (eventType) {
    case Event.ONCLICK: {
      Element e = DOM.eventGetTarget(event);
      if (shouldTreeDelegateFocusToElement(e)) {
        // The click event should have given focus to this element
        // already.
        // Avoid moving focus back up to the tree (so that focusable
        // widgets
        // attached to TreeItems can receive keyboard events).
      } else {
//        setFocus(true);
      }
      break;
    }
    case Event.ONMOUSEDOWN: {
      if (mouseListeners != null) {
        mouseListeners.fireMouseEvent(this, event);
      }
      elementClicked(root, DOM.eventGetTarget(event));
      break;
    }
    case Event.ONMOUSEUP: {
      if (mouseListeners != null) {
        mouseListeners.fireMouseEvent(this, event);
      }
      break;
    }
    case Event.ONMOUSEMOVE: {
      if (mouseListeners != null) {
        mouseListeners.fireMouseEvent(this, event);
      }
      break;
    }
    case Event.ONMOUSEOVER: {
      if (mouseListeners != null) {
        mouseListeners.fireMouseEvent(this, event);
      }
      break;
    }
    case Event.ONMOUSEOUT: {
      if (mouseListeners != null) {
        mouseListeners.fireMouseEvent(this, event);
      }
      break;
    }
//    case Event.ONFOCUS:
//      // If we already have focus, ignore the focus event.
//      if (focusListeners != null) {
//        focusListeners.fireFocusEvent(this, event);
//      }
//      break;
//
//    case Event.ONBLUR: {
//      if (focusListeners != null) {
//        focusListeners.fireFocusEvent(this, event);
//      }
//
//      break;
//    }
    case Event.ONKEYDOWN:
      // If nothing"s selected, select the first item.
      if (curSelection == null) {
        if (root.getChildCount() > 0) {
          onSelection(root.getChild(0), true);
        }
        super.onBrowserEvent(event);
        return;
      }
      if (lastEventType == Event.ONKEYDOWN) {
        // Two key downs in a row signal a duplicate event. Multiple key
        // downs can be triggered in the current configuration
        // independent
        // of the browser.
        return;
      }
      // Handle keyboard events
      switch (DOM.eventGetKeyCode(event)) {
      case KeyboardListener.KEY_UP: {
        moveSelectionUp(curSelection);
        DOM.eventPreventDefault(event);
        break;
      }
      case KeyboardListener.KEY_DOWN: {
        moveSelectionDown(curSelection, true);
        DOM.eventPreventDefault(event);
        break;
      }
      case KeyboardListener.KEY_LEFT: {
        if (curSelection.getState()) {
          curSelection.setState(false);
        }
        DOM.eventPreventDefault(event);
        break;
      }
      case KeyboardListener.KEY_RIGHT: {
        if (!curSelection.getState()) {
          curSelection.setState(true);
        }
        DOM.eventPreventDefault(event);
        break;
      }
      }
      // Intentional fallthrough.
    case Event.ONKEYUP:
      if (eventType == Event.ONKEYUP) {
        // If we got here because of a key tab, then we need to make
        // sure the
        // current tree item is selected.
        if (DOM.eventGetKeyCode(event) == KeyboardListener.KEY_TAB) {
          Vector chain = new Vector();
          collectElementChain(chain, getElement(), DOM.eventGetTarget(event));
          TreeItem item = findItemByChain(chain, 0, root);
          if (item != getSelectedItem()) {
            setSelectedItem(item, true);
          }
        }
      }
      // Intentional fallthrough.
    case Event.ONKEYPRESS: {
      if (keyboardListeners != null) {
        keyboardListeners.fireKeyboardEvent(this, event);
      }
      break;
    }
    }
    // We must call SynthesizedWidget"s implementation for all other events.
    super.onBrowserEvent(event);
    lastEventType = eventType;
  }
  
  public void removeKeyboardListener(KeyboardListener listener) {
    if (keyboardListeners != null) {
      keyboardListeners.remove(listener);
    }
  }
  public void removeTreeTableListener(TreeTableListener listener) {
    if (listeners != null) {
      listeners.remove(listener);
    }
  }
  /**
   * Sets the base URL under which this tree will find its default images.
   * These images must be named "tree_white.gif", "tree_open.gif", and
   * "tree_closed.gif".
   */
  public void setImageBase(String baseUrl) {
    imageBase = baseUrl;
    root.updateStateRecursive();
  }
  /**
   * Selects a specified item.
   * 
   * @param item
   *            the item to be selected, or <code>null</code> to deselect
   *            all items
   */
  public void setSelectedItem(TreeItem item) {
    setSelectedItem(item, true);
  }
  /**
   * Selects a specified item.
   * 
   * @param item
   *            the item to be selected, or <code>null</code> to deselect
   *            all items
   * @param fireEvents
   *            <code>true</code> to allow selection events to be fired
   */
  public void setSelectedItem(TreeItem item, boolean fireEvents) {
    if (item == null) {
      if (curSelection == null) {
        return;
      }
      curSelection.setSelected(false);
      curSelection = null;
      return;
    }
    onSelection(item, fireEvents);
  }
  /**
   * Iterator of tree items.
   */
  public Iterator treeItemIterator() {
    List accum = new ArrayList();
    root.addTreeItems(accum);
    return accum.iterator();
  }
  protected void onLoad() {
    root.updateStateRecursive();
    
    renderTable();
    updateVisibility();
  }
  void fireStateChanged(TreeItem item) {
    if (listeners != null) {
      listeners.fireItemStateChanged(item);
    }
  }
  /**
   * Collects parents going up the element tree, terminated at the tree root.
   */
  private void collectElementChain(Vector chain, Element hRoot, Element hElem) {
    if ((hElem == null) || DOM.rupare(hElem, hRoot)) {
      return;
    }
    collectElementChain(chain, hRoot, DOM.getParent(hElem));
    chain.add(hElem);
  }
  private boolean elementClicked(TreeItem root, Element hElem) {
    Vector chain = new Vector();
    collectElementChain(chain, getElement(), hElem);
    TreeItem item = findItemByChain(chain, 0, root);
    if (item != null) {
      if (DOM.rupare(item.getImageElement(), hElem)) {
        item.setState(!item.getState(), true);
        return true;
      } else if (DOM.isOrHasChild(item.getElement(), hElem)) {
        onSelection(item, true);
        return true;
      }
    }
    return false;
  }
  private TreeItem findDeepestOpenChild(TreeItem item) {
    if (!item.getState()) {
      return item;
    }
    return findDeepestOpenChild(item.getChild(item.getChildCount() - 1));
  }
  private TreeItem findItemByChain(Vector chain, int idx, TreeItem root) {
    if (idx == chain.size()) {
      return root;
    }
    for (int i = 0, s = chain.size(); i < s; i++) {
      Element elem = (Element) chain.get(i);
      String n = getNodeName(elem);
      if ("div".equalsIgnoreCase(n)) {
        return findItemByElement(root, elem);
      }
    }
    return null;
  }
  private TreeItem findItemByElement(TreeItem item, Element elem) {
    if (DOM.rupare(item.getElement(), elem)) {
      return item;
    }
    for (int i = 0, n = item.getChildCount(); i < n; ++i) {
      TreeItem child = item.getChild(i);
      child = findItemByElement(child, elem);
      if (child != null) {
        return child;
      }
    }
    return null;
  }
  private native String getNodeName(Element elem) /*-{
    return elem.nodeName;
  }-*/;
  /**
   * Moves to the next item, going into children as if dig is enabled.
   */
  private void moveSelectionDown(TreeItem sel, boolean dig) {
    if (sel == root) {
      return;
    }
    TreeItem parent = sel.getParentItem();
    if (parent == null) {
      parent = root;
    }
    int idx = parent.getChildIndex(sel);
    if (!dig || !sel.getState()) {
      if (idx < parent.getChildCount() - 1) {
        onSelection(parent.getChild(idx + 1), true);
      } else {
        moveSelectionDown(parent, false);
      }
    } else if (sel.getChildCount() > 0) {
      onSelection(sel.getChild(0), true);
    }
  }
  /**
   * Moves the selected item up one.
   */
  private void moveSelectionUp(TreeItem sel) {
    TreeItem parent = sel.getParentItem();
    if (parent == null) {
      parent = root;
    }
    int idx = parent.getChildIndex(sel);
    if (idx > 0) {
      TreeItem sibling = parent.getChild(idx - 1);
      onSelection(findDeepestOpenChild(sibling), true);
    } else {
      onSelection(parent, true);
    }
  }
  private void onSelection(TreeItem item, boolean fireEvents) {
    // "root" isn"t a real item, so don"t let it be selected
    // (some cases in the keyboard handler will try to do this)
    if (item == root) {
      return;
    }
    if (curSelection != null) {
      curSelection.setSelected(false);
    }
    curSelection = item;
    if (curSelection != null) {
//      moveFocus(curSelection);
      // Select the item and fire the selection event.
      curSelection.setSelected(true);
      if (fireEvents && (listeners != null)) {
        listeners.fireItemSelected(curSelection);
      }
    }
  }
  private native boolean shouldTreeDelegateFocusToElement(Element elem) /*-{
    var focus = 
      ((elem.nodeName == "SELECT") || 
      (elem.nodeName == "INPUT")  || 
      (elem.nodeName == "CHECKBOX")
    );
    return focus;
  }-*/;
  public void updateVisibility() {
    for (int i = 0, s = root.getChildCount(); i < s; i++) {
      TreeItem item = root.getChild(i);
      updateVisibility(item);
    }
  }
  protected void updateVisibility(TreeItem item) {
    if (item.isOpen()) {
      showChildren(item);
    } else {
      hideChildren(item);
    }
  }
  void setVisible(boolean visible, int row) {
    UIObject.setVisible(getRowFormatter().getElement(row), visible);
  }
  protected void setVisible(boolean visible, int row, int count) {
    for (int r = row, s = row + count; r < s; r++) {
      setVisible(visible, r);
    }
  }
  public void showChildren(TreeItem item) {
    for (int i = 0, s = item.getChildCount(); i < s; i++) {
      TreeItem child = item.getChild(i);
      setVisible(true, child.getRow());
      if (child.isOpen()) {
        showChildren(child);
      }
    }
  }
  public void hideChildren(TreeItem item) {
    setChildrenVisible(item, false);
  }
  
  public void setChildrenVisible(TreeItem item, boolean visible) {
    if (item.getChildCount() == 0) {
      return;
    }
    int row = item.getRow() + 1;
    int lastChildRow = getLastChildRow(item);
    int count = lastChildRow - row + 1;
    setVisible(visible, row, count);
  }
  protected TreeItem getNextSibling(TreeItem item) {
    TreeItem p = item.getParentItem();
    if (p == null) {
      int idx = root.getChildIndex(item) + 1;
      if (idx < root.getChildCount()) {
        // Gets the next sibling
        return root.getChild(idx);
      }
    } else {
      int idx = p.getChildIndex(item) + 1;
      if (idx < p.getChildCount()) {
        // Gets the next sibling
        return p.getChild(idx);
      }
    }
    return null;
  }
  protected TreeItem getNextNonChild(TreeItem item) {
    TreeItem next = getNextSibling(item);
    if (next != null) {
      return next;
    }
    TreeItem p = item.getParentItem();
    if (p != null) {
      return getNextNonChild(p);
    } else {
      return null;
    }
  }
  public int getLastChildRow(TreeItem item) {
    // Checks the row of the next sibling
    TreeItem next = getNextNonChild(item);
    if (next != null) {
      return next.getRow() - 1;
    }
    return getRowCount() - 1;
  }
  
  public void renderTable() {
    render(root);
  }
  
  /**
   * Renders TreeItems recursively. 
   * @param item
   */
  public void render(TreeItem item) {
    getRenderer().renderTreeItem(this, item, item.getRow());
    if (item.getParentItem() != null) {
      updateVisibility(item.getParentItem());
    }
    
    for (int i = 0, s = item.getChildCount(); i < s; i++) {
      TreeItem child = item.getChild(i);
      render(child);
    }
  }
  public TreeTableRenderer getRenderer() {
    if (renderer == null) {
      renderer = new DefaultRenderer();
    }
    return renderer;
  }
  public void setRenderer(TreeTableRenderer renderer) {
    this.renderer = renderer;
  }
  
//  /**
//   * Move the tree focus to the specified selected item.
//   * 
//   * @param selection
//   */
//  private void moveFocus(TreeItem selection) {
//    HasFocus focusableWidget = selection.getFocusableWidget();
//    if (focusableWidget != null) {
//      focusableWidget.setFocus(true);
//      DOM.scrollIntoView(((Widget) focusableWidget).getElement());
//    } else {
//      // Get the location and size of the given item"s content element
//      // relative
//      // to the tree.
//      Element selectedElem = selection.getContentElem();
//      int containerLeft = getAbsoluteLeft();
//      int containerTop = getAbsoluteTop();
//
//      int left = DOM.getAbsoluteLeft(selectedElem) - containerLeft;
//      int top = DOM.getAbsoluteTop(selectedElem) - containerTop;
//      int width = DOM.getIntAttribute(selectedElem, "offsetWidth");
//      int height = DOM.getIntAttribute(selectedElem, "offsetHeight");
//
//      // Set the focusable element"s position and size to exactly underlap
//      // the
//      // item"s content element.
//      DOM.setIntStyleAttribute(focusable, "left", left);
//      DOM.setIntStyleAttribute(focusable, "top", top);
//      DOM.setIntStyleAttribute(focusable, "width", width);
//      DOM.setIntStyleAttribute(focusable, "height", height);
//
//      // Scroll it into view.
//      DOM.scrollIntoView(focusable);
//
//      // Ensure Focus is set, as focus may have been previously delegated
//      // by
//      // tree.
//      impl.focus(focusable);
//    }
//  }
  
//  public int getTabIndex() {
//    return impl.getTabIndex(focusable);
//  }
//  public void addFocusListener(FocusListener listener) {
//    if (focusListeners == null) {
//      focusListeners = new FocusListenerCollection();
//    }
//    focusListeners.add(listener);
//  }
//  public void removeFocusListener(FocusListener listener) {
//    if (focusListeners != null) {
//      focusListeners.remove(listener);
//    }
//  }
//  public void setAccessKey(char key) {
//    impl.setAccessKey(focusable, key);
//  }
//  public void setFocus(boolean focus) {
//    if (focus) {
//      impl.focus(focusable);
//    } else {
//      impl.blur(focusable);
//    }
//  }
//  public void setTabIndex(int index) {
//    impl.setTabIndex(focusable, index);
//  }
  /**
   * Default renderer for TreeTable. Renders the user object into
   * the TreeItem. Widget user objects are preserved. Arrays are mapped
   * into the row with first object rendered into the TreeItem. All
   * other objects are rendered to the TreeItem with toString().
   */
  class DefaultRenderer implements TreeTableRenderer {
    public void renderTreeItem(TreeTable table, TreeItem item, int row) {
      Object obj = item.getUserObject();
      if (obj instanceof Widget) {
        item.setWidget((Widget) obj);
      } else if (obj instanceof Object[]) {
        Object [] objs = (Object []) obj;
        if (objs.length > 0) {
          Object o = objs[0];
          if (o instanceof Widget) {
            item.setWidget((Widget) o);
          } else if (o != null) {
            item.setHTML(o.toString());
          } else {
            item.setText(null);
          }
          for (int i = 1, s = objs.length; i < s; i++) {
            o = objs[i];
            if (o instanceof Widget) {
              setWidget(row, i, (Widget) o);
            } else if (o != null) {
              setHTML(row, i, o.toString());
            } else {
              setHTML(row, i, null);
            }
          }
        }
      } else if (obj != null) {
        item.setHTML(obj.toString());
      }
    }
  }
  
  public void setWidget(int row, int column, Widget widget) {
    if (column != getTreeColumn()) {
      super.setWidget(row, column, widget);
    } else {
      if (widget instanceof TreeItem) {
        super.setWidget(row, column, widget);
      } else {
        throw new RuntimeException("Cannot add non-TreeItem to tree column");
      }
    }
  }
  
  public void setText(int row, int column, String text) {
    if (column != getTreeColumn()) {
      super.setText(row, column, text);
    } else {
      throw new RuntimeException("Cannot add non-TreeItem to tree column");
    }
  }
  
  public void setHTML(int row, int column, String text) {
    if (column != getTreeColumn()) {
      super.setHTML(row, column, text);
    } else {
      throw new RuntimeException("Cannot add non-TreeItem to tree column");
    }
  }
}