Add side-by-side trees to the bikeshed

Review at http://gwt-code-reviews.appspot.com/160802


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7669 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockSample.java b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockSample.java
index 5539171..c2b6210 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockSample.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/StockSample.java
@@ -31,7 +31,8 @@
 import com.google.gwt.bikeshed.sample.stocks.shared.StockRequest;
 import com.google.gwt.bikeshed.sample.stocks.shared.StockResponse;
 import com.google.gwt.bikeshed.sample.stocks.shared.Transaction;
-import com.google.gwt.bikeshed.tree.client.TreeView;
+import com.google.gwt.bikeshed.tree.client.SideBySideTreeView;
+import com.google.gwt.bikeshed.tree.client.StandardTreeView;
 import com.google.gwt.core.client.EntryPoint;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.KeyUpEvent;
@@ -177,8 +178,9 @@
   
   private PagingTableListView<Transaction> transactionTable;
   
-  private TreeView transactionTree;
+  private StandardTreeView transactionTree1;
 
+  private SideBySideTreeView transactionTree2;
   /**
    * The timer used to update the stock quotes.
    */
@@ -244,9 +246,13 @@
     transactionTable.addColumn(Columns.subtotalColumn);
     
     // Create the transactions tree.
-    transactionTree = new TreeView(new TransactionTreeViewModel(favoritesListModel,
+    transactionTree1 = new StandardTreeView(new TransactionTreeViewModel(favoritesListModel,
         transactionListListModelsByTicker), null);
-    transactionTree.setAnimationEnabled(true);
+    transactionTree1.setAnimationEnabled(true);
+    
+    // Create the transactions tree.
+    transactionTree2 = new SideBySideTreeView(new TransactionTreeViewModel(favoritesListModel,
+        transactionListListModelsByTicker), null);
 
     Columns.favoriteColumn.setFieldUpdater(new FieldUpdater<StockQuote, Boolean>() {
       public void update(StockQuote object, Boolean value) {
@@ -319,7 +325,9 @@
     RootPanel.get().add(new HTML("<hr>"));
     RootPanel.get().add(transactionTable);
     RootPanel.get().add(new HTML("<hr>"));
-    RootPanel.get().add(transactionTree);
+    RootPanel.get().add(transactionTree1);
+    RootPanel.get().add(new HTML("<hr>"));
+    RootPanel.get().add(transactionTree2);
 
     // Add a handler to send the name to the server
     queryField.addKeyUpHandler(new KeyUpHandler() {
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/TransactionTreeViewModel.java b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/TransactionTreeViewModel.java
index fcaf4b2..a44d1d4 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/TransactionTreeViewModel.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/stocks/client/TransactionTreeViewModel.java
@@ -16,10 +16,11 @@
 package com.google.gwt.bikeshed.sample.stocks.client;
 
 import com.google.gwt.bikeshed.cells.client.Cell;
-import com.google.gwt.bikeshed.cells.client.TextCell;
 import com.google.gwt.bikeshed.list.shared.ListListModel;
+import com.google.gwt.bikeshed.list.shared.ListModel;
+import com.google.gwt.bikeshed.sample.stocks.shared.StockQuote;
 import com.google.gwt.bikeshed.sample.stocks.shared.Transaction;
-import com.google.gwt.bikeshed.tree.client.TreeNodeView;
+import com.google.gwt.bikeshed.tree.client.TreeNode;
 import com.google.gwt.bikeshed.tree.client.TreeViewModel;
 
 import java.util.Map;
@@ -57,7 +58,8 @@
     this.transactionListListModelsByTicker = transactionListListModelsByTicker;
   }
 
-  public <T> NodeInfo<?> getNodeInfo(T value, TreeNodeView<T> treeNodeView) {
+  @SuppressWarnings("unused")
+  public <T> NodeInfo<?> getNodeInfo(T value, TreeNode<T> treeNode) {
     if (value == null) {
       return new TreeViewModel.DefaultNodeInfo<StockQuote>(stockQuoteListModel,
           STOCK_QUOTE_CELL) {
@@ -67,8 +69,14 @@
         }
       };
     } else if (value instanceof StockQuote) {
-      return new TreeViewModel.DefaultNodeInfo<Transaction>(
-          transactionListListModelsByTicker.get(((StockQuote) value).getTicker()), TRANSACTION_CELL);
+      String ticker = ((StockQuote) value).getTicker();
+      ListListModel<Transaction> listModel = transactionListListModelsByTicker.get(ticker);
+      if (listModel == null) {
+        listModel = new ListListModel<Transaction>();
+        transactionListListModelsByTicker.put(ticker, listModel);
+      }
+      return new TreeViewModel.DefaultNodeInfo<Transaction>(listModel,
+          TRANSACTION_CELL);
     }
     
     throw new IllegalArgumentException(value.toString());
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/tree/client/MyTreeViewModel.java b/bikeshed/src/com/google/gwt/bikeshed/sample/tree/client/MyTreeViewModel.java
index 89474f5..c0aaba5 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/sample/tree/client/MyTreeViewModel.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/tree/client/MyTreeViewModel.java
@@ -20,7 +20,7 @@
 import com.google.gwt.bikeshed.cells.client.ValueUpdater;
 import com.google.gwt.bikeshed.list.shared.AsyncListModel;
 import com.google.gwt.bikeshed.list.shared.ListModel;
-import com.google.gwt.bikeshed.tree.client.TreeNodeView;
+import com.google.gwt.bikeshed.tree.client.TreeNode;
 import com.google.gwt.bikeshed.tree.client.TreeViewModel;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.user.client.Timer;
@@ -93,7 +93,7 @@
     }
   };
 
-  public <T> NodeInfo<?> getNodeInfo(T value, TreeNodeView<T> treeNodeView) {
+  public <T> NodeInfo<?> getNodeInfo(T value, TreeNode<T> treeNode) {
     if (value instanceof String) {
       return getNodeInfoHelper((String) value);
     }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/sample/tree/client/TreeSample.java b/bikeshed/src/com/google/gwt/bikeshed/sample/tree/client/TreeSample.java
index 5a59f8b..3063d4b 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/sample/tree/client/TreeSample.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/sample/tree/client/TreeSample.java
@@ -15,8 +15,10 @@
  */
 package com.google.gwt.bikeshed.sample.tree.client;
 
-import com.google.gwt.bikeshed.tree.client.TreeView;
+import com.google.gwt.bikeshed.tree.client.SideBySideTreeView;
+import com.google.gwt.bikeshed.tree.client.StandardTreeView;
 import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.user.client.ui.HTML;
 import com.google.gwt.user.client.ui.RootPanel;
 
 /**
@@ -25,8 +27,13 @@
 public class TreeSample implements EntryPoint {
 
   public void onModuleLoad() {
-    TreeView tree = new TreeView(new MyTreeViewModel(), "...");
+    StandardTreeView tree = new StandardTreeView(new MyTreeViewModel(), "...");
     tree.setAnimationEnabled(true);
     RootPanel.get().add(tree);
+
+    RootPanel.get().add(new HTML("<hr>"));
+    
+    SideBySideTreeView sstree = new SideBySideTreeView(new MyTreeViewModel(), "...");
+    RootPanel.get().add(sstree);
   }
 }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeNodeView.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeNodeView.java
new file mode 100644
index 0000000..8ac9dd9
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeNodeView.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2010 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.google.gwt.bikeshed.tree.client;
+
+import static com.google.gwt.bikeshed.tree.client.SideBySideTreeView.COLUMN_WIDTH;
+
+import com.google.gwt.bikeshed.cells.client.Cell;
+import com.google.gwt.bikeshed.tree.client.TreeViewModel.NodeInfo;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
+
+import java.util.List;
+
+/**
+ * A tree view that displays each level in a side-by-side manner.
+ * 
+ * @param <T> the type that this {@link TreeNodeView} contains
+ */
+public class SideBySideTreeNodeView<T> extends TreeNodeView<T> {
+
+  private final int imageLeft;
+
+  private int level;
+
+  private String path;
+
+  /**
+   * Construct a {@link TreeNodeView}.
+   * 
+   * @param tree the parent {@link TreeView}
+   * @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
+   */
+  SideBySideTreeNodeView(final TreeView tree, final SideBySideTreeNodeView<?> parent,
+      NodeInfo<T> parentNodeInfo, Element elem, T value, int level, String path) {
+    super(tree, parent, parentNodeInfo, value);
+    this.imageLeft = 85 - tree.getImageWidth();
+    this.level = level;
+    this.path = path;
+    
+    setElement(elem);
+  }
+
+  @Override
+  protected <C> TreeNodeView<C> createTreeNodeView(NodeInfo<C> nodeInfo,
+      Element childElem, C childValue, int idx) {
+    return new SideBySideTreeNodeView<C>(tree,
+        SideBySideTreeNodeView.this, nodeInfo, childElem, childValue,
+        level + 1, path + "-" + idx);
+  }
+  
+  @Override
+  protected <C> void emitHtml(StringBuilder sb, NodeInfo<C> nodeInfo,
+      List<C> childValues, List<TreeNodeView<?>> savedViews) {
+    TreeViewModel model = tree.getTreeViewModel();
+    int imageWidth = tree.getImageWidth();
+    Cell<C> theCell = nodeInfo.getCell();
+
+    int idx = 0;
+    for (C childValue : childValues) {
+      sb.append("<div id=\"" + path + "-" + idx + "\" style=\"position:relative;padding-right:");
+      sb.append(imageWidth);
+      sb.append("px;\">");
+      if (savedViews.get(idx) != null) {
+        sb.append(tree.getOpenImageHtml(imageLeft));
+      } else if (model.isLeaf(childValue)) {
+        sb.append(LEAF_IMAGE);
+      } else {
+        sb.append(tree.getClosedImageHtml(imageLeft));
+      }
+      sb.append("<div class=\"gwt-sstree-cell\">");
+      theCell.render(childValue, sb);
+      sb.append("</div></div>");
+      
+      idx++;
+    }
+  }
+
+  /**
+   * Ensure that the child container exists and return it.
+   * 
+   * @return the child container
+   */
+  @Override
+  protected Element ensureChildContainer() {
+    if (childContainer == null) {
+      // Create the container within the top-level widget element.
+      Element container = createContainer(level);
+      container.setInnerHTML("");
+      Element animFrame = container.appendChild(
+          Document.get().createDivElement());
+      animFrame.getStyle().setPosition(Position.RELATIVE);
+      childContainer = animFrame.appendChild(Document.get().createDivElement());
+    }
+
+    return childContainer;
+  }
+
+  /**
+   * @return the element that contains the rendered cell
+   */
+  @Override
+  protected Element getCellParent() {
+    return getElement().getChild(1).cast();
+  }
+  
+  /**
+   * @return the image element
+   */
+  @Override
+  protected Element getImageElement() {
+    return getElement().getFirstChildElement();
+  }
+
+  @Override
+  protected int getImageLeft() {
+    return imageLeft;
+  }
+  
+  @Override
+  protected void postClose() {
+    destroyContainer(level);
+  }
+
+  @Override
+  protected void preOpen() {
+    // Close siblings of this node
+    TreeNodeView<?> parentNode = getParentTreeNodeView();
+    if (parentNode != null) {
+      int numSiblings = parentNode.getChildCount();
+      for (int i = 0; i < numSiblings; i++) {
+        Element container = parentNode.getChildContainer().getChild(i).cast();
+
+        TreeNodeView<?> sibling = parentNode.getChildTreeNodeView(i);
+        if (sibling == this) {
+          container.setClassName("gwt-SideBySideTree-selectedItem");
+        } else {
+          if (sibling.getState()) {
+            sibling.setState(false);
+            container.setClassName("gwt-SideBySideTree-unselectedItem");
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Returns the container for child nodes at the given level.
+   */
+  private Element createContainer(int level) {
+    // Resize the root element
+    Element rootElement = tree.getElement();
+    rootElement.getStyle().setWidth((level + 1) * COLUMN_WIDTH, Unit.PX);
+    
+    // Create children of the root container as needed.
+    int childCount = rootElement.getChildCount();
+    while (childCount <= level) {
+      Element div = rootElement.appendChild(Document.get().createDivElement());
+      div.setClassName("gwt-SideBySideTreeColumn");
+      Style style = div.getStyle();
+      style.setPosition(Position.ABSOLUTE);
+      style.setTop(0, Unit.PX);
+      style.setLeft(level * COLUMN_WIDTH, Unit.PX);
+      
+      childCount++;
+    }
+    
+    return rootElement.getChild(level).cast();
+  }
+
+  /**
+   * Destroys the containers for child nodes at the given level and all
+   * subsequent levels.
+   */
+  private void destroyContainer(int level) {
+    // Resize the root element
+    Element rootElement = tree.getElement();
+    rootElement.getStyle().setWidth((level + 1) * COLUMN_WIDTH, Unit.PX);
+    
+    // Create children of the root container as needed.
+    int childCount = rootElement.getChildCount();
+    while (childCount > level) {
+      rootElement.removeChild(rootElement.getLastChild());
+      childCount--;
+    }
+    
+    childContainer = null;
+  }
+}
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeView.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeView.java
new file mode 100644
index 0000000..cab7067
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeView.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2010 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.google.gwt.bikeshed.tree.client;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+
+/**
+ * A view of a tree.
+ */
+public class SideBySideTreeView extends TreeView {
+  
+  protected static final int COLUMN_HEIGHT = 200;
+
+  protected static final int COLUMN_WIDTH = 100;
+
+  /**
+   * Construct a new {@link TreeView}.
+   * 
+   * @param <T> the type of data in the root node
+   * @param viewModel the {@link TreeViewModel} that backs the tree
+   * @param rootValue the hidden root value of the tree
+   */
+  public <T> SideBySideTreeView(TreeViewModel viewModel, T rootValue) {
+    super(viewModel);
+    
+    Element rootElement = Document.get().createDivElement();
+    rootElement.setClassName("gwt-SideBySideTreeView");
+    Style style = rootElement.getStyle();
+    style.setPosition(Position.RELATIVE);
+    style.setWidth(COLUMN_WIDTH, Unit.PX);
+    style.setHeight(COLUMN_HEIGHT, Unit.PX);
+    setElement(rootElement);
+
+    // Add event handlers.
+    sinkEvents(Event.ONCLICK | Event.ONMOUSEDOWN | Event.ONMOUSEUP);
+
+    // Associate a view with the item.
+    TreeNodeView<T> root = new SideBySideTreeNodeView<T>(this, null, null,
+        rootElement, rootValue, 0, "gwt-sstree");
+    setRootNode(root);
+    root.setState(true);
+  }
+
+  @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();
+          String id = target.getId();
+          boolean inCell = target.getClassName().equals("gwt-sstree-cell");
+          while (id == null || !id.startsWith("gwt-sstree")) {
+            target = target.getParentElement();
+            if (target == null) {
+              return;
+            }
+
+            id = target.getId();
+            inCell |= target.getClassName().equals("gwt-sstree-cell");
+          }
+
+          if (id.startsWith("gwt-sstree-")) {
+            id = id.substring(11);
+            String[] path = id.split("-");
+
+            TreeNodeView<?> nodeView = rootNode;
+            for (String s : path) {
+              nodeView = nodeView.getChildTreeNodeView(Integer.parseInt(s));
+            }
+            if (inCell) {
+              nodeView.fireEventToCell(event);
+            } else {
+              nodeView.setState(!nodeView.getState());
+            }
+          }
+        }
+        break;
+    }
+  }
+}
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeNodeView.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeNodeView.java
new file mode 100644
index 0000000..fcdb562
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeNodeView.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2010 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.google.gwt.bikeshed.tree.client;
+
+import com.google.gwt.bikeshed.cells.client.Cell;
+import com.google.gwt.bikeshed.tree.client.TreeViewModel.NodeInfo;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Overflow;
+import com.google.gwt.dom.client.Style.Position;
+
+import java.util.List;
+
+/**
+ * A view of a tree node.
+ * 
+ * @param <T> the type that this {@link TreeNodeView} contains
+ */
+public class StandardTreeNodeView<T> extends TreeNodeView<T> {
+
+  /**
+   * Construct a {@link TreeNodeView}.
+   * 
+   * @param tree the parent {@link TreeView}
+   * @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
+   */
+  StandardTreeNodeView(final TreeView tree,
+      final StandardTreeNodeView<?> parent, NodeInfo<T> parentNodeInfo,
+      Element elem, T value) {
+    super(tree, parent, parentNodeInfo, value);
+    setElement(elem);
+  }
+  
+  @Override
+  protected void postClose() {
+    tree.maybeAnimateTreeNode(this);
+  }
+
+  @Override
+  protected <C> TreeNodeView<C> createTreeNodeView(NodeInfo<C> nodeInfo,
+      Element childElem, C childValue, int idx) {
+    return new StandardTreeNodeView<C>(tree, this, nodeInfo, childElem,
+        childValue);
+  }
+
+  @Override
+  protected <C> void emitHtml(StringBuilder sb, NodeInfo<C> nodeInfo,
+      List<C> childValues, List<TreeNodeView<?>> savedViews) {
+    TreeViewModel model = tree.getTreeViewModel();
+    int imageWidth = tree.getImageWidth();
+    Cell<C> theCell = nodeInfo.getCell();
+    
+    int idx = 0;
+    for (C childValue : childValues) {
+      sb.append("<div style=\"position:relative;padding-left:");
+      sb.append(imageWidth);
+      sb.append("px;\">");
+      if (savedViews.get(idx) != null) {
+        sb.append(tree.getOpenImageHtml(0));
+      } else if (model.isLeaf(childValue)) {
+        sb.append(LEAF_IMAGE);
+      } else {
+        sb.append(tree.getClosedImageHtml(0));
+      }
+      sb.append("<div>");
+      theCell.render(childValue, sb);
+      sb.append("</div>");
+      sb.append("</div>");
+    }
+  }
+
+  /**
+   * Ensure that the child container exists and return it.
+   * 
+   * @return the child container
+   */
+  @Override
+  protected Element ensureChildContainer() {
+    if (childContainer == null) {
+      // If this is a root node or the element does not exist, create it.
+      Element animFrame = getElement().appendChild(
+          Document.get().createDivElement());
+      animFrame.getStyle().setPosition(Position.RELATIVE);
+      animFrame.getStyle().setOverflow(Overflow.HIDDEN);
+      childContainer = animFrame.appendChild(Document.get().createDivElement());
+    }
+    return childContainer;
+  }
+
+  /**
+   * @return the element that contains the rendered cell
+   */
+  @Override
+  protected Element getCellParent() {
+    return getElement().getChild(1).cast();
+  }
+  
+  /**
+   * @return the image element
+   */
+  @Override
+  protected Element getImageElement() {
+    return getElement().getFirstChildElement();
+  }
+}
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeView.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeView.java
new file mode 100644
index 0000000..241cfcb
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeView.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2010 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.google.gwt.bikeshed.tree.client;
+
+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.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.HasAnimation;
+
+import java.util.ArrayList;
+
+/**
+ * A view of a tree.
+ */
+public class StandardTreeView extends TreeView implements HasAnimation {
+
+  /**
+   * A {@link TreeNodeAnimation} that reveals the contents of child nodes.
+   */
+  public static class RevealAnimation extends TreeNodeViewAnimation {
+
+    /**
+     * Create a new {@link RevealAnimation}.
+     * 
+     * @return the new animation
+     */
+    public static RevealAnimation create() {
+      return new RevealAnimation();
+    }
+
+    /**
+     * The container that holds the child container.
+     */
+    Element animFrame;
+    /**
+     * The container that holds the children.
+     */
+    Element childContainer;
+
+    /**
+     * The target height when opening, the start height when closing.
+     */
+    int height;
+    /**
+     * True if the node is opening, false if closing.
+     */
+    boolean opening;
+
+    /**
+     * Not instantiable.
+     */
+    private RevealAnimation() {
+    }
+
+    /**
+     * Animate a {@link TreeNodeView} into its new state.
+     * 
+     * @param node the {@link TreeNodeView} to animate
+     * @param isAnimationEnabled true to animate
+     */
+    @Override
+    public void animate(TreeNodeView<?> node, boolean isAnimationEnabled) {
+      // Cancel any pending animations.
+      cancel();
+
+      // Initialize the fields.
+      this.opening = node.getState();
+      childContainer = node.getChildContainer();
+      animFrame = childContainer.getParentElement();
+
+      if (isAnimationEnabled) {
+        // Animated.
+        int duration = getDuration();
+        int childCount = childContainer.getChildCount();
+        if (childCount < 4) {
+          // Reduce the duration if there are less than four items or it will
+          // look really slow.
+          duration = (int) ((childCount / 4.0) * duration);
+        }
+        run(duration);
+      } else {
+        // Non animated.
+        cleanup();
+      }
+    }
+
+    @Override
+    protected void onComplete() {
+      cleanup();
+    }
+
+    @Override
+    protected void onStart() {
+      if (opening) {
+        animFrame.getStyle().setHeight(1.0, Unit.PX);
+        animFrame.getStyle().clearDisplay();
+        height = childContainer.getScrollHeight();
+      } else {
+        height = childContainer.getOffsetHeight();
+      }
+    }
+
+    @Override
+    protected void onUpdate(double progress) {
+      if (opening) {
+        double curHeight = progress * height;
+        animFrame.getStyle().setHeight(curHeight, Unit.PX);
+      } else {
+        double curHeight = (1.0 - progress) * height;
+        animFrame.getStyle().setHeight(curHeight, Unit.PX);
+      }
+    }
+
+    /**
+     * Put the node back into a clean state and clear fields.
+     */
+    private void cleanup() {
+      if (opening) {
+        animFrame.getStyle().clearDisplay();
+      } else {
+        animFrame.getStyle().setDisplay(Display.NONE);
+        childContainer.setInnerHTML("");
+      }
+      animFrame.getStyle().clearHeight();
+      this.childContainer = null;
+      this.animFrame = null;
+    }
+  }
+
+  /**
+   * A {@link TreeNodeAnimation} that slides children into view.
+   */
+  public static class SlideAnimation extends RevealAnimation {
+    /**
+     * Create a new {@link RevealAnimation}.
+     * 
+     * @return the new animation
+     */
+    public static SlideAnimation create() {
+      return new SlideAnimation();
+    }
+
+    /**
+     * Not instantiable.
+     */
+    private SlideAnimation() {
+    }
+
+    @Override
+    protected void onComplete() {
+      childContainer.getStyle().clearPosition();
+      childContainer.getStyle().clearTop();
+      childContainer.getStyle().clearWidth();
+      super.onComplete();
+    }
+
+    @Override
+    protected void onStart() {
+      super.onStart();
+      if (opening) {
+        childContainer.getStyle().setTop(-height, Unit.PX);
+      } else {
+        childContainer.getStyle().setTop(0, Unit.PX);
+      }
+      childContainer.getStyle().setPosition(Position.RELATIVE);
+    }
+
+    @Override
+    protected void onUpdate(double progress) {
+      super.onUpdate(progress);
+      if (opening) {
+        double curTop = (1.0 - progress) * -height;
+        childContainer.getStyle().setTop(curTop, Unit.PX);
+      } else {
+        double curTop = progress * -height;
+        childContainer.getStyle().setTop(curTop, Unit.PX);
+      }
+    }
+  }
+
+  /**
+   * Construct a new {@link TreeView}.
+   * 
+   * @param <T> the type of data in the root node
+   * @param viewModel the {@link TreeViewModel} that backs the tree
+   * @param rootValue the hidden root value of the tree
+   */
+  public <T> StandardTreeView(TreeViewModel viewModel, T rootValue) {
+    super(viewModel);
+    setElement(Document.get().createDivElement());
+    setStyleName("gwt-TreeView");
+
+    // We use one animation for the entire tree.
+    animation = SlideAnimation.create();
+    
+    // Add event handlers.
+    sinkEvents(Event.ONCLICK | Event.ONMOUSEDOWN | Event.ONMOUSEUP);
+
+    // Associate a view with the item.
+    TreeNodeView<T> root = new StandardTreeNodeView<T>(this, null, null, getElement(), rootValue);
+    setRootNode(root);
+    root.setState(true);
+  }
+
+  @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, rootNode);
+        }
+        break;
+    }
+  }
+
+  /**
+   * Collects parents going up the element tree, terminated at the tree root.
+   */
+  private void collectElementChain(ArrayList<Element> chain, ArrayList<String> ids, Element hRoot,
+      Element hElem) {
+    if ((hElem == null) || (hElem == hRoot)) {
+      return;
+    }
+
+    collectElementChain(chain, ids, hRoot, hElem.getParentElement());
+    chain.add(hElem);
+    ids.add(hElem.getId());
+  }
+
+  private boolean elementClicked(Element hElem, NativeEvent event, TreeNodeView<?> rootNode) {
+    ArrayList<Element> chain = new ArrayList<Element>();
+    ArrayList<String> ids = new ArrayList<String>();
+    collectElementChain(chain, ids, getElement(), hElem);
+
+    TreeNodeView<?> nodeView = findItemByChain(chain, 0, rootNode);
+    if (nodeView != null && nodeView != rootNode) {
+      if (nodeView.getImageElement().isOrHasChild(hElem)) {
+        nodeView.setState(!nodeView.getState());
+        return true;
+      } else if (nodeView.getCellParent().isOrHasChild(hElem)) {
+        nodeView.fireEventToCell(event);
+        return true;
+      }
+    }
+
+    return false;
+  }
+  
+  private TreeNodeView<?> findItemByChain(ArrayList<Element> chain, int idx,
+      TreeNodeView<?> parent) {
+    if (idx == chain.size()) {
+      return parent;
+    }
+  
+    Element hCurElem = chain.get(idx);
+    for (int i = 0, n = parent.getChildCount(); i < n; ++i) {
+      TreeNodeView<?> child = parent.getChildTreeNodeView(i);
+      if (child.getElement() == hCurElem) {
+        TreeNodeView<?> retItem = findItemByChain(chain, idx + 1, child);
+        if (retItem == null) {
+          return child;
+        }
+        return retItem;
+      }
+    }
+  
+    return findItemByChain(chain, idx + 1, parent);
+  }
+}
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNode.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNode.java
new file mode 100644
index 0000000..e9a0694
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNode.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2010 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.google.gwt.bikeshed.tree.client;
+
+/**
+ * A interface allowing navigation within a tree view.
+ * 
+ * @param <T> the type of data stored at this node.
+ */
+public interface TreeNode<T> {
+  
+  TreeNode<?> getChildNode(int childIndex);
+  
+  int getChildCount();
+  
+  TreeNode<?> getParentNode();
+  
+  T getValue();
+}
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNodeView.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNodeView.java
index ab35216..1ba192c 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNodeView.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNodeView.java
@@ -15,7 +15,6 @@
  */
 package com.google.gwt.bikeshed.tree.client;
 
-import com.google.gwt.bikeshed.cells.client.Cell;
 import com.google.gwt.bikeshed.list.shared.ListEvent;
 import com.google.gwt.bikeshed.list.shared.ListHandler;
 import com.google.gwt.bikeshed.list.shared.ListModel;
@@ -26,8 +25,7 @@
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.NativeEvent;
 import com.google.gwt.dom.client.Style.Display;
-import com.google.gwt.dom.client.Style.Overflow;
-import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Composite;
 
 import java.util.ArrayList;
@@ -40,110 +38,90 @@
  * 
  * @param <T> the type that this {@link TreeNodeView} contains
  */
-public class TreeNodeView<T> extends Composite {
+public abstract class TreeNodeView<T> extends Composite implements TreeNode<T> {
 
   /**
    * The element used in place of an image when a node has no children.
    */
-  private static final String LEAF_IMAGE = "<div style='position:absolute;display:none;'></div>";
-
-  private boolean animate;
-
-  /**
-   * The children of this {@link TreeNodeView}.
-   */
-  private List<TreeNodeView<?>> children;
+  protected static final String LEAF_IMAGE = "<div style='position:absolute;display:none;'></div>";
+  
+  protected boolean animate;
 
   /**
    * A reference to the element that contains the children.
    */
-  private Element childContainer;
+  protected Element childContainer;
 
   /**
+   * The children of this {@link TreeNodeView}.
+   */
+  protected List<TreeNodeView<?>> children;
+  
+  /**
    * The list registration for the list of children.
    */
-  private ListRegistration listReg;
-
+  protected ListRegistration listReg;
+  
   /**
    * The info about children of this node.
    */
-  private NodeInfo<?> nodeInfo;
+  protected NodeInfo<?> nodeInfo;
 
   /**
    * Indicates whether or not we've loaded the node info.
    */
-  private boolean nodeInfoLoaded;
+  protected 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;
+  protected boolean open;
 
   /**
    * The containing {@link TreeView}.
    */
-  private TreeView tree;
+  protected TreeView tree;
 
   /**
+   * The parent {@link SideBySideTreeNodeView}.
+   */
+  private TreeNodeView<?> parentNode;
+  
+  /**
+   * The info about this node.
+   */
+  private NodeInfo<T> parentNodeInfo;
+  
+  /**
    * This node's value.
    */
   private T value;
 
-  /**
-   * Construct a {@link TreeNodeView}.
-   * 
-   * @param tree the parent {@link TreeView}
-   * @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(final TreeView tree, final TreeNodeView<?> parent,
-      NodeInfo<T> parentNodeInfo, Element elem, T value) {
-    this.value = value;
+  public TreeNodeView(TreeView tree, TreeNodeView<?> parent, NodeInfo<T> parentNodeInfo, T value) {
     this.tree = tree;
-    this.parent = parent;
-    // We pass in parentNodeInfo so we know that it is type T.
+    this.parentNode = parent;
     this.parentNodeInfo = parentNodeInfo;
-    setElement(elem);
+    this.value = value;
   }
-
-  /**
-   * Get the child at the specified index.
-   * 
-   * @return the child node
-   */
-  public TreeNodeView<?> getChild(int index) {
-    if ((index < 0) || (index >= getChildCount())) {
-      return null;
-    }
-    return children.get(index);
-  }
-
-  /**
-   * Get the number of children under this node.
-   * 
-   * @return the child count
-   */
+  
   public int getChildCount() {
     return children == null ? 0 : children.size();
   }
 
-  /**
-   * Get the parent {@link TreeNodeView}.
-   */
+  public TreeNode<?> getChildNode(int childIndex) {
+    return children.get(childIndex);
+  }
+
+  public TreeNodeView<?> getChildTreeNodeView(int childIndex) {
+    return children.get(childIndex);
+  }
+
+  public TreeNode<?> getParentNode() {
+    return parentNode;
+  }
+
   public TreeNodeView<?> getParentTreeNodeView() {
-    return parent;
+    return parentNode;
   }
 
   /**
@@ -165,6 +143,15 @@
   }
 
   /**
+   * Check if this is a root node at the top of the tree.
+   * 
+   * @return true if a root node, false if not
+   */
+  public boolean isRootNode() {
+    return getParentNode() == null;
+  }
+  
+  /**
    * Sets whether this item's children are displayed.
    * 
    * @param open whether the item is open
@@ -186,79 +173,40 @@
     if (this.open == open) {
       return;
     }
-
+    
     this.animate = true;
     this.open = open;
     if (open) {
       if (!nodeInfoLoaded) {
-        nodeInfo = tree.getTreeViewModel().getNodeInfo(value, this);
+        nodeInfo = tree.getTreeViewModel().getNodeInfo(getValue(), this);
         nodeInfoLoaded = true;
       }
-
+      
+      preOpen();
       // If we don't have any nodeInfo, we must be a leaf node.
       if (nodeInfo != null) {
         onOpen(nodeInfo);
-      }
+      } 
     } else {
       cleanup();
-      tree.maybeAnimateTreeNode(this);
+      postClose();
     }
 
     // Update the image.
     updateImage();
   }
-
-  boolean consumeAnimate() {
-    boolean hasAnimate = animate;
-    animate = false;
-    return hasAnimate;
-  }
-
+  
   /**
-   * Fire an event to the {@link Cell}.
-   * 
-   * @param event the native event
+   * Unregister the list handler and destroy all child nodes.
    */
-  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 element that contains the children
-   */
-  Element getChildContainer() {
-    return childContainer;
-  }
-
-  /**
-   * @return the image element
-   */
-  Element getImageElement() {
-    return getElement().getFirstChildElement();
-  }
-
-  NodeInfo<T> getParentNodeInfo() {
-    return parentNodeInfo;
-  }
-
-  /**
-   * Cleanup this node and all its children. This node can still be used.
-   */
-  private void cleanup() {
+  protected void cleanup() {
     // Unregister the list handler.
     if (listReg != null) {
       listReg.removeHandler();
       listReg = null;
     }
-
-    // Recursively kill chidren.
+  
+    // Recursively kill children.
     if (children != null) {
       for (TreeNodeView<?> child : children) {
         child.cleanup();
@@ -266,13 +214,25 @@
       children = null;
     }
   }
+  
+  protected boolean consumeAnimate() {
+    boolean hasAnimate = animate;
+    animate = false;
+    return hasAnimate;
+  }
+  
+  protected abstract <C> TreeNodeView<C> createTreeNodeView(NodeInfo<C> nodeInfo,
+      Element childElem, C childValue, int idx);
+
+  protected abstract <C> void emitHtml(StringBuilder sb, NodeInfo<C> nodeInfo,
+      List<C> childValues, List<TreeNodeView<?>> savedViews);
 
   /**
    * Ensure that the animation frame exists and return it.
    * 
    * @return the animation frame
    */
-  private Element ensureAnimationFrame() {
+  protected Element ensureAnimationFrame() {
     return ensureChildContainer().getParentElement();
   }
 
@@ -281,47 +241,79 @@
    * 
    * @return the child container
    */
-  private Element ensureChildContainer() {
-    if (childContainer == null) {
-      // If this is a root node or the element does not exist, create it.
-      Element animFrame = getElement().appendChild(
-          Document.get().createDivElement());
-      animFrame.getStyle().setPosition(Position.RELATIVE);
-      animFrame.getStyle().setOverflow(Overflow.HIDDEN);
-      childContainer = animFrame.appendChild(Document.get().createDivElement());
+  protected abstract Element ensureChildContainer();
+
+  /**
+   * Fire an event to the {@link com.google.gwt.bikeshed.cells.client.Cell}.
+   * 
+   * @param event the native event
+   */
+  protected void fireEventToCell(NativeEvent event) {
+    if (parentNodeInfo != null) {
+      Element cellParent = getCellParent();
+      parentNodeInfo.onBrowserEvent(cellParent, value, event);
+    } else {
+      Window.alert("parentNodeInfo == null");
     }
+  }
+  
+  /**
+   * Returns the element that parents the cell contents of this node.
+   */
+  protected abstract Element getCellParent();
+
+  /**
+   * Returns the element that contains the children of this node.
+   */
+  protected Element getChildContainer() {
     return childContainer;
   }
-
-  private Object getValueKey() {
-    return parentNodeInfo.getKey(getValue());
-  }
-
+  
   /**
-   * Check if this is a root node at the top of the tree.
-   * 
-   * @return true if a root node, false if not
+   * Returns the element corresponding to the open/close image.
    */
-  private boolean isRootNode() {
-    return getParentTreeNodeView() == null;
+  protected abstract Element getImageElement();
+
+  /**
+   * Returns the left position of the open/close image within a tree item node.
+   * The default implementation returns 0.
+   */
+  protected int getImageLeft() {
+    return 0;
+  }
+  
+  /**
+   * Returns the nodeInfo for this node's parent.
+   */
+  protected NodeInfo<T> getParentNodeInfo() {
+    return parentNodeInfo;
   }
 
   /**
-   * Set up the node when it is opened.
+   * Returns the key for the value of this node using the parent's
+   * implementation of NodeInfo.getKey().
+   */
+  protected Object getValueKey() {
+    return getParentNodeInfo().getKey(getValue());
+  }
+  
+  /**
+   * Set up the node when it is opened.  Delegates to createMap(), emitHtml(),
+   * and createTreeNodeView() for subclass-specific functionality.
    * 
    * @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 NodeInfo<C> nodeInfo) {
+  protected <C> void onOpen(final NodeInfo<C> nodeInfo) {
     // Add a loading message.
     ensureChildContainer().setInnerHTML(tree.getLoadingHtml());
     ensureAnimationFrame().getStyle().setProperty("display", "");
-
+    
     // Get the node info.
     ListModel<C> listModel = nodeInfo.getListModel();
     listReg = listModel.addListHandler(new ListHandler<C>() {
-      public void onDataChanged(ListEvent<C> event) {
+      public void onDataChanged(ListEvent<C> event) {    
         // TODO - handle event start and length
 
         // Construct a map of former child views based on their value keys.
@@ -335,16 +327,13 @@
             }
           }
         }
-
+        
         // Hide the child container so we can animate it.
-        ensureAnimationFrame().getStyle().setDisplay(Display.NONE);
+        if (tree.isAnimationEnabled()) {
+          ensureAnimationFrame().getStyle().setDisplay(Display.NONE);
+        }
 
-        // Construct the child contents.
-        TreeViewModel model = tree.getTreeViewModel();
-        int imageWidth = tree.getImageWidth();
-        Cell<C> theCell = nodeInfo.getCell();
-        StringBuilder sb = new StringBuilder();
-
+        List<TreeNodeView<?>> savedViews = new ArrayList<TreeNodeView<?>>();
         for (C childValue : event.getValues()) {
           // Remove any child elements that correspond to prior children
           // so the call to setInnerHtml will not destroy them
@@ -352,29 +341,21 @@
           if (savedView != null) {
             savedView.getElement().removeFromParent();
           }
-
-          sb.append("<div style=\"position:relative;padding-left:");
-          sb.append(imageWidth);
-          sb.append("px;\">");
-          if (savedView != null) {
-            sb.append(tree.getOpenImageHtml());
-          } else if (model.isLeaf(childValue)) {
-            sb.append(LEAF_IMAGE);
-          } else {
-            sb.append(tree.getClosedImageHtml());
-          }
-          sb.append("<div>");
-          theCell.render(childValue, sb);
-          sb.append("</div>");
-          sb.append("</div>");
+          savedViews.add(savedView);
         }
+
+        // Construct the child contents.
+        StringBuilder sb = new StringBuilder();
+        emitHtml(sb, nodeInfo, event.getValues(), savedViews);
         childContainer.setInnerHTML(sb.toString());
 
         // Create the child TreeNodeViews from the new elements.
         children = new ArrayList<TreeNodeView<?>>();
         Element childElem = childContainer.getFirstChildElement();
+        int idx = 0;
         for (C childValue : event.getValues()) {
-          TreeNodeView<C> child = new TreeNodeView<C>(tree, TreeNodeView.this, nodeInfo, childElem, childValue);
+          TreeNodeView<C> child = createTreeNodeView(nodeInfo, childElem,
+              childValue, idx);
           TreeNodeView<?> savedChild = map.get(nodeInfo.getKey(childValue));
           // Copy the saved child's state into the new child
           if (savedChild != null) {
@@ -383,18 +364,22 @@
             child.listReg = savedChild.listReg;
             child.nodeInfo = savedChild.nodeInfo;
             child.nodeInfoLoaded = savedChild.nodeInfoLoaded;
-            child.open = savedChild.open;
+            child.open = savedChild.getState();
 
-            // Copy the animation frame element to the new child
+            // Copy the child container element to the new child
             child.getElement().appendChild(savedChild.childContainer.getParentElement());
           }
 
           children.add(child);
           childElem = childElem.getNextSiblingElement();
+          
+          idx++;
         }
 
         // Animate the child container open.
-        tree.maybeAnimateTreeNode(TreeNodeView.this);
+        if (tree.isAnimationEnabled()) {
+          tree.maybeAnimateTreeNode(TreeNodeView.this);
+        }
       }
 
       public void onSizeChanged(SizeChangeEvent event) {
@@ -404,16 +389,29 @@
   }
 
   /**
+   * Called from setState(boolean, boolean) following the call to cleanup().
+   */
+  protected void postClose() {
+  }
+
+  /**
+   * Called from setState(boolean, boolean) prior to the call to onOpen().
+   */
+  protected void preOpen() {
+  }
+
+  /**
    * Update the image based on the current state.
    */
-  private void updateImage() {
+  protected void updateImage() {
     // Early out if this is a root node.
     if (isRootNode()) {
       return;
     }
 
     // Replace the image element with a new one.
-    String html = open ? tree.getOpenImageHtml() : tree.getClosedImageHtml();
+    int imageLeft = getImageLeft();
+    String html = open ? tree.getOpenImageHtml(imageLeft) : tree.getClosedImageHtml(imageLeft);
     if (nodeInfoLoaded && nodeInfo == null) {
       html = LEAF_IMAGE;
     }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeView.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeView.java
index 529ccba..17dd5c9 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeView.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeView.java
@@ -17,49 +17,20 @@
 
 import com.google.gwt.animation.client.Animation;
 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.dom.client.Style.Display;
-import com.google.gwt.dom.client.Style.Position;
-import com.google.gwt.dom.client.Style.Unit;
 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.Event;
 import com.google.gwt.user.client.ui.AbstractImagePrototype;
-import com.google.gwt.user.client.ui.HasAnimation;
 import com.google.gwt.user.client.ui.Widget;
 
-import java.util.ArrayList;
-
 /**
  * A view of a tree.
  */
-public class TreeView extends Widget implements HasAnimation {
-
-  private static final Resources DEFAULT_RESOURCES = GWT.create(Resources.class);
+public abstract class TreeView extends Widget {
 
   /**
-   * A ClientBundle that provides images for this widget.
+   * An Animation of a {@link TreeNodeView}.
    */
-  public interface Resources extends ClientBundle {
-
-    /**
-     * An image indicating a closed branch.
-     */
-    ImageResource treeClosed();
-
-    /**
-     * An image indicating an open branch.
-     */
-    ImageResource treeOpen();
-  }
-
-  /**
-   * The animation used for {@link TreeNodeView}.
-   */
-  public abstract static class TreeNodeAnimation extends Animation {
+  public abstract static class TreeNodeViewAnimation extends Animation {
 
     /**
      * The default animation delay in milliseconds.
@@ -72,212 +43,56 @@
     private int duration = DEFAULT_ANIMATION_DURATION;
 
     /**
-     * Not instantiable.
-     */
-    private TreeNodeAnimation() {
-    }
-
-    /**
-     * Get the duration of animations in milliseconds.
+     * Animate a {@link TreeNodeView} into its new state.
      * 
-     * @return the animation duration
+     * @param node the {@link TreeNodeView} to animate
+     * @param isAnimationEnabled true to animate
      */
+    public abstract void animate(TreeNodeView<?> node,
+        boolean isAnimationEnabled);
+
     public int getDuration() {
       return duration;
     }
-
-    /**
-     * Set the animation duration in milliseconds.
-     * 
-     * @param duration the duration
-     */
+    
     public void setDuration(int duration) {
       this.duration = duration;
     }
+  }
+  
+  /**
+   * A ClientBundle that provides images for this widget.
+   */
+  interface Resources extends ClientBundle {
 
     /**
-     * Animate a {@link TreeNodeView} into its new state.
-     * 
-     * @param node the {@link TreeNodeView} to animate
-     * @param isAnimationEnabled true to animate
+     * An image indicating a closed branch.
      */
-    abstract void animate(TreeNodeView<?> node, boolean isAnimationEnabled);
+    ImageResource treeClosed();
+
+    /**
+     * An image indicating an open branch.
+     */
+    ImageResource treeOpen();
   }
 
+  private static final Resources DEFAULT_RESOURCES = GWT.create(Resources.class);
+ 
   /**
-   * A {@link TreeNodeAnimation} that reveals the contents of child nodes.
+   * The animation.
    */
-  public static class RevealAnimation extends TreeNodeAnimation {
-
-    /**
-     * Create a new {@link RevealAnimation}.
-     * 
-     * @return the new animation
-     */
-    public static RevealAnimation create() {
-      return new RevealAnimation();
-    }
-
-    /**
-     * The container that holds the child container.
-     */
-    Element animFrame;
-    /**
-     * The container that holds the children.
-     */
-    Element childContainer;
-
-    /**
-     * The target height when opening, the start height when closing.
-     */
-    int height;
-    /**
-     * True if the node is opening, false if closing.
-     */
-    boolean opening;
-
-    /**
-     * Not instantiable.
-     */
-    private RevealAnimation() {
-    }
-
-    @Override
-    protected void onComplete() {
-      cleanup();
-    }
-
-    @Override
-    protected void onStart() {
-      if (opening) {
-        animFrame.getStyle().setHeight(1.0, Unit.PX);
-        animFrame.getStyle().clearDisplay();
-        height = childContainer.getScrollHeight();
-      } else {
-        height = childContainer.getOffsetHeight();
-      }
-    }
-
-    @Override
-    protected void onUpdate(double progress) {
-      if (opening) {
-        double curHeight = progress * height;
-        animFrame.getStyle().setHeight(curHeight, Unit.PX);
-      } else {
-        double curHeight = (1.0 - progress) * height;
-        animFrame.getStyle().setHeight(curHeight, Unit.PX);
-      }
-    }
-
-    /**
-     * Animate a {@link TreeNodeView} into its new state.
-     * 
-     * @param node the {@link TreeNodeView} to animate
-     * @param isAnimationEnabled true to animate
-     */
-    @Override
-    void animate(TreeNodeView<?> node, boolean isAnimationEnabled) {
-      // Cancel any pending animations.
-      cancel();
-
-      // Initialize the fields.
-      this.opening = node.getState();
-      childContainer = node.getChildContainer();
-      animFrame = childContainer.getParentElement();
-
-      if (isAnimationEnabled) {
-        // Animated.
-        int duration = getDuration();
-        int childCount = childContainer.getChildCount();
-        if (childCount < 4) {
-          // Reduce the duration if there are less than four items or it will
-          // look really slow.
-          duration = (int) ((childCount / 4.0) * duration);
-        }
-        run(duration);
-      } else {
-        // Non animated.
-        cleanup();
-      }
-    }
-
-    /**
-     * Put the node back into a clean state and clear fields.
-     */
-    private void cleanup() {
-      if (opening) {
-        animFrame.getStyle().clearDisplay();
-      } else {
-        animFrame.getStyle().setDisplay(Display.NONE);
-        childContainer.setInnerHTML("");
-      }
-      animFrame.getStyle().clearHeight();
-      this.childContainer = null;
-      this.animFrame = null;
-    }
-  }
-
+  protected TreeNodeViewAnimation animation;
+  
   /**
-   * A {@link TreeNodeAnimation} that slides children into view.
+   * The hidden root node in the tree.
    */
-  public static class SlideAnimation extends RevealAnimation {
-    /**
-     * Create a new {@link RevealAnimation}.
-     * 
-     * @return the new animation
-     */
-    public static SlideAnimation create() {
-      return new SlideAnimation();
-    }
-
-    /**
-     * Not instantiable.
-     */
-    private SlideAnimation() {
-    }
-
-    @Override
-    protected void onComplete() {
-      childContainer.getStyle().clearPosition();
-      childContainer.getStyle().clearTop();
-      childContainer.getStyle().clearWidth();
-      super.onComplete();
-    }
-
-    @Override
-    protected void onStart() {
-      super.onStart();
-      if (opening) {
-        childContainer.getStyle().setTop(-height, Unit.PX);
-      } else {
-        childContainer.getStyle().setTop(0, Unit.PX);
-      }
-      childContainer.getStyle().setPosition(Position.RELATIVE);
-    }
-
-    @Override
-    protected void onUpdate(double progress) {
-      super.onUpdate(progress);
-      if (opening) {
-        double curTop = (1.0 - progress) * -height;
-        childContainer.getStyle().setTop(curTop, Unit.PX);
-      } else {
-        double curTop = progress * -height;
-        childContainer.getStyle().setTop(curTop, Unit.PX);
-      }
-    }
-  }
-
-  /**
-   * We use one animation for the entire {@link TreeView}.
-   */
-  private TreeNodeAnimation animation = SlideAnimation.create();
+  protected TreeNodeView<?> rootNode;
 
   /**
    * The HTML used to generate the closed image.
    */
   private String closedImageHtml;
-
+  
   /**
    * Indicates whether or not animations are enabled.
    */
@@ -298,36 +113,14 @@
    * 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;
-
+ 
   /**
    * Construct a new {@link TreeView}.
    * 
-   * @param <T> the type of data in the root node
    * @param viewModel the {@link TreeViewModel} that backs the tree
-   * @param rootValue the hidden root value of the tree
    */
-  public <T> TreeView(TreeViewModel viewModel, T rootValue) {
+  public TreeView(TreeViewModel viewModel) {
     this.model = viewModel;
-    this.resources = DEFAULT_RESOURCES;
-    setElement(Document.get().createDivElement());
-    setStyleName("gwt-TreeView");
-
-    // Add event handlers.
-    sinkEvents(Event.ONCLICK | Event.ONMOUSEDOWN | Event.ONMOUSEUP);
-
-    // Associate a view with the item.
-    rootNode = new TreeNodeView<T>(this, null, null, getElement(), rootValue);
-    rootNode.setState(true);
   }
 
   /**
@@ -337,20 +130,10 @@
    * @return the animation
    * @see #isAnimationEnabled()
    */
-  public TreeNodeAnimation getAnimation() {
+  public Animation getAnimation() {
     return animation;
   }
-
-  /**
-   * Get the HTML string that is displayed while nodes wait for their children
-   * to load.
-   * 
-   * @return the loading HTML string
-   */
-  public String getLoadingHtml() {
-    return loadingHtml;
-  }
-
+  
   public TreeViewModel getTreeViewModel() {
     return model;
   }
@@ -358,60 +141,36 @@
   public boolean isAnimationEnabled() {
     return isAnimationEnabled;
   }
-
-  @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;
-    }
-  }
-
+  
   /**
    * Set the animation used to open and close nodes in this tree. You must call
    * {@link #setAnimationEnabled(boolean)} to enable or disable animation.
    * 
-   * @param animation the animation
+   * @param animation a {@link TreeNodeViewAnimation}.
    * @see #setAnimationEnabled(boolean)
    */
-  public void setAnimation(TreeNodeAnimation animation) {
+  public void setAnimation(TreeNodeViewAnimation animation) {
     assert animation != null : "animation cannot be null";
     this.animation = animation;
   }
-
+  
   public void setAnimationEnabled(boolean enable) {
     this.isAnimationEnabled = enable;
     if (!enable && animation != null) {
       animation.cancel();
     }
   }
-
-  /**
-   * Set the HTML string that will be displayed when a node is waiting for its
-   * child nodes to load.
-   * 
-   * @param loadingHtml the HTML string
-   */
-  public void setLoadingHtml(String loadingHtml) {
-    this.loadingHtml = loadingHtml;
-  }
-
   /**
    * @return the HTML to render the closed image.
    */
-  String getClosedImageHtml() {
+  protected String getClosedImageHtml(int left) {
     if (closedImageHtml == null) {
-      AbstractImagePrototype proto = AbstractImagePrototype.create(resources.treeClosed());
+      AbstractImagePrototype proto =
+        AbstractImagePrototype.create(DEFAULT_RESOURCES.treeClosed());
+      // CHECKSTYLE_OFF
       closedImageHtml = proto.getHTML().replace("style='",
-          "style='position:absolute;left:0px;top:0px;");
+          "style='position:absolute;left:" + left + "px;top:0px;");
+      // CHECKSTYLE_ON
     }
     return closedImageHtml;
   }
@@ -421,19 +180,32 @@
    * 
    * @return the maximum width required for images.
    */
-  int getImageWidth() {
-    return Math.max(resources.treeClosed().getWidth(),
-        resources.treeOpen().getWidth());
+  protected int getImageWidth() {
+    return Math.max(DEFAULT_RESOURCES.treeClosed().getWidth(),
+        DEFAULT_RESOURCES.treeOpen().getWidth());
   }
 
   /**
+   * Get the HTML string that is displayed while nodes wait for their children
+   * to load.
+   * 
+   * @return the loading HTML string
+   */
+  protected String getLoadingHtml() {
+    return loadingHtml;
+  }
+  
+  /**
    * @return the HTML to render the open image.
    */
-  String getOpenImageHtml() {
+  protected String getOpenImageHtml(int left) {
     if (openImageHtml == null) {
-      AbstractImagePrototype proto = AbstractImagePrototype.create(resources.treeOpen());
+      AbstractImagePrototype proto =
+        AbstractImagePrototype.create(DEFAULT_RESOURCES.treeOpen());
+      // CHECKSTYLE_OFF
       openImageHtml = proto.getHTML().replace("style='",
-          "style='position:absolute;left:0px;top:0px;");
+          "style='position:absolute;left:" + left + "px;top:0px;");
+      // CHECKSTYLE_ON
     }
     return openImageHtml;
   }
@@ -443,59 +215,27 @@
    * 
    * @param node the node to animate
    */
-  void maybeAnimateTreeNode(TreeNodeView<?> node) {
-    animation.animate(node, node.consumeAnimate() && isAnimationEnabled);
+  protected void maybeAnimateTreeNode(TreeNodeView<?> node) {
+    if (animation != null) {
+      animation.animate(node, node.consumeAnimate() && isAnimationEnabled());
+    }
   }
 
   /**
-   * Collects parents going up the element tree, terminated at the tree root.
+   * Set the HTML string that will be displayed when a node is waiting for its
+   * child nodes to load.
+   * 
+   * @param loadingHtml the HTML string
    */
-  private void collectElementChain(ArrayList<Element> chain, Element hRoot,
-      Element hElem) {
-    if ((hElem == null) || (hElem == hRoot)) {
-      return;
-    }
-
-    collectElementChain(chain, hRoot, hElem.getParentElement());
-    chain.add(hElem);
+  protected void setLoadingHtml(String loadingHtml) {
+    this.loadingHtml = loadingHtml;
   }
 
-  private boolean elementClicked(Element hElem, NativeEvent event) {
-    ArrayList<Element> chain = new ArrayList<Element>();
-    collectElementChain(chain, getElement(), hElem);
-
-    TreeNodeView<?> nodeView = findItemByChain(chain, 0, rootNode);
-    if (nodeView != null && nodeView != rootNode) {
-      if (nodeView.getImageElement().isOrHasChild(hElem)) {
-        nodeView.setState(!nodeView.getState(), true);
-        return true;
-      } else if (nodeView.getCellParent().isOrHasChild(hElem)) {
-        nodeView.fireEventToCell(event);
-        return true;
-      }
-    }
-
-    return false;
+  TreeNodeView<?> getRootNode() {
+    return rootNode;
   }
 
-  private TreeNodeView<?> findItemByChain(ArrayList<Element> chain, int idx,
-      TreeNodeView<?> parent) {
-    if (idx == chain.size()) {
-      return parent;
-    }
-
-    Element hCurElem = chain.get(idx);
-    for (int i = 0, n = parent.getChildCount(); i < n; ++i) {
-      TreeNodeView<?> child = parent.getChild(i);
-      if (child.getElement() == hCurElem) {
-        TreeNodeView<?> retItem = findItemByChain(chain, idx + 1, child);
-        if (retItem == null) {
-          return child;
-        }
-        return retItem;
-      }
-    }
-
-    return findItemByChain(chain, idx + 1, parent);
+  void setRootNode(TreeNodeView<?> rootNode) {
+    this.rootNode = rootNode;
   }
 }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeViewModel.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeViewModel.java
index b3ae963..14f1656 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeViewModel.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeViewModel.java
@@ -119,10 +119,10 @@
    * {@link Cell} to retrieve the children of the specified value.
    * 
    * @param value the value in the parent node
-   * @param treeNodeView the {@link TreeNodeView} that contains the value
+   * @param treeNode the {@link TreeNode} that contains the value
    * @return the {@link NodeInfo}
    */
-  <T> NodeInfo<?> getNodeInfo(T value, TreeNodeView<T> treeNodeView);
+  <T> NodeInfo<?> getNodeInfo(T value, TreeNode<T> treeNode);
 
   /**
    * Check if the value is known to be a leaf node.
diff --git a/bikeshed/war/Stocks.css b/bikeshed/war/Stocks.css
index 7aca7ac..37018e6 100644
--- a/bikeshed/war/Stocks.css
+++ b/bikeshed/war/Stocks.css
@@ -1,5 +1,24 @@
 /** Add css rules here for your application. */
 
+div.gwt-SideBySideTreeColumn {
+  width: 100px;
+  height: 200px;
+  overflow-y: scroll;
+  overflow-x: auto;
+  position: relative;
+}
+
+div.gwt-SideBySideTreeView {
+  border: 2px solid black;
+}
+
+div.gwt-SideBySideTree-selectedItem {
+  background-color: rgb(56, 117, 215);
+}
+
+div.gwt-SideBySideTree-unselectedItem {
+  background-color: rgb(255, 255, 255);
+}
 
 /** Example rules used by the template application (remove for your app) */
 h1 {
diff --git a/bikeshed/war/Tree.css b/bikeshed/war/Tree.css
index 7aca7ac..37018e6 100644
--- a/bikeshed/war/Tree.css
+++ b/bikeshed/war/Tree.css
@@ -1,5 +1,24 @@
 /** Add css rules here for your application. */
 
+div.gwt-SideBySideTreeColumn {
+  width: 100px;
+  height: 200px;
+  overflow-y: scroll;
+  overflow-x: auto;
+  position: relative;
+}
+
+div.gwt-SideBySideTreeView {
+  border: 2px solid black;
+}
+
+div.gwt-SideBySideTree-selectedItem {
+  background-color: rgb(56, 117, 215);
+}
+
+div.gwt-SideBySideTree-unselectedItem {
+  background-color: rgb(255, 255, 255);
+}
 
 /** Example rules used by the template application (remove for your app) */
 h1 {