First implementation of TreeView as a widget, without depending on Tree.


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7615 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/bikeshed/src/com/google/gwt/sample/tree/client/MyTreeViewModel.java b/bikeshed/src/com/google/gwt/sample/tree/client/MyTreeViewModel.java
index a5e025f..aff8791 100644
--- a/bikeshed/src/com/google/gwt/sample/tree/client/MyTreeViewModel.java
+++ b/bikeshed/src/com/google/gwt/sample/tree/client/MyTreeViewModel.java
@@ -87,8 +87,6 @@
   public <T> NodeInfo<?> getNodeInfo(T value, TreeNodeView<T> treeNodeView) {
     if (value instanceof String) {
       return getNodeInfoHelper((String) value);
-    } else if (value instanceof Integer) {
-      return getNodeInfoHelper((Integer) value);
     }
 
     // Unhandled type.
@@ -96,9 +94,8 @@
     throw new IllegalArgumentException("Unsupported object type: " + type);
   }
 
-  @SuppressWarnings("unused")
-  private NodeInfo<?> getNodeInfoHelper(final Integer value) {
-    return null;
+  public boolean isLeaf(Object value) {
+    return value instanceof Integer;
   }
 
   private NodeInfo<?> getNodeInfoHelper(final String value) {
diff --git a/bikeshed/src/com/google/gwt/sample/tree/client/TreeNodeView.java b/bikeshed/src/com/google/gwt/sample/tree/client/TreeNodeView.java
index 4cc54fa..87f8576 100644
--- a/bikeshed/src/com/google/gwt/sample/tree/client/TreeNodeView.java
+++ b/bikeshed/src/com/google/gwt/sample/tree/client/TreeNodeView.java
@@ -16,12 +16,9 @@
 package com.google.gwt.sample.tree.client;
 
 import com.google.gwt.cells.client.Cell;
-import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.event.shared.GwtEvent;
-import com.google.gwt.event.shared.HandlerManager;
-import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
 import com.google.gwt.list.shared.ListEvent;
 import com.google.gwt.list.shared.ListHandler;
 import com.google.gwt.list.shared.ListModel;
@@ -29,7 +26,6 @@
 import com.google.gwt.list.shared.SizeChangeEvent;
 import com.google.gwt.sample.tree.client.TreeViewModel.NodeInfo;
 import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.TreeItem;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -42,61 +38,9 @@
 public class TreeNodeView<T> extends Composite {
 
   /**
-   * A {@link TreeItem} that fires value change events when the state changes.
+   * The element used in place of an image when a node has no children.
    */
-  public static class ExtraTreeItem extends TreeItem implements
-      HasValueChangeHandlers<Boolean> {
-
-    private HandlerManager handlerManager = new HandlerManager(this);
-
-    public ExtraTreeItem(String value) {
-      super(value);
-    }
-
-    public HandlerRegistration addValueChangeHandler(
-        ValueChangeHandler<Boolean> handler) {
-      return handlerManager.addHandler(ValueChangeEvent.getType(), handler);
-    }
-
-    public void fireEvent(GwtEvent<?> event) {
-      handlerManager.fireEvent(event);
-    }
-
-    @Override
-    public void setState(boolean open, boolean fireEvents) {
-      super.setState(open, fireEvents);
-      if (open) {
-        ValueChangeEvent.fire(this, true);
-      } else {
-        ValueChangeEvent.fire(this, false);
-      }
-    }
-  }
-
-  /**
-   * The list registration for the list of children.
-   */
-  private ListRegistration listReg;
-
-  /**
-   * The TreeItem that displays this node.
-   */
-  private ExtraTreeItem treeItem;
-
-  /**
-   * This node's value.
-   */
-  private T value;
-
-  /**
-   * The parent {@link TreeNodeView}.
-   */
-  private TreeNodeView<?> parent;
-
-  /**
-   * The containing {@link TreeView}.
-   */
-  private TreeView tree;
+  private static final String LEAF_IMAGE = "<div style='position:absolute;display:none;'></div>";
 
   /**
    * The children of this {@link TreeNodeView}.
@@ -104,29 +48,67 @@
   private List<TreeNodeView<?>> children;
 
   /**
-   * The info about the child nodes.
+   * A reference to the element that contains the children.
+   */
+  private Element childContainer;
+
+  /**
+   * The list registration for the list of children.
+   */
+  private ListRegistration listReg;
+
+  /**
+   * The info about children of this node.
    */
   private NodeInfo<?> nodeInfo;
 
   /**
+   * Indicates whether or not we've loaded the node info.
+   */
+  private boolean nodeInfoLoaded;
+
+  /**
+   * Indicates whether or not this node is open.
+   */
+  private boolean open;
+
+  /**
+   * The parent {@link TreeNodeView}.
+   */
+  private TreeNodeView<?> parent;
+
+  /**
    * The info about this node.
    */
   private NodeInfo<T> parentNodeInfo;
 
   /**
+   * The containing {@link TreeView}.
+   */
+  private TreeView tree;
+
+  /**
+   * This node's value.
+   */
+  private T value;
+
+  /**
    * Construct a {@link TreeNodeView}.
    * 
-   * @param value the value of this node
    * @param tree the parent {@link TreeView}
-   * @param treeItem this nodes view
+   * @param parent the parent {@link TreeNodeView}
+   * @param parentNodeInfo the {@link NodeInfo} of the parent
+   * @param elem the outer element of this {@link TreeNodeView}.
+   * @param value the value of this node
    */
-  TreeNodeView(T value, final TreeView tree, final TreeNodeView<?> parent,
-      ExtraTreeItem treeItem, NodeInfo<T> parentNodeInfo) {
+  TreeNodeView(final TreeView tree, final TreeNodeView<?> parent,
+      NodeInfo<T> parentNodeInfo, Element elem, T value) {
     this.value = value;
     this.tree = tree;
     this.parent = parent;
-    this.treeItem = treeItem;
+    // We pass in parentNodeInfo so we know that it is type T.
     this.parentNodeInfo = parentNodeInfo;
+    setElement(elem);
   }
 
   /**
@@ -158,6 +140,15 @@
   }
 
   /**
+   * Check whether or not this {@link TreeNodeView} is open.
+   * 
+   * @return true if open, false if closed
+   */
+  public boolean getState() {
+    return open;
+  }
+
+  /**
    * Get the value contained in this node.
    * 
    * @return the value of the node
@@ -166,90 +157,180 @@
     return value;
   }
 
-  NodeInfo<?> getNodeInfo() {
-    return nodeInfo;
+  /**
+   * 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
+   */
+  public void setState(boolean open, boolean fireEvents) {
+    // TODO(jlabanca) - allow people to add open/close handlers.
+
+    // Early out.
+    if (this.open == open) {
+      return;
+    }
+
+    this.open = open;
+    if (open) {
+      if (!nodeInfoLoaded) {
+        nodeInfo = tree.getTreeViewModel().getNodeInfo(value, this);
+        nodeInfoLoaded = true;
+      }
+      if (nodeInfo != null) {
+        onOpen(nodeInfo);
+      }
+    } else {
+      // Unregister the list handler.
+      if (listReg != null) {
+        listReg.removeHandler();
+        listReg = null;
+      }
+
+      // Remove the children.
+      childContainer.setInnerHTML("");
+      children.clear();
+    }
+
+    // Update the image.
+    updateImage();
+  }
+
+  /**
+   * Fire an event to the {@link Cell}.
+   * 
+   * @param event the native event
+   */
+  void fireEventToCell(NativeEvent event) {
+    parentNodeInfo.onBrowserEvent(getCellParent(), value, event);
+  }
+
+  /**
+   * @return the element that contains the rendered cell
+   */
+  Element getCellParent() {
+    return getElement().getChild(1).cast();
+  }
+
+  /**
+   * @return the image element
+   */
+  Element getImageElement() {
+    return getElement().getFirstChildElement();
   }
 
   NodeInfo<T> getParentNodeInfo() {
     return parentNodeInfo;
   }
 
-  TreeItem getTreeItem() {
-    return treeItem;
-  }
-
   /**
-   * Initialize the node info.
+   * Set the {@link Element} that will contain the children. Used by
+   * {@link TreeView}.
    * 
-   * @param nodeInfo the {@link NodeInfo} that provides information about the
-   *          child values
+   * @param elem the child container element
    */
-  void initNodeInfo(final NodeInfo<?> nodeInfo) {
-    // Force a + icon if this node might have children.
-    if (nodeInfo != null) {
-      this.nodeInfo = nodeInfo;
-      treeItem.addItem("loading...");
-      treeItem.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
-        public void onValueChange(ValueChangeEvent<Boolean> event) {
-          if (event.getValue()) {
-            onOpen(tree, nodeInfo);
-          } else {
-            onClose();
-          }
-        }
-      });
-    }
-  }
-
-  /**
-   * Cleanup when the node is closed.
-   */
-  private void onClose() {
-    if (listReg != null) {
-      listReg.removeHandler();
-      listReg = null;
-    }
+  void initChildContainer(Element elem) {
+    assert this.childContainer == null : "childContainer already initialized.";
+    this.childContainer = elem;
   }
 
   /**
    * Setup the node when it is opened.
    * 
-   * @param tree the parent {@link TreeView}
    * @param nodeInfo the {@link NodeInfo} that provides information about the
    *          child values
    * @param <C> the child data type of the node.
    */
-  private <C> void onOpen(final TreeView tree, final NodeInfo<C> nodeInfo) {
+  private <C> void onOpen(final NodeInfo<C> nodeInfo) {
+    // Get the node info.
     ListModel<C> listModel = nodeInfo.getListModel();
     listReg = listModel.addListHandler(new ListHandler<C>() {
       public void onDataChanged(ListEvent<C> event) {
         // TODO - handle event start and length
-        treeItem.removeItems();
 
-        // Add child tree items.
+        // Construct the child contents.
+        TreeViewModel model = tree.getTreeViewModel();
+        int imageWidth = tree.getImageWidth();
         Cell<C> theCell = nodeInfo.getCell();
+        StringBuilder sb = new StringBuilder();
         children = new ArrayList<TreeNodeView<?>>();
         for (C childValue : event.getValues()) {
-          // TODO(jlabanca): Use one StringBuilder.
-          StringBuilder sb = new StringBuilder();
+          sb.append("<div style=\"position:relative;padding-left:");
+          sb.append(imageWidth);
+          sb.append("px;\">");
+          if (model.isLeaf(childValue)) {
+            sb.append(LEAF_IMAGE);
+          } else {
+            sb.append(tree.getClosedImageHtml());
+          }
+          sb.append("<div>");
           theCell.render(childValue, sb);
-          ExtraTreeItem child = new ExtraTreeItem(sb.toString());
-          treeItem.addItem(child);
-          children.add(tree.createChildView(childValue, TreeNodeView.this,
-              child, nodeInfo));
+          sb.append("</div>");
+          sb.append("</div>");
+        }
+
+        // Replace contents of the child container.
+        if (childContainer == null) {
+          Element elem = getElement();
+          initChildContainer(Document.get().createDivElement());
+          elem.appendChild(childContainer);
+        }
+        childContainer.setInnerHTML(sb.toString());
+
+        // Create the child TreeNodeViews from the new elements.
+        children = new ArrayList<TreeNodeView<?>>();
+        Element childElem = childContainer.getFirstChildElement();
+        for (C childValue : event.getValues()) {
+          TreeNodeView<C> child = new TreeNodeView<C>(tree, TreeNodeView.this,
+              nodeInfo, childElem, childValue);
+          children.add(child);
+          childElem = childElem.getNextSiblingElement();
         }
       }
 
       public void onSizeChanged(SizeChangeEvent event) {
-        // TODO (jlabanca): Handle case when item is over.
+        if (children == null) {
+          return;
+        }
+
+        // Shrink the list based on the new size.
         int size = event.getSize();
-        treeItem.removeItems();
-        if (size > 0) {
-          // Add a placeholder to force a + icon.
-          treeItem.addItem("loading...");
+        int currentSize = children.size();
+        for (int i = currentSize - 1; i >= size; i--) {
+          childContainer.getLastChild().removeFromParent();
+          children.remove(i);
         }
       }
     });
     listReg.setRangeOfInterest(0, 100);
   }
+
+  /**
+   * Update the image based on the current state.
+   */
+  private void updateImage() {
+    // Early out if this is a root node.
+    if (getParentTreeNodeView() == null) {
+      return;
+    }
+
+    // Replace the image element with a new one.
+    String html = open ? tree.getOpenImageHtml() : tree.getClosedImageHtml();
+    if (nodeInfoLoaded && nodeInfo == null) {
+      html = LEAF_IMAGE;
+    }
+    Element tmp = Document.get().createDivElement();
+    tmp.setInnerHTML(html);
+    Element imageElem = tmp.getFirstChildElement();
+    getElement().replaceChild(imageElem, getImageElement());
+  }
 }
diff --git a/bikeshed/src/com/google/gwt/sample/tree/client/TreeView.java b/bikeshed/src/com/google/gwt/sample/tree/client/TreeView.java
index 20c91d1..d31d99b 100644
--- a/bikeshed/src/com/google/gwt/sample/tree/client/TreeView.java
+++ b/bikeshed/src/com/google/gwt/sample/tree/client/TreeView.java
@@ -15,22 +15,47 @@
  */
 package com.google.gwt.sample.tree.client;
 
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.sample.tree.client.TreeNodeView.ExtraTreeItem;
-import com.google.gwt.sample.tree.client.TreeViewModel.NodeInfo;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.ImageResource;
 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.Composite;
-import com.google.gwt.user.client.ui.Tree;
-import com.google.gwt.user.client.ui.TreeItem;
+import com.google.gwt.user.client.ui.AbstractImagePrototype;
+import com.google.gwt.user.client.ui.Widget;
 
 import java.util.ArrayList;
 
 /**
  * A view of a tree.
  */
-public class TreeView extends Composite {
+public class TreeView extends Widget {
+
+  private static final Resources DEFAULT_RESOURCES = GWT.create(Resources.class);
+
+  /**
+   * A ClientBundle that provides images for this widget.
+   */
+  public interface Resources extends ClientBundle {
+
+    /**
+     * An image indicating a closed branch.
+     */
+    ImageResource treeClosed();
+
+    /**
+     * An image indicating an open branch.
+     */
+    ImageResource treeOpen();
+  }
+
+  /**
+   * The HTML used to generate the closed image.
+   */
+  private String closedImageHtml;
 
   /**
    * The {@link TreeViewModel} that backs the tree.
@@ -38,6 +63,16 @@
   private TreeViewModel model;
 
   /**
+   * The HTML used to generate the open image.
+   */
+  private String openImageHtml;
+
+  /**
+   * The Resources used by this tree.
+   */
+  private Resources resources;
+
+  /**
    * The hidden root node in the tree.
    */
   private TreeNodeView<?> rootNode;
@@ -51,56 +86,72 @@
    */
   public <T> TreeView(TreeViewModel viewModel, T rootValue) {
     this.model = viewModel;
+    this.resources = DEFAULT_RESOURCES;
+    setElement(Document.get().createDivElement());
+    getElement().getStyle().setPosition(Position.RELATIVE);
+    setStyleName("gwt-TreeView");
 
-    // Initialize the widget.
-    Tree tree = new Tree() {
-      @Override
-      public void onBrowserEvent(Event event) {
-        super.onBrowserEvent(event);
-
-        switch (event.getTypeInt()) {
-          case Event.ONMOUSEUP: {
-            if ((DOM.eventGetCurrentTarget(event) == getElement())
-                && (event.getButton() == Event.BUTTON_LEFT)) {
-              elementClicked(DOM.eventGetTarget(event), event);
-            }
-            break;
-          }
-        }
-      }
-    };
-    initWidget(tree);
-    sinkEvents(Event.ONMOUSEUP);
-
-    // Add the root item.
-    ExtraTreeItem rootItem = new ExtraTreeItem("Dummy UI Root");
-    tree.addItem(rootItem);
+    // Add event handlers.
+    sinkEvents(Event.ONCLICK | Event.ONMOUSEDOWN | Event.ONMOUSEUP);
 
     // Associate a view with the item.
-    rootNode = createChildView(rootValue, null, rootItem, null);
+    rootNode = new TreeNodeView<T>(this, null, null, getElement(), rootValue);
+    rootNode.initChildContainer(getElement());
+    rootNode.setState(true);
   }
 
   public TreeViewModel getTreeViewModel() {
     return model;
   }
 
+  @Override
+  public void onBrowserEvent(Event event) {
+    super.onBrowserEvent(event);
+
+    int eventType = DOM.eventGetType(event);
+    switch (eventType) {
+      case Event.ONMOUSEUP:
+        Element currentTarget = event.getCurrentEventTarget().cast();
+        if (currentTarget == getElement()) {
+          Element target = event.getEventTarget().cast();
+          elementClicked(target, event);
+        }
+        break;
+    }
+  }
+
   /**
-   * Create a child view for this tree.
-   * 
-   * @param <C> the data type of the child
-   * @param childValue the value of the child
-   * @param parentTreeNodeView the parent {@link TreeNodeView}
-   * @param childItem the DOM view of the child
-   * @return a {@link TreeNodeView} for the child
+   * @return the HTML to render the closed image.
    */
-  <C> TreeNodeView<C> createChildView(C childValue,
-      TreeNodeView<?> parentTreeNodeView, ExtraTreeItem childItem,
-      NodeInfo<C> parentNodeInfo) {
-    TreeNodeView<C> childView = new TreeNodeView<C>(childValue, this,
-        parentTreeNodeView, childItem, parentNodeInfo);
-    NodeInfo<?> nodeInfo = model.getNodeInfo(childValue, childView);
-    childView.initNodeInfo(nodeInfo);
-    return childView;
+  String getClosedImageHtml() {
+    if (closedImageHtml == null) {
+      AbstractImagePrototype proto = AbstractImagePrototype.create(resources.treeClosed());
+      closedImageHtml = proto.getHTML().replace("style='",
+          "style='position:absolute;left:0px;top:0px;");
+    }
+    return closedImageHtml;
+  }
+
+  /**
+   * Get the width required for the images.
+   * 
+   * @return the maximum width required for images.
+   */
+  int getImageWidth() {
+    return Math.max(resources.treeClosed().getWidth(),
+        resources.treeOpen().getWidth());
+  }
+
+  /**
+   * @return the HTML to render the open image.
+   */
+  String getOpenImageHtml() {
+    if (openImageHtml == null) {
+      AbstractImagePrototype proto = AbstractImagePrototype.create(resources.treeOpen());
+      openImageHtml = proto.getHTML().replace("style='",
+          "style='position:absolute;left:0px;top:0px;");
+    }
+    return openImageHtml;
   }
 
   /**
@@ -112,7 +163,7 @@
       return;
     }
 
-    collectElementChain(chain, hRoot, DOM.getParent(hElem));
+    collectElementChain(chain, hRoot, hElem.getParentElement());
     chain.add(hElem);
   }
 
@@ -122,9 +173,11 @@
 
     TreeNodeView<?> nodeView = findItemByChain(chain, 0, rootNode);
     if (nodeView != null && nodeView != rootNode) {
-      TreeItem item = nodeView.getTreeItem();
-      if (DOM.isOrHasChild(item.getElement(), hElem)) {
-        fireEvent(nodeView, event);
+      if (nodeView.getImageElement().isOrHasChild(hElem)) {
+        nodeView.setState(!nodeView.getState(), true);
+        return true;
+      } else if (nodeView.getCellParent().isOrHasChild(hElem)) {
+        nodeView.fireEventToCell(event);
         return true;
       }
     }
@@ -141,7 +194,7 @@
     Element hCurElem = chain.get(idx);
     for (int i = 0, n = parent.getChildCount(); i < n; ++i) {
       TreeNodeView<?> child = parent.getChild(i);
-      if (child.getTreeItem().getElement() == hCurElem) {
+      if (child.getElement() == hCurElem) {
         TreeNodeView<?> retItem = findItemByChain(chain, idx + 1, child);
         if (retItem == null) {
           return child;
@@ -152,9 +205,4 @@
 
     return findItemByChain(chain, idx + 1, parent);
   }
-
-  private <T> void fireEvent(TreeNodeView<T> nodeView, NativeEvent event) {
-    nodeView.getParentNodeInfo().onBrowserEvent(
-        nodeView.getTreeItem().getElement(), nodeView.getValue(), event);
-  }
 }
diff --git a/bikeshed/src/com/google/gwt/sample/tree/client/TreeViewModel.java b/bikeshed/src/com/google/gwt/sample/tree/client/TreeViewModel.java
index 20aacbe..d45a9e9 100644
--- a/bikeshed/src/com/google/gwt/sample/tree/client/TreeViewModel.java
+++ b/bikeshed/src/com/google/gwt/sample/tree/client/TreeViewModel.java
@@ -109,4 +109,13 @@
    * @return the {@link NodeInfo}
    */
   <T> NodeInfo<?> getNodeInfo(T value, TreeNodeView<T> treeNodeView);
+
+  /**
+   * Check if the value is known to be a leaf node.
+   * 
+   * @param value the value at the node
+   * @return true if the node is known to be a leaf node, false if not
+   */
+
+  boolean isLeaf(Object value);
 }
diff --git a/bikeshed/src/com/google/gwt/sample/tree/client/treeClosed.gif b/bikeshed/src/com/google/gwt/sample/tree/client/treeClosed.gif
new file mode 100644
index 0000000..7bda586
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/sample/tree/client/treeClosed.gif
Binary files differ
diff --git a/bikeshed/src/com/google/gwt/sample/tree/client/treeOpen.gif b/bikeshed/src/com/google/gwt/sample/tree/client/treeOpen.gif
new file mode 100644
index 0000000..0fcf791
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/sample/tree/client/treeOpen.gif
Binary files differ