Refactoring Tree code.  SideBySideTreeView now uses SimpleCellList, which a protected method that allows users to use any ListView.  SideBySideTreeNodeView shares a lot of code with SimpleCellList.  Added animations to SideBySideTreeView.  NodeInfo now includes a SelectionModel, but still allows a single instance to be used across an entire tree of different types.  NodeInfo now returns one Cell instead of a list of HasCell, but CompositeCell has been added to combine HasCells into a single Cell.

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


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7967 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/bikeshed/src/com/google/gwt/bikeshed/cells/client/Cell.java b/bikeshed/src/com/google/gwt/bikeshed/cells/client/Cell.java
index 32271d7..1248a46 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/cells/client/Cell.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/cells/client/Cell.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -20,7 +20,7 @@
 
 /**
  * A light weight representation of a renderable object.
- *
+ * 
  * @param <C> the type that this Cell represents
  * @param <V> the type of view data that this cell consumes
  */
@@ -35,9 +35,18 @@
   }
 
   /**
+   * Check if this cell depends on the selection state.
+   * 
+   * @return true if dependant on selection, false if not
+   */
+  public boolean dependsOnSelection() {
+    return false;
+  }
+
+  /**
    * Handle a browser event that took place within the cell. The default
    * implementation returns null.
-   *
+   * 
    * @param parent the parent Element
    * @param value the value associated with the cell
    * @param viewData the view data associated with the cell, or null
@@ -53,7 +62,7 @@
   /**
    * Render a cell as HTML into a StringBuilder, suitable for passing to
    * {@link Element#setInnerHTML} on a container element.
-   *
+   * 
    * @param value the cell value to be rendered
    * @param viewData view data associated with the cell
    * @param sb the StringBuilder to be written to
diff --git a/bikeshed/src/com/google/gwt/bikeshed/cells/client/CompositeCell.java b/bikeshed/src/com/google/gwt/bikeshed/cells/client/CompositeCell.java
new file mode 100644
index 0000000..902fb8d
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/bikeshed/cells/client/CompositeCell.java
@@ -0,0 +1,159 @@
+/*
+ * 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.cells.client;
+
+import com.google.gwt.bikeshed.list.client.HasCell;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * <p>
+ * A {@link Cell} that is composed of other {@link Cell}s.
+ * </p>
+ * <p>
+ * When this cell is rendered, it will render each component {@link Cell} inside
+ * a span. If the component {@link Cell} uses block level elements (such as a
+ * Div), the component cells will stack vertically.
+ * </p>
+ * 
+ * @param <C> the type that this Cell represents
+ * @param <V> the type of view data that this cell consumes
+ */
+public class CompositeCell<C, V> extends Cell<C, V> {
+
+  /**
+   * The cells that compose this {@link Cell}.
+   */
+  private List<HasCell<C, ?, V>> hasCells = new ArrayList<HasCell<C, ?, V>>();
+
+  /**
+   * Add a {@link HasCell} to the composite.
+   * 
+   * @param hasCell the {@link HasCell} to add
+   */
+  public void addHasCell(HasCell<C, ?, V> hasCell) {
+    hasCells.add(hasCell);
+  }
+
+  @Override
+  public boolean consumesEvents() {
+    // TODO(jlabanca): Should we cache this value? Can it change?
+    for (HasCell<C, ?, V> hasCell : hasCells) {
+      if (hasCell.getCell().consumesEvents()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public boolean dependsOnSelection() {
+    // TODO(jlabanca): Should we cache this value? Can it change?
+    for (HasCell<C, ?, V> hasCell : hasCells) {
+      if (hasCell.getCell().dependsOnSelection()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Insert a {@link HasCell} into the composite.
+   * 
+   * @param index the index to insert into
+   * @param hasCell the {@link HasCell} to insert
+   */
+  public void insertHasCell(int index, HasCell<C, ?, V> hasCell) {
+    hasCells.add(index, hasCell);
+  }
+
+  @Override
+  public V onBrowserEvent(Element parent, C value, V viewData,
+      NativeEvent event, ValueUpdater<C, V> valueUpdater) {
+    int index = 0;
+    Element target = event.getEventTarget().cast();
+    Element wrapper = parent.getFirstChildElement();
+    while (wrapper != null) {
+      if (wrapper.isOrHasChild(target)) {
+        return onBrowserEventImpl(wrapper, value, viewData, event,
+            valueUpdater, hasCells.get(index));
+      }
+
+      index++;
+      wrapper = wrapper.getNextSiblingElement();
+    }
+    return viewData;
+  }
+
+  /**
+   * Remove a {@link HasCell} from the composite.
+   * 
+   * @param hasCell the {@link HasCell} to remove
+   */
+  public void removeHasCell(HasCell<C, ?, V> hasCell) {
+    hasCells.remove(hasCell);
+  }
+
+  @Override
+  public void render(C value, V viewData, StringBuilder sb) {
+    for (HasCell<C, ?, V> hasCell : hasCells) {
+      render(value, viewData, sb, hasCell);
+    }
+  }
+
+  @Override
+  public void setValue(Element parent, C object, V viewData) {
+    for (HasCell<C, ?, V> hasCell : hasCells) {
+      setValueImpl(parent, object, viewData, hasCell);
+    }
+  }
+
+  protected <X> void render(C value, V viewData, StringBuilder sb,
+      HasCell<C, X, V> hasCell) {
+    Cell<X, V> cell = hasCell.getCell();
+    sb.append("<span>");
+    cell.render(hasCell.getValue(value), viewData, sb);
+    sb.append("</span>");
+  }
+
+  private <X> V onBrowserEventImpl(Element parent, final C object, V viewData,
+      NativeEvent event, final ValueUpdater<C, V> valueUpdater,
+      final HasCell<C, X, V> hasCell) {
+    ValueUpdater<X, V> tempUpdater = null;
+    final FieldUpdater<C, X, V> fieldUpdater = hasCell.getFieldUpdater();
+    if (fieldUpdater != null) {
+      tempUpdater = new ValueUpdater<X, V>() {
+        public void update(X value, V viewData) {
+          fieldUpdater.update(-1, object, value, viewData);
+          if (valueUpdater != null) {
+            valueUpdater.update(object, viewData);
+          }
+        }
+      };
+    }
+    Cell<X, V> cell = hasCell.getCell();
+    return cell.onBrowserEvent(parent, hasCell.getValue(object), viewData,
+        event, tempUpdater);
+  }
+
+  private <X> void setValueImpl(Element parent, C object, V viewData,
+      HasCell<C, X, V> hasCell) {
+    hasCell.getCell().setValue(parent, hasCell.getValue(object), viewData);
+  }
+}
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/HasCell.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/HasCell.java
index c4a4228..69315ca 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/client/HasCell.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/HasCell.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -22,15 +22,13 @@
  * An interface for extracting a value from an underlying data type, provide a
  * cell to render that value, and provide a FieldUpdater to perform notification
  * of updates to the cell.
- *
+ * 
  * @param <T> the underlying data type
  * @param <C> the cell data type
  * @param <V> the view data type
  */
 public interface HasCell<T, C, V> {
 
-  boolean dependsOnSelection();
-
   Cell<C, V> getCell();
 
   FieldUpdater<T, C, V> getFieldUpdater();
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/ListView.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/ListView.java
index 3b13cb3..108f9e1 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/client/ListView.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/ListView.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -16,19 +16,20 @@
 package com.google.gwt.bikeshed.list.client;
 
 import com.google.gwt.bikeshed.list.shared.Range;
+import com.google.gwt.bikeshed.list.shared.SelectionModel;
 
 import java.util.List;
 
 /**
  * A list view.
- *
+ * 
  * @param <T> the data type of each row
  */
 public interface ListView<T> {
 
   /**
    * A list view delegate, implemented by classes that supply data to a view.
-   *
+   * 
    * @param <T> the data type of each row
    */
   public interface Delegate<T> {
@@ -39,7 +40,21 @@
 
   Range getRange();
 
+  /**
+   * Set a range of data in the view.
+   * 
+   * @param start the start index of the data
+   * @param length the length of the data
+   * @param values the values within the range
+   */
   void setData(int start, int length, List<T> values);
 
   void setDataSize(int size, boolean isExact);
+
+  /**
+   * Set the {@link SelectionModel} used by this {@link ListView}.
+   * 
+   * @param selectionModel the {@link SelectionModel}
+   */
+  void setSelectionModel(SelectionModel<? super T> selectionModel);
 }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java
index 6a1e047..8d98aa1 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -41,7 +41,7 @@
 
 /**
  * A list view that supports paging and columns.
- *
+ * 
  * @param <T> the data type of each row
  */
 public class PagingTableListView<T> extends Widget implements ListView<T> {
@@ -70,7 +70,8 @@
   private ProvidesKey<T> providesKey;
 
   private HandlerRegistration selectionHandler;
-  private SelectionModel<T> selectionModel;
+  private SelectionModel<? super T> selectionModel;
+
   private TableElement table;
   private TableSectionElement tbody;
   private TableSectionElement tfoot;
@@ -86,7 +87,7 @@
 
   /**
    * Constructs a table with the given page size.
-   *
+   * 
    * @param pageSize the page size
    */
   public PagingTableListView(final int pageSize) {
@@ -349,6 +350,8 @@
         TableCellElement cell = row.getCells().getItem(c);
         StringBuilder sb = new StringBuilder();
         columns.get(c).render(q, sb);
+        // TODO(jlabanca): Render as one HTML string instead of embedding HTML
+        // in each cell.
         cell.setInnerHTML(sb.toString());
 
         // TODO: Really total hack! There's gotta be a better way...
@@ -362,7 +365,7 @@
 
   public void setDataSize(int size, boolean isExact) {
     this.size = size;
-    refresh();
+    updateRowVisibility();
   }
 
   public void setDelegate(Delegate<T> delegate) {
@@ -371,9 +374,9 @@
 
   /**
    * Set the number of rows per page and refresh the table.
-   *
+   * 
    * @param pageSize the page size
-   *
+   * 
    * @throw {@link IllegalArgumentException} if pageSize is negative or 0
    */
   public void setPageSize(int pageSize) {
@@ -396,9 +399,9 @@
   }
 
   /**
-   * Set the starting index of the current visible page.  The actual page
-   * start will be clamped in the range [0, getSize() - 1].
-   *
+   * Set the starting index of the current visible page. The actual page start
+   * will be clamped in the range [0, getSize() - 1].
+   * 
    * @param pageStart the index of the row that should appear at the start of
    *          the page
    */
@@ -410,19 +413,16 @@
   /**
    * Sets the {@link ProvidesKey} instance that will be used to generate keys
    * for each record object as needed.
-   *
-   * @param providesKey an instance of {@link ProvidesKey<T>} used to generate
-   *          keys for record objects.
+   * 
+   * @param providesKey an instance of {@link ProvidesKey} used to generate keys
+   *          for record objects.
    */
-  // TODO - when is this valid?  Do we rehash column view data if it changes?
+  // TODO - when is this valid? Do we rehash column view data if it changes?
   public void setProvidesKey(ProvidesKey<T> providesKey) {
     this.providesKey = providesKey;
   }
 
-  /**
-   * Sets the selection model.
-   */
-  public void setSelectionModel(SelectionModel<T> selectionModel) {
+  public void setSelectionModel(SelectionModel<? super T> selectionModel) {
     if (selectionHandler != null) {
       selectionHandler.removeHandler();
       selectionHandler = null;
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleCellList.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleCellList.java
index 06156ca..d83fd87 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleCellList.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleCellList.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -17,172 +17,166 @@
 
 import com.google.gwt.bikeshed.cells.client.Cell;
 import com.google.gwt.bikeshed.cells.client.ValueUpdater;
+import com.google.gwt.bikeshed.list.client.impl.SimpleCellListImpl;
 import com.google.gwt.bikeshed.list.shared.Range;
-import com.google.gwt.bikeshed.list.shared.AbstractListViewAdapter.DefaultRange;
+import com.google.gwt.bikeshed.list.shared.SelectionModel;
 import com.google.gwt.dom.client.DivElement;
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.Style.Display;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.Widget;
 
-import java.util.ArrayList;
 import java.util.List;
 
 /**
  * A single column list of cells.
- *
+ * 
  * @param <T> the data type of list items
  */
 public class SimpleCellList<T> extends Widget implements ListView<T> {
 
+  /**
+   * Style name applied to even rows.
+   * TODO(jlabanca): These style names only apply to SideBySideTreeView.
+   */
+  private static final String STYLENNAME_EVEN = "gwt-sstree-evenRow";
+
+  /**
+   * Style name applied to odd rows.
+   * TODO(jlabanca): These style names only apply to SideBySideTreeView.
+   */
+  private static final String STYLENNAME_ODD = "gwt-sstree-oddRow";
+
+  /**
+   * Style name applied to selected rows.
+   * TODO(jlabanca): These style names only apply to SideBySideTreeView.
+   */
+  private static final String STYLENNAME_SELECTED = "gwt-sstree-selectedItem";
+
   private final Cell<T, Void> cell;
-  private final ArrayList<T> data = new ArrayList<T>();
-  private Delegate<T> delegate;
-  private int increment;
-  private int initialMaxSize;
-  private int maxSize;
-  private int seq; // for debugging - TODO: remove
-  private final Element showFewerElem;
-  private final Element showMoreElem;
-  private int size;
-  private final Element tmpElem;
+  private final SimpleCellListImpl<T> impl;
   private ValueUpdater<T, Void> valueUpdater;
 
   public SimpleCellList(Cell<T, Void> cell, int maxSize, int increment) {
-    this.initialMaxSize = this.maxSize = maxSize;
-    this.increment = increment;
     this.cell = cell;
-    this.seq = 0;
 
-    tmpElem = Document.get().createDivElement();
+    // Create the DOM hierarchy.
+    Element childContainer = Document.get().createDivElement();
 
-    showMoreElem = Document.get().createDivElement();
-    showMoreElem.setInnerHTML("<button>Show more</button>");
+    Element showMoreElem = Document.get().createPushButtonElement();
+    showMoreElem.setInnerText("Show more");
 
-    showFewerElem = Document.get().createDivElement();
-    showFewerElem.setInnerHTML("<button>Show fewer</button>");
+    Element showFewerElem = Document.get().createPushButtonElement();
+    showFewerElem.setInnerText("Show fewer");
 
-    showOrHide(showMoreElem, false);
-    showOrHide(showFewerElem, false);
+    Element emptyMessageElem = Document.get().createDivElement();
+    emptyMessageElem.setInnerHTML("<i>no data</i>");
 
     // TODO: find some way for cells to communicate what they're interested in.
     DivElement outerDiv = Document.get().createDivElement();
-    DivElement innerDiv = Document.get().createDivElement();
-    outerDiv.appendChild(innerDiv);
+    outerDiv.appendChild(childContainer);
+    outerDiv.appendChild(emptyMessageElem);
     outerDiv.appendChild(showFewerElem);
     outerDiv.appendChild(showMoreElem);
     setElement(outerDiv);
-    sinkEvents(Event.ONCLICK);
-    sinkEvents(Event.ONCHANGE);
+    sinkEvents(Event.ONCLICK | Event.ONCHANGE | Event.MOUSEEVENTS);
+
+    // Create the implementation.
+    impl = new SimpleCellListImpl<T>(this, cell, maxSize, increment,
+        childContainer, emptyMessageElem, showMoreElem, showFewerElem) {
+
+      @Override
+      protected void emitHtml(StringBuilder sb, List<T> values, int start,
+          Cell<T, Void> cell, SelectionModel<? super T> selectionModel) {
+        int length = values.size();
+        int end = start + length;
+        for (int i = start; i < end; i++) {
+          T value = values.get(i - start);
+          boolean isSelected = selectionModel == null ? false
+              : selectionModel.isSelected(value);
+          sb.append("<div __idx='").append(i).append("'");
+          sb.append(" class='");
+          sb.append(i % 2 == 0 ? STYLENNAME_EVEN : STYLENNAME_ODD);
+          if (isSelected) {
+            sb.append(" ").append(STYLENNAME_SELECTED);
+          }
+          sb.append("'>");
+          cell.render(value, null, sb);
+          sb.append("</div>");
+        }
+      }
+
+      @Override
+      protected void setSelected(Element elem, boolean selected) {
+        setStyleName(elem, STYLENNAME_SELECTED, selected);
+      }
+    };
   }
 
   public Range getRange() {
-    return new DefaultRange(0, maxSize);
+    return impl.getRange();
   }
 
   @Override
   public void onBrowserEvent(Event event) {
+    super.onBrowserEvent(event);
+
     Element target = event.getEventTarget().cast();
-    if (target.getParentElement() == showMoreElem) {
-      this.maxSize += increment;
-      sizeChanged();
-      if (delegate != null) {
-        delegate.onRangeChanged(this);
+    int type = event.getTypeInt();
+    if (type == Event.ONCLICK) {
+      // Open the node when the open image is clicked.
+      Element showFewerElem = impl.getShowFewerElem();
+      Element showMoreElem = impl.getShowMoreElem();
+      if (showFewerElem != null && showFewerElem.isOrHasChild(target)) {
+        impl.showFewer();
+        return;
+      } else if (showMoreElem != null && showMoreElem.isOrHasChild(target)) {
+        impl.showMore();
+        return;
       }
-    } else if (target.getParentElement() == showFewerElem) {
-      this.maxSize = Math.max(initialMaxSize, maxSize - increment);
-      sizeChanged();
-      if (delegate != null) {
-        delegate.onRangeChanged(this);
-      }
-    } else {
-      String idxString = "";
-      while ((target != null)
-          && ((idxString = target.getAttribute("__idx")).length() == 0)) {
-        target = target.getParentElement();
-      }
-      if (idxString.length() > 0) {
-        int idx = Integer.parseInt(idxString);
-        cell.onBrowserEvent(target, data.get(idx), null, event, valueUpdater);
+    }
+
+    // Forward the event to the cell.
+    String idxString = "";
+    while ((target != null)
+        && ((idxString = target.getAttribute("__idx")).length() == 0)) {
+      target = target.getParentElement();
+    }
+    if (idxString.length() > 0) {
+      int idx = Integer.parseInt(idxString);
+      T value = impl.getValue(idx);
+      cell.onBrowserEvent(target, value, null, event, valueUpdater);
+      if (!cell.consumesEvents() && type == Event.ONMOUSEDOWN) {
+        SelectionModel<? super T> selectionModel = impl.getSelectionModel();
+        if (selectionModel != null) {
+          selectionModel.setSelected(value, true);
+        }
       }
     }
   }
 
   public void setData(int start, int length, List<T> values) {
-    // Construct a run of element from start (inclusive) to start + length (exclusive)
-    StringBuilder sb = new StringBuilder();
-    for (int i = 0; i < length; i++) {
-      sb.append("<div __idx='" + (start + i) + "' __seq='" + seq++ + "'>");
-      cell.render(values.get(i), null, sb);
-      sb.append("</div>");
-    }
-
-    Element parent = getElement().getFirstChildElement();
-    if (start == 0 && length == maxSize) {
-      parent.setInnerHTML(sb.toString());
-    } else {
-      makeElements();
-      tmpElem.setInnerHTML(sb.toString());
-      for (int i = 0; i < length; i++) {
-        Element child = parent.getChild(start + i).cast();
-        parent.replaceChild(tmpElem.getChild(0), child);
-      }
-    }
+    impl.setData(values, start);
   }
 
   public void setDataSize(int size, boolean isExact) {
-    this.size = size;
-    sizeChanged();
+    impl.setDataSize(size);
   }
 
   public void setDelegate(Delegate<T> delegate) {
-    this.delegate = delegate;
+    impl.setDelegate(delegate);
   }
 
+  public void setSelectionModel(final SelectionModel<? super T> selectionModel) {
+    impl.setSelectionModel(selectionModel);
+  }
+
+  /**
+   * Set the value updater to use when cells modify items.
+   * 
+   * @param valueUpdater the {@link ValueUpdater}
+   */
   public void setValueUpdater(ValueUpdater<T, Void> valueUpdater) {
     this.valueUpdater = valueUpdater;
   }
-
-  private void makeElements() {
-    Element parent = getElement().getFirstChildElement();
-    int childCount = parent.getChildCount();
-
-    int actualSize = Math.min(size, maxSize);
-    if (actualSize > childCount) {
-      // Create new elements with a "loading..." message
-      StringBuilder sb = new StringBuilder();
-      int newElements = actualSize - childCount;
-      for (int i = 0; i < newElements; i++) {
-        sb.append("<div __idx='" + (childCount + i) + "'><i>loading...</i></div>");
-      }
-
-      if (childCount == 0) {
-        parent.setInnerHTML(sb.toString());
-      } else {
-        tmpElem.setInnerHTML(sb.toString());
-        for (int i = 0; i < newElements; i++) {
-          parent.appendChild(tmpElem.getChild(0));
-        }
-      }
-    } else if (actualSize < childCount) {
-      // Remove excess elements
-      while (actualSize < childCount) {
-        parent.getChild(--childCount).removeFromParent();
-      }
-    }
-  }
-
-  private void showOrHide(Element element, boolean show) {
-    if (show) {
-      element.getStyle().clearDisplay();
-    } else {
-      element.getStyle().setDisplay(Display.NONE);
-    }
-  }
-
-  private void sizeChanged() {
-    showOrHide(showMoreElem, size > maxSize);
-    showOrHide(showFewerElem, maxSize > initialMaxSize);
-  }
 }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/impl/SimpleCellListImpl.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/impl/SimpleCellListImpl.java
new file mode 100644
index 0000000..726aecc
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/impl/SimpleCellListImpl.java
@@ -0,0 +1,340 @@
+/*
+ * 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.list.client.impl;
+
+import com.google.gwt.bikeshed.cells.client.Cell;
+import com.google.gwt.bikeshed.list.client.ListView;
+import com.google.gwt.bikeshed.list.client.ListView.Delegate;
+import com.google.gwt.bikeshed.list.shared.Range;
+import com.google.gwt.bikeshed.list.shared.SelectionModel;
+import com.google.gwt.bikeshed.list.shared.AbstractListViewAdapter.DefaultRange;
+import com.google.gwt.bikeshed.list.shared.SelectionModel.SelectionChangeEvent;
+import com.google.gwt.bikeshed.list.shared.SelectionModel.SelectionChangeHandler;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.event.shared.HandlerRegistration;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Implementation of {@link SimpleCellListImpl}. This class is subject to change
+ * or deletion. Do not rely on this class.
+ * 
+ * @param <T> the data type of items in the list
+ */
+public abstract class SimpleCellListImpl<T> {
+
+  private final Cell<T, Void> cell;
+  private final Element childContainer;
+  private final List<T> data = new ArrayList<T>();
+  private final Set<Object> selectedKeys = new HashSet<Object>();
+  private Delegate<T> delegate;
+  private final Element emptyMessageElem;
+  private final int increment;
+  private final int initialMaxSize;
+  private final ListView<T> listView;
+  private int maxSize;
+  private HandlerRegistration selectionHandler;
+  private SelectionModel<? super T> selectionModel;
+  private final Element showFewerElem;
+  private final Element showMoreElem;
+  private int size;
+  private final Element tmpElem;
+
+  public SimpleCellListImpl(ListView<T> listView, Cell<T, Void> cell,
+      int maxSize, int increment, Element childContainer,
+      Element emptyMessageElem, Element showMoreElem, Element showFewerElem) {
+    this.cell = cell;
+    this.childContainer = childContainer;
+    this.emptyMessageElem = emptyMessageElem;
+    this.increment = increment;
+    this.initialMaxSize = maxSize;
+    this.listView = listView;
+    this.maxSize = maxSize;
+    this.showFewerElem = showFewerElem;
+    this.showMoreElem = showMoreElem;
+    tmpElem = Document.get().createDivElement();
+
+    showOrHide(showMoreElem, false);
+    showOrHide(showFewerElem, false);
+    showOrHide(emptyMessageElem, false);
+  }
+
+  public Range getRange() {
+    return new DefaultRange(0, maxSize);
+  }
+
+  public SelectionModel<? super T> getSelectionModel() {
+    return selectionModel;
+  }
+
+  public Element getShowFewerElem() {
+    return showFewerElem;
+  }
+
+  public Element getShowMoreElem() {
+    return showMoreElem;
+  }
+
+  public T getValue(int i) {
+    return data.get(i);
+  }
+
+  /**
+   * Set the data in the list.
+   * 
+   * @param values the new data
+   * @param start the start index
+   */
+  public void setData(List<T> values, int start) {
+    int oldSize = size;
+    int len = values.size();
+    int end = start + len;
+
+    // The size must be at least as large as the data.
+    if (end > oldSize) {
+      size = end;
+      sizeChanged();
+    }
+
+    // Create placeholders up to the specified index.
+    while (data.size() < start) {
+      data.add(null);
+    }
+
+    // Insert the new values into the data array.
+    for (int i = start; i < end; i++) {
+      T value = values.get(i - start);
+      if (i < data.size()) {
+        data.set(i, value);
+      } else {
+        data.add(value);
+
+        // Update our local cache of selected values. We only need to consider
+        // new values at this point. If any existing value changes its selection
+        // state, we'll find out from the selection model.
+        if (selectionModel != null && selectionModel.isSelected(value)) {
+          selectedKeys.add(getKey(value));
+        }
+      }
+    }
+
+    // Construct a run of element from start (inclusive) to start + len
+    // (exclusive)
+    StringBuilder sb = new StringBuilder();
+    emitHtml(sb, values, start, cell, selectionModel);
+
+    // Replace the DOM elements with the new rendered cells.
+    if (oldSize == 0 || (start == 0 && len >= oldSize)) {
+      childContainer.setInnerHTML(sb.toString());
+    } else {
+      makeElements();
+      tmpElem.setInnerHTML(sb.toString());
+      Element toReplace = childContainer.getChild(start).cast();
+      for (int i = start; i < end; i++) {
+        // The child will be removed from tmpElem, so always use index 0.
+        Element nextSibling = toReplace.getNextSiblingElement();
+        childContainer.replaceChild(tmpElem.getChild(0), toReplace);
+        toReplace = nextSibling;
+      }
+    }
+  }
+
+  /**
+   * Set the overall size of the list.
+   * 
+   * @param size the overall size
+   */
+  public void setDataSize(int size) {
+    this.size = size;
+    int toRemove = data.size() - size;
+    for (int i = 0; i < toRemove; i++) {
+      removeLastItem();
+    }
+    sizeChanged();
+  }
+
+  public void setDelegate(Delegate<T> delegate) {
+    this.delegate = delegate;
+  }
+
+  public void setSelectionModel(final SelectionModel<? super T> selectionModel) {
+    // Remove the old selection model.
+    if (selectionHandler != null) {
+      selectionHandler.removeHandler();
+      selectionHandler = null;
+    }
+
+    // Set the new selection model.
+    this.selectionModel = selectionModel;
+    if (selectionModel != null) {
+      selectionHandler = selectionModel.addSelectionChangeHandler(new SelectionChangeHandler() {
+        public void onSelectionChange(SelectionChangeEvent event) {
+          // Determine if our selection states are stale.
+          boolean dependsOnSelection = cell.dependsOnSelection();
+          boolean refreshRequired = false;
+          Element cellElem = childContainer.getFirstChildElement();
+          for (T value : data) {
+            boolean selected = selectionModel.isSelected(value);
+            Object key = getKey(value);
+            if (selected != selectedKeys.contains(key)) {
+              refreshRequired = true;
+              if (selected) {
+                selectedKeys.add(key);
+              } else {
+                selectedKeys.remove(key);
+              }
+              if (!dependsOnSelection) {
+                // The cell doesn't depend on Selection, so we only need to
+                // update the style.
+                setSelected(cellElem, selected);
+              }
+            }
+            cellElem = cellElem.getNextSiblingElement();
+          }
+
+          // Refresh the entire list if needed.
+          if (refreshRequired && dependsOnSelection) {
+            setData(data, 0);
+          }
+        }
+      });
+    }
+  }
+
+  /**
+   * Show fewer items.
+   */
+  public void showFewer() {
+    this.maxSize = Math.max(initialMaxSize, maxSize - increment);
+    sizeChanged();
+    if (delegate != null) {
+      delegate.onRangeChanged(listView);
+    }
+  }
+
+  /**
+   * Show more items.
+   */
+  public void showMore() {
+    this.maxSize += increment;
+    sizeChanged();
+    if (delegate != null) {
+      delegate.onRangeChanged(listView);
+    }
+  }
+
+  /**
+   * Construct the HTML that represents the list of items.
+   * 
+   * @param sb the {@link StringBuilder} to build into
+   * @param values the values to render
+   * @param start the start index
+   * @param cell the cell to use as a renderer
+   * @param selectionModel the {@link SelectionModel}
+   */
+  protected abstract void emitHtml(StringBuilder sb, List<T> values, int start,
+      Cell<T, Void> cell, SelectionModel<? super T> selectionModel);
+
+  /**
+   * Remove the last element from the list.
+   */
+  protected void removeLastItem() {
+    data.remove(data.size() - 1);
+    childContainer.getLastChild().removeFromParent();
+  }
+
+  /**
+   * Mark an element as selected or unselected. This is called when a cells
+   * selection state changes, but the cell does not depend on selection.
+   * 
+   * @param elem the element to modify
+   * @param selected true if selected, false if not
+   */
+  protected abstract void setSelected(Element elem, boolean selected);
+
+  /**
+   * Get the key for a given item.
+   * 
+   * @param value the item
+   * @return the key, or null if there is no selection model
+   */
+  private Object getKey(T value) {
+    return selectionModel == null ? null
+        : selectionModel.getKeyProvider().getKey(value);
+  }
+
+  /**
+   * Create placeholder elements that will be replaced with data. This is used s
+   * when replacing a subset of the list.
+   */
+  private void makeElements() {
+    int childCount = childContainer.getChildCount();
+    int actualSize = Math.min(data.size(), maxSize);
+    if (actualSize > childCount) {
+      // Create new elements with a "loading..." message
+      StringBuilder sb = new StringBuilder();
+      int newElements = actualSize - childCount;
+      for (int i = 0; i < newElements; i++) {
+        // TODO(jlabanca): Make this I18N friendly.
+        sb.append("<div __idx='" + (childCount + i)
+            + "'><i>loading...</i></div>");
+      }
+
+      if (childCount == 0) {
+        childContainer.setInnerHTML(sb.toString());
+      } else {
+        tmpElem.setInnerHTML(sb.toString());
+        for (int i = 0; i < newElements; i++) {
+          childContainer.appendChild(tmpElem.getChild(0));
+        }
+      }
+    } else if (actualSize < childCount) {
+      // Remove excess elements
+      while (actualSize < childCount) {
+        removeLastItem();
+        childCount--;
+      }
+    }
+  }
+
+  /**
+   * Show or hide an element.
+   * 
+   * @param element the element
+   * @param show true to show, false to hide
+   */
+  private void showOrHide(Element element, boolean show) {
+    if (show) {
+      element.getStyle().clearDisplay();
+    } else {
+      element.getStyle().setDisplay(Display.NONE);
+    }
+  }
+
+  /**
+   * Called when the size of the list changes.
+   */
+  private void sizeChanged() {
+    showOrHide(showMoreElem, size > maxSize);
+    showOrHide(showFewerElem, maxSize > initialMaxSize);
+    showOrHide(emptyMessageElem, size == 0);
+  }
+}
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MultiSelectionModel.java b/bikeshed/src/com/google/gwt/bikeshed/list/shared/MultiSelectionModel.java
similarity index 75%
rename from bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MultiSelectionModel.java
rename to bikeshed/src/com/google/gwt/bikeshed/list/shared/MultiSelectionModel.java
index 6ebe91f..9d6d776 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MultiSelectionModel.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/shared/MultiSelectionModel.java
@@ -1,19 +1,19 @@
 /*
  * 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.sample.bikeshed.cookbook.client;
+package com.google.gwt.bikeshed.list.shared;
 
 import com.google.gwt.bikeshed.list.shared.SelectionModel.AbstractSelectionModel;
 
@@ -22,26 +22,34 @@
 
 /**
  * A simple selection model that allows multiple objects to be selected.
- *
+ * 
  * @param <T> the record data type
  */
 public class MultiSelectionModel<T> extends AbstractSelectionModel<T> {
 
   private Set<T> selectedSet = new TreeSet<T>();
+  private Set<Object> selectedKeys = new TreeSet<Object>();
 
+  /**
+   * Get the set of selected items.
+   * 
+   * @return the set of selected items
+   */
   public Set<T> getSelectedSet() {
     return selectedSet;
   }
 
   public boolean isSelected(T object) {
-    return selectedSet.contains(object);
+    return selectedKeys.contains(getKeyProvider().getKey(object));
   }
 
   public void setSelected(T object, boolean selected) {
     if (selected) {
       selectedSet.add(object);
+      selectedKeys.add(getKeyProvider().getKey(object));
     } else {
       selectedSet.remove(object);
+      selectedKeys.remove(getKeyProvider().getKey(object));
     }
     scheduleSelectionChangeEvent();
   }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/shared/SelectionModel.java b/bikeshed/src/com/google/gwt/bikeshed/list/shared/SelectionModel.java
index 3b3a0ee..e6ef41f 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/shared/SelectionModel.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/shared/SelectionModel.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -25,7 +25,7 @@
 
 /**
  * A model for selection within a list.
- *
+ * 
  * @param <T> the data type of records in the list
  */
 public interface SelectionModel<T> extends HasHandlers {
@@ -37,7 +37,7 @@
 
     /**
      * Called when {@link SelectionChangeEvent} is fired.
-     *
+     * 
      * @param event the {@link SelectionChangeEvent} that was fired
      */
     void onSelectionChange(SelectionChangeEvent event);
@@ -57,7 +57,7 @@
     /**
      * Fires a selection change event on all registered handlers in the handler
      * manager. If no such handlers exist, this method will do nothing.
-     *
+     * 
      * @param source the source of the handlers
      */
     static void fire(SelectionModel<?> source) {
@@ -69,7 +69,7 @@
 
     /**
      * Gets the type associated with this event.
-     *
+     * 
      * @return returns the handler type
      */
     public static Type<SelectionChangeHandler> getType() {
@@ -99,7 +99,7 @@
   /**
    * A default implementation of SelectionModel that provides listener addition
    * and removal.
-   *
+   * 
    * @param <T> the data type of records in the list
    */
   public abstract class AbstractSelectionModel<T> implements SelectionModel<T> {
@@ -127,7 +127,7 @@
      */
     public ProvidesKey<T> getKeyProvider() {
       if (keyProvider == null) {
-        keyProvider  = new ProvidesKey<T>() {
+        keyProvider = new ProvidesKey<T>() {
           public Object getKey(T item) {
             return item;
           }
@@ -137,6 +137,15 @@
     }
 
     /**
+     * Set the key provider for items in this model.
+     * 
+     * @param keyProvider the {@link ProvidesKey}
+     */
+    public void setKeyProvider(ProvidesKey<T> keyProvider) {
+      this.keyProvider = keyProvider;
+    }
+
+    /**
      * Schedules a {@link SelectionModel.SelectionChangeEvent} to fire at the
      * end of the current event loop.
      */
@@ -155,21 +164,21 @@
 
   /**
    * Adds a {@link SelectionChangeEvent} handler.
-   *
+   * 
    * @param handler the handler
    * @return the registration for the event
    */
   HandlerRegistration addSelectionChangeHandler(SelectionChangeHandler handler);
 
   /**
-   * Returns a ProvidesKey instance that may be used to provide a unique
-   * key for each record.
+   * Returns a ProvidesKey instance that may be used to provide a unique key for
+   * each record.
    */
   ProvidesKey<T> getKeyProvider();
 
   /**
    * Check if an object is selected.
-   *
+   * 
    * @param object the object
    * @return true if selected, false if not
    */
@@ -177,7 +186,7 @@
 
   /**
    * Set the selected state of an object.
-   *
+   * 
    * @param object the object to select or deselect
    * @param selected true to select, false to deselect
    */
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SingleSelectionModel.java b/bikeshed/src/com/google/gwt/bikeshed/list/shared/SingleSelectionModel.java
similarity index 72%
rename from bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SingleSelectionModel.java
rename to bikeshed/src/com/google/gwt/bikeshed/list/shared/SingleSelectionModel.java
index 7fae547..293c4c3 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SingleSelectionModel.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/shared/SingleSelectionModel.java
@@ -1,30 +1,31 @@
 /*
  * 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.sample.bikeshed.cookbook.client;
+package com.google.gwt.bikeshed.list.shared;
 
 import com.google.gwt.bikeshed.list.shared.SelectionModel.AbstractSelectionModel;
 
 /**
  * A simple selection model that allows only one object to be selected a a time.
- *
+ * 
  * @param <T> the record data type
  */
 public final class SingleSelectionModel<T> extends AbstractSelectionModel<T> {
 
   private T curSelection;
+  private Object curKey;
 
   /**
    * Gets the currently-selected object.
@@ -34,14 +35,22 @@
   }
 
   public boolean isSelected(T object) {
-    return object.equals(curSelection);
+    if (curSelection == null || curKey == null || object == null) {
+      return false;
+    }
+    ProvidesKey<T> keyProvider = getKeyProvider();
+    Object newKey = keyProvider.getKey(object);
+    return curKey.equals(newKey);
   }
 
   public void setSelected(T object, boolean selected) {
+    Object key = getKeyProvider().getKey(object);
     if (selected) {
       curSelection = object;
-    } else if (object.equals(curSelection)) {
+      curKey = key;
+    } else if (curKey != null && curKey.equals(key)) {
       curSelection = null;
+      curKey = null;
     }
     scheduleSelectionChangeEvent();
   }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeNodeView.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeNodeView.java
deleted file mode 100644
index 36e2b4c..0000000
--- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeNodeView.java
+++ /dev/null
@@ -1,253 +0,0 @@
-/*
- * 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.list.client.HasCell;
-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.Display;
-import com.google.gwt.dom.client.Style.Position;
-import com.google.gwt.dom.client.Style.Unit;
-import com.google.gwt.user.client.ui.RequiresResize;
-
-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 int columnWidth;
-
-  private final int imageLeft;
-
-  private int level;
-
-  private int maxColumns;
-
-  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
-   * @param maxColumns the maximum number of columns to display
-   */
-  SideBySideTreeNodeView(final TreeView tree, final SideBySideTreeNodeView<?> parent,
-      NodeInfo<T> parentNodeInfo, Element elem, T value, int level, String path,
-      int columnWidth, int maxColumns) {
-    super(tree, parent, parentNodeInfo, value);
-    this.imageLeft = columnWidth - 16 - tree.getImageWidth();
-    this.level = level;
-    this.path = path;
-    this.columnWidth = columnWidth;
-    this.maxColumns = maxColumns;
-
-    setElement(elem);
-  }
-
-  @Override
-  protected <C> TreeNodeView<C> createTreeNodeView(NodeInfo<C> nodeInfo,
-      Element childElem, C childValue, Void viewData, int idx) {
-    return new SideBySideTreeNodeView<C>(getTree(), this, nodeInfo, childElem,
-        childValue, level + 1, path + "-" + idx, columnWidth, maxColumns);
-  }
-
-  @Override
-  protected <C> void emitHtml(StringBuilder sb, List<C> childValues,
-      List<HasCell<C, ?, Void>> hasCells, List<TreeNodeView<?>> savedViews) {
-    TreeView tree = getTree();
-    TreeViewModel model = tree.getTreeViewModel();
-    int imageWidth = tree.getImageWidth();
-
-    int idx = 0;
-    for (C childValue : childValues) {
-      sb.append("<div id=\"" + path + "-" + idx +
-          "\" class=\"gwt-sstree-unselectedItem gwt-sstree-" +
-          ((idx % 2) == 0 ? "even" : "odd") + "Row\"" +
-          " 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, this)) {
-        sb.append(LEAF_IMAGE);
-      } else {
-        sb.append(tree.getClosedImageHtml(imageLeft));
-      }
-      sb.append("<div class=\"gwt-sstree-cell\">");
-      for (int i = 0; i < hasCells.size(); i++) {
-        sb.append("<span __idx='");
-        sb.append(i);
-        sb.append("'>");
-        render(sb, childValue, hasCells.get(i));
-        sb.append("</span>");
-      }
-      sb.append("</div></div>");
-
-      idx++;
-    }
-  }
-
-  /**
-   * Ensure that the child container exists and return it.
-   *
-   * @return the child container
-   */
-  @Override
-  protected Element ensureChildContainer() {
-    if (getChildContainer() == 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);
-      animFrame.setId("animFrame");
-      setChildContainer(animFrame.appendChild(Document.get().createDivElement()));
-    }
-
-    // TODO(jgw): Kind of a hack. We should probably be propagating onResize()
-    // down from the TreeView, but this is simpler for the moment.
-    TreeView tree = getTree();
-    if (tree instanceof RequiresResize) {
-      ((RequiresResize) tree).onResize();
-    }
-
-    return getChildContainer();
-  }
-
-  /**
-   * @return the element that contains the rendered cell
-   */
-  @Override
-  protected Element getCellParent() {
-    return getElement().getChild(1).cast();
-  }
-
-  @Override
-  protected Element getContainer() {
-    return getTree().getElement().getChild(level).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-sstree-selectedItem");
-        } else {
-          if (sibling.getState()) {
-            sibling.setState(false);
-          }
-
-          container.setClassName("gwt-sstree-unselectedItem " +
-              (((i % 2) == 0) ? "gwt-sstree-evenRow" : "gwt-sstree-oddRow"));
-        }
-      }
-    }
-  }
-
-  /**
-   * Returns the container for child nodes at the given level.
-   */
-  private Element createContainer(int level) {
-    // Resize the root element
-    Element rootElement = getTree().getElement();
-    rootElement.getStyle().setWidth(Math.min(maxColumns, level + 1) * columnWidth, 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-sstree-column");
-      Style style = div.getStyle();
-      style.setPosition(Position.ABSOLUTE);
-      style.setTop(0, Unit.PX);
-      style.setWidth(columnWidth, Unit.PX);
-
-      childCount++;
-    }
-
-    Element child = rootElement.getFirstChild().cast();
-
-    int x = Math.min(0, maxColumns - (level + 1));
-    while (child != null) {
-      Style style = child.getStyle();
-      style.setLeft(x * columnWidth, Unit.PX);
-      if (x < 0) {
-        style.setDisplay(Display.NONE);
-      } else {
-        style.clearDisplay();
-      }
-      child = child.getNextSibling().cast();
-      x++;
-    }
-
-    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 = getTree().getElement();
-    rootElement.getStyle().setWidth((level + 1) * columnWidth, Unit.PX);
-
-    // Create children of the root container as needed.
-    int childCount = rootElement.getChildCount();
-    while (childCount > level) {
-      rootElement.removeChild(rootElement.getLastChild());
-      childCount--;
-    }
-
-    setChildContainer(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
index 2f048db..500c007 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeView.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeView.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -15,143 +15,615 @@
  */
 package com.google.gwt.bikeshed.tree.client;
 
-import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.animation.client.Animation;
+import com.google.gwt.bikeshed.cells.client.Cell;
+import com.google.gwt.bikeshed.cells.client.ValueUpdater;
+import com.google.gwt.bikeshed.list.client.ListView;
+import com.google.gwt.bikeshed.list.client.SimpleCellList;
+import com.google.gwt.bikeshed.list.shared.ProvidesKey;
+import com.google.gwt.bikeshed.list.shared.Range;
+import com.google.gwt.bikeshed.list.shared.SelectionModel;
+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.Node;
-import com.google.gwt.dom.client.NodeList;
-import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Style.Overflow;
 import com.google.gwt.dom.client.Style.Position;
 import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.dom.client.Style.Visibility;
 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.ProvidesResize;
+import com.google.gwt.user.client.ui.RequiresResize;
+import com.google.gwt.user.client.ui.ScrollPanel;
+import com.google.gwt.user.client.ui.SplitLayoutPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * A view of a tree.
  */
-public class SideBySideTreeView extends TreeView {
-
-  protected int columnWidth;
-
-  protected int maxColumns;
+public class SideBySideTreeView extends TreeView implements ProvidesResize,
+    RequiresResize, HasAnimation {
 
   /**
-   * Construct a new {@link TreeView} that will display as many columns as
-   * needed.
-   *
-   * @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
-   * @param columnWidth
+   * The element used in place of an image when a node has no children.
    */
-  public <T> SideBySideTreeView(TreeViewModel viewModel, T rootValue,
-      int columnWidth) {
-    this(viewModel, rootValue, columnWidth, Integer.MAX_VALUE);
+  private static final String LEAF_IMAGE = "<div style='position:absolute;display:none;'></div>";
+
+  /**
+   * The style name assigned to each column.
+   */
+  private static final String STYLENAME_COLUMN = "gwt-sstree-column";
+
+  /**
+   * The style name assigned to each column.
+   */
+  private static final String STYLENAME_OPEN = "gwt-sstree-openItem";
+
+  /**
+   * The prefix of the ID assigned to open cells.
+   */
+  private static final String ID_PREFIX_OPEN = "__gwt-sstree-open-";
+
+  /**
+   * The animation used to scroll to the newly added list view.
+   */
+  private class ScrollAnimation extends Animation {
+
+    /**
+     * The starting scroll position.
+     */
+    private int startScrollLeft;
+
+    /**
+     * The ending scroll position.
+     */
+    private int targetScrollLeft;
+
+    @Override
+    protected void onComplete() {
+      getElement().setScrollLeft(targetScrollLeft);
+    }
+
+    @Override
+    protected void onUpdate(double progress) {
+      int diff = targetScrollLeft - startScrollLeft;
+      getElement().setScrollLeft(startScrollLeft + (int) (diff * progress));
+    }
+
+    void scrollToEnd() {
+      Element elem = getElement();
+      targetScrollLeft = elem.getScrollWidth() - elem.getClientWidth();
+
+      if (isAnimationEnabled()) {
+        // Animate the scrolling.
+        startScrollLeft = elem.getScrollLeft();
+        run(250);
+      } else {
+        // Scroll instantly.
+        onComplete();
+      }
+    }
   }
 
   /**
+   * A wrapper around a cell that adds an open button.
+   * 
+   * @param <C> the data type of the cell
+   */
+  private class CellDecorator<C> extends Cell<C, Void> {
+
+    /**
+     * The cell used to render the inner contents.
+     */
+    private final Cell<C, Void> cell;
+
+    /**
+     * The level of this list view.
+     */
+    private final int level;
+
+    /**
+     * The key of the currently open item.
+     */
+    private Object openKey;
+
+    /**
+     * The key provider for the node.
+     */
+    private final ProvidesKey<C> providesKey;
+
+    /**
+     * Construct a new {@link CellDecorator}.
+     * 
+     * @param nodeInfo the {@link NodeInfo} associated with the cell
+     * @param level the level of items rendered by this decorator
+     */
+    public CellDecorator(NodeInfo<C> nodeInfo, int level) {
+      this.cell = nodeInfo.getCell();
+      this.level = level;
+      this.providesKey = nodeInfo.getProvidesKey();
+    }
+
+    @Override
+    public boolean consumesEvents() {
+      return cell.consumesEvents();
+    }
+
+    @Override
+    public boolean dependsOnSelection() {
+      return cell.dependsOnSelection();
+    }
+
+    public Object getOpenKey() {
+      return openKey;
+    }
+
+    @Override
+    public Void onBrowserEvent(Element parent, C value, Void viewData,
+        NativeEvent event, ValueUpdater<C, Void> valueUpdater) {
+      Element target = event.getEventTarget().cast();
+      if (getImageElement(parent).isOrHasChild(target)) {
+        if (Event.getTypeInt(event.getType()) == Event.ONMOUSEDOWN) {
+          trimToLevel(level);
+
+          // Remove style from currently open item.
+          Element curOpenItem = Document.get().getElementById(getOpenId());
+          if (curOpenItem != null) {
+            replaceImageElement(curOpenItem.getParentElement(), false);
+          }
+
+          // Save the key of the new open item and update the Element.
+          openKey = providesKey.getKey(value);
+          replaceImageElement(parent, true);
+
+          // Add a tree node for the next level.
+          appendTreeNode(getTreeViewModel().getNodeInfo(value));
+        }
+        return viewData;
+      } else {
+        return cell.onBrowserEvent(getCellParent(parent), value, viewData,
+            event, valueUpdater);
+      }
+    }
+
+    @Override
+    public void render(C value, Void viewData, StringBuilder sb) {
+      boolean isOpen = (openKey == null) ? false
+          : openKey.equals(providesKey.getKey(value));
+      int imageWidth = getImageWidth();
+      sb.append("<div style='position:relative;padding-right:");
+      sb.append(imageWidth);
+      sb.append("px;'");
+      if (isOpen) {
+        sb.append(" class='").append(STYLENAME_OPEN).append("'");
+        sb.append(" id='").append(getOpenId()).append("'");
+      }
+      sb.append(">");
+      if (isOpen) {
+        sb.append(getOpenImageHtml());
+      } else if (getTreeViewModel().isLeaf(value)) {
+        sb.append(LEAF_IMAGE);
+      } else {
+        sb.append(getClosedImageHtml());
+      }
+      sb.append("<div>");
+      cell.render(value, viewData, sb);
+      sb.append("</div></div>");
+    }
+
+    @Override
+    public void setValue(Element parent, C value, Void viewData) {
+      cell.setValue(getCellParent(parent), value, viewData);
+    }
+
+    /**
+     * Get the parent element of the decorated cell.
+     * 
+     * @param parent the parent of this cell
+     * @return the decorated cell's parent
+     */
+    private Element getCellParent(Element parent) {
+      return parent.getFirstChildElement().getChild(1).cast();
+    }
+
+    /**
+     * Get the image element of the decorated cell.
+     * 
+     * @param parent the parent of this cell
+     * @return the image element
+     */
+    private Element getImageElement(Element parent) {
+      return parent.getFirstChildElement().getFirstChildElement();
+    }
+
+    /**
+     * Get the ID of the open element.
+     * 
+     * @return the ID
+     */
+    private String getOpenId() {
+      return ID_PREFIX_OPEN + level + "-" + uniqueId;
+    }
+
+    /**
+     * Replace the image element of a cell.
+     * 
+     * @param parent the parent element of the cell
+     * @param open true if open, false if closed
+     */
+    private void replaceImageElement(Element parent, boolean open) {
+      // Update the style name and ID.
+      Element wrapper = parent.getFirstChildElement();
+      if (open) {
+        wrapper.addClassName(STYLENAME_OPEN);
+        wrapper.setId(getOpenId());
+      } else {
+        wrapper.removeClassName(STYLENAME_OPEN);
+        wrapper.setId("");
+      }
+
+      // Replace the image element.
+      String html = open ? getOpenImageHtml() : getClosedImageHtml();
+      Element tmp = Document.get().createDivElement();
+      tmp.setInnerHTML(html);
+      Element imageElem = tmp.getFirstChildElement();
+      Element oldImg = getImageElement(parent);
+      wrapper.replaceChild(imageElem, oldImg);
+    }
+  }
+
+  /**
+   * A node in the tree.
+   * 
+   * @param <C> the data type of the children of the node
+   */
+  private class TreeNode<C> {
+    private CellDecorator<C> cell;
+    private ListView<C> listView;
+    private NodeInfo<C> nodeInfo;
+    private Widget widget;
+
+    /**
+     * Construct a new {@link TreeNode}.
+     * 
+     * @param nodeInfo the nodeInfo for the children nodes
+     * @param listView the list view assocated with the node
+     * @param widget the widget that represents the list view
+     */
+    public TreeNode(NodeInfo<C> nodeInfo, ListView<C> listView,
+        CellDecorator<C> cell, Widget widget) {
+      this.cell = cell;
+      this.listView = listView;
+      this.nodeInfo = nodeInfo;
+      this.widget = widget;
+    }
+
+    /**
+     * Get the {@link CellDecorator} used to render the node.
+     * 
+     * @return the cell decorator
+     */
+    public CellDecorator<C> getCell() {
+      return cell;
+    }
+
+    /**
+     * Get the widget that represents this {@link TreeNode}.
+     * 
+     * @return the widget
+     */
+    public Widget getWidget() {
+      return widget;
+    }
+
+    /**
+     * Unregister the list view and remove it from the widget.
+     */
+    void cleanup() {
+      listView.setSelectionModel(null);
+      nodeInfo.unsetView();
+      getSplitLayoutPanel().remove(widget);
+    }
+  }
+
+  /**
+   * The counter used to assigned unique IDs.
+   */
+  private static int NEXT_ID = 0;
+
+  /**
+   * The animation used for scrolling.
+   */
+  private final ScrollAnimation animation = new ScrollAnimation();
+
+  /**
+   * The default width of new columns.
+   */
+  private int defaultWidth = 200;
+
+  /**
+   * The HTML used to generate the closed image.
+   */
+  private String closedImageHtml;
+
+  /**
+   * The unique ID assigned to this tree view.
+   */
+  private final int uniqueId = NEXT_ID++;
+
+  /**
+   * A boolean indicating whether or not animations are enabled.
+   */
+  private boolean isAnimationEnabled;
+
+  /**
+   * The minimum width of new columns.
+   */
+  private int minWidth = getImageWidth() + 20;
+
+  /**
+   * The HTML used to generate the open image.
+   */
+  private String openImageHtml;
+
+  /**
+   * The element used to maintain the scrollbar when columns are removed.
+   */
+  private Element scrollLock;
+
+  /**
+   * The visible {@link TreeNode}.
+   */
+  private List<TreeNode<?>> treeNodes = new ArrayList<TreeNode<?>>();
+
+  /**
    * 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
-   * @param columnWidth the width of each column
-   * @param maxColumns the maximum number of columns to display horizontally
    */
-  public <T> SideBySideTreeView(TreeViewModel viewModel, T rootValue,
-      int columnWidth, int maxColumns) {
-    super(viewModel);
+  public <T> SideBySideTreeView(TreeViewModel viewModel, T rootValue) {
+    super(viewModel, new SplitLayoutPanel());
+    getElement().getStyle().setOverflow(Overflow.AUTO);
+    setStyleName("gwt-SideBySideTreeView");
 
-    this.columnWidth = columnWidth;
-    this.maxColumns = maxColumns;
+    // Add a placeholder to maintain the scroll width.
+    scrollLock = Document.get().createDivElement();
+    scrollLock.getStyle().setPosition(Position.ABSOLUTE);
+    scrollLock.getStyle().setVisibility(Visibility.HIDDEN);
+    scrollLock.getStyle().setZIndex(-32767);
+    scrollLock.getStyle().setBackgroundColor("red");
+    scrollLock.getStyle().setTop(0, Unit.PX);
+    scrollLock.getStyle().setLeft(0, Unit.PX);
+    scrollLock.getStyle().setHeight(1, Unit.PX);
+    scrollLock.getStyle().setWidth(1, Unit.PX);
+    getElement().appendChild(scrollLock);
 
-    Element rootElement = Document.get().createDivElement();
-    rootElement.setClassName("gwt-sstree");
-    Style style = rootElement.getStyle();
-    style.setPosition(Position.RELATIVE);
-    style.setWidth(columnWidth, Unit.PX);
-    setElement(rootElement);
+    // Associate the first ListView with the rootValue.
+    appendTreeNode(viewModel.getNodeInfo(rootValue));
 
-    // Add event handlers.
-    sinkEvents(Event.ONCLICK | Event.ONMOUSEDOWN | Event.ONMOUSEUP | Event.ONCHANGE);
+    // Catch scroll events.
+    sinkEvents(Event.ONSCROLL);
+  }
 
-    // Associate a view with the item.
-    TreeNodeView<T> root = new SideBySideTreeNodeView<T>(this, null, null,
-        rootElement, rootValue, 0, "gwt-sstree", columnWidth, maxColumns);
-    setRootNode(root);
-    root.setState(true);
+  /**
+   * Get the default width of new columns.
+   * 
+   * @return the default width in pixels
+   */
+  public int getDefaultColumnWidth() {
+    return defaultWidth;
+  }
+
+  /**
+   * Get the minimum width of columns.
+   * 
+   * @return the minimum width in pixels
+   */
+  public int getMinimumColumnWidth() {
+    return minWidth;
+  }
+
+  public boolean isAnimationEnabled() {
+    return isAnimationEnabled;
   }
 
   @Override
   public void onBrowserEvent(Event event) {
-    super.onBrowserEvent(event);
-
-    int eventType = DOM.eventGetType(event);
-    switch (eventType) {
-      case Event.ONMOUSEUP: case Event.ONCHANGE:
-        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 = getRootTreeNodeView();
-            for (String s : path) {
-              nodeView = nodeView.getChildTreeNodeView(Integer.parseInt(s));
-            }
-            if (inCell) {
-              nodeView.fireEventToCell(event);
-
-              // TODO(jgw): Kind of a hacky way to set selection. Need to generalize
-              // this to some sort of keyboard/mouse->selection controller.
-              if (getSelectionModel() != null) {
-                String type = event.getType();
-                if ("mouseup".equals(type)) {
-                  getSelectionModel().setSelected(nodeView.getValue(), true);
-                }
-              }
-            } else {
-              nodeView.setState(!nodeView.getState());
-            }
-          }
-        }
+    switch (DOM.eventGetType(event)) {
+      case Event.ONSCROLL:
+        // Shorten the scroll bar is possible.
+        adjustScrollLock();
         break;
     }
+    super.onBrowserEvent(event);
   }
 
   public void onResize() {
-    if (!isAttached()) {
-      return;
-    }
+    getSplitLayoutPanel().onResize();
+  }
 
-    int height = getElement().getOffsetHeight();
-    NodeList<Node> children = getElement().getChildNodes();
-    for (int i = 0; i < children.getLength(); ++i) {
-      Element child = children.getItem(i).cast();
-      child.getStyle().setHeight(height, Unit.PX);
+  public void setAnimationEnabled(boolean enable) {
+    this.isAnimationEnabled = enable;
+  }
+
+  /**
+   * Set the default width of new columns.
+   * 
+   * @param width the default width in pixels
+   */
+  public void setDefaultColumnWidth(int width) {
+    this.defaultWidth = width;
+  }
+
+  /**
+   * Set the minimum width of columns.
+   * 
+   * @param minWidth the minimum width in pixels
+   */
+  public void setMinimumColumnWidth(int minWidth) {
+    this.minWidth = minWidth;
+  }
+
+  /**
+   * Create a {@link ListView} that will display items. The {@link ListView}
+   * must extend {@link com.google.gwt.user.client.ui.Widget}.
+   * 
+   * @param <C> the item type in the list view
+   * @param nodeInfo the node info with child data
+   * @param cell the cell to use in the list view
+   * @return the {@link ListView}
+   */
+  protected <C> ListView<C> createListView(NodeInfo<C> nodeInfo,
+      Cell<C, Void> cell) {
+    SimpleCellList<C> listView = new SimpleCellList<C>(cell, 100, 100);
+    listView.setValueUpdater(nodeInfo.getValueUpdater());
+    return listView;
+  }
+
+  /**
+   * Adjust the size of the scroll lock element based on the new position of the
+   * scroll bar.
+   */
+  private void adjustScrollLock() {
+    int scrollLeft = getElement().getScrollLeft();
+    if (scrollLeft > 0) {
+      int clientWidth = getElement().getClientWidth();
+      scrollLock.getStyle().setWidth(scrollLeft + clientWidth, Unit.PX);
+    } else {
+      scrollLock.getStyle().setWidth(1.0, Unit.PX);
     }
   }
 
-  @Override
-  protected void onLoad() {
-    // TODO(jgw): This is a total hack.
-    Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() {
-      public void execute() {
-        onResize();
+  /**
+   * Create a new {@link TreeNode} and append it to the end of the LayoutPanel.
+   * 
+   * @param <C> the data type of the children
+   * @param nodeInfo the info about the node
+   */
+  private <C> void appendTreeNode(final NodeInfo<C> nodeInfo) {
+    // Create the list view and its scrollable container.
+    final int level = treeNodes.size();
+    CellDecorator<C> cell = new CellDecorator<C>(nodeInfo, level);
+    final ListView<C> listView = createListView(nodeInfo, cell);
+    ScrollPanel scrollable = new ScrollPanel((Widget) listView);
+    scrollable.setStyleName(STYLENAME_COLUMN);
+
+    // Create a delegate list view so we can trap data changes.
+    ListView<C> listViewDelegate = new ListView<C>() {
+      public Range getRange() {
+        return listView.getRange();
       }
-    });
+
+      public void setData(int start, int length, List<C> values) {
+        // Trim to the current level if the open node no longer exists.
+        TreeNode<?> node = treeNodes.get(level);
+        Object openKey = node.getCell().openKey;
+        if (openKey != null) {
+          boolean stillExists = false;
+          ProvidesKey<C> keyProvider = nodeInfo.getProvidesKey();
+          for (C value : values) {
+            if (openKey.equals(keyProvider.getKey(value))) {
+              stillExists = true;
+              break;
+            }
+          }
+          if (!stillExists) {
+            trimToLevel(level);
+          }
+        }
+
+        // Refresh the list.
+        listView.setData(start, length, values);
+      }
+
+      public void setDataSize(int size, boolean isExact) {
+        listView.setDataSize(size, isExact);
+      }
+
+      public void setDelegate(Delegate<C> delegate) {
+        listView.setDelegate(delegate);
+      }
+
+      public void setSelectionModel(SelectionModel<? super C> selectionModel) {
+        listView.setSelectionModel(selectionModel);
+      }
+    };
+
+    // Create a TreeNode.
+    TreeNode<C> treeNode = new TreeNode<C>(nodeInfo, listViewDelegate, cell,
+        scrollable);
+    treeNodes.add(treeNode);
+
+    // Attach the view to the selection model and node info.
+    listView.setSelectionModel(nodeInfo.getSelectionModel());
+    nodeInfo.setView(listViewDelegate);
+
+    // Add the ListView to the LayoutPanel.
+    SplitLayoutPanel splitPanel = getSplitLayoutPanel();
+    splitPanel.insertWest(scrollable, defaultWidth, null);
+    splitPanel.setWidgetMinSize(scrollable, minWidth);
+    splitPanel.forceLayout();
+
+    // Scroll to the right.
+    animation.scrollToEnd();
+  }
+
+  /**
+   * @return the HTML to render the closed image.
+   */
+  private String getClosedImageHtml() {
+    if (closedImageHtml == null) {
+      AbstractImagePrototype proto = AbstractImagePrototype.create(getResources().treeClosed());
+      closedImageHtml = proto.getHTML().replace("style='",
+          "style='position:absolute;right:0px;top:0px;");
+    }
+    return closedImageHtml;
+  }
+
+  /**
+   * @return the HTML to render the open image.
+   */
+  private String getOpenImageHtml() {
+    if (openImageHtml == null) {
+      AbstractImagePrototype proto = AbstractImagePrototype.create(getResources().treeOpen());
+      openImageHtml = proto.getHTML().replace("style='",
+          "style='position:absolute;right:0px;top:0px;");
+    }
+    return openImageHtml;
+  }
+
+  /**
+   * Get the {@link SplitLayoutPanel} used to lay out the views.
+   * 
+   * @return the {@link SplitLayoutPanel}
+   */
+  private SplitLayoutPanel getSplitLayoutPanel() {
+    return (SplitLayoutPanel) getWidget();
+  }
+
+  /**
+   * Reduce the number of {@link ListView} down to the specified level.
+   * 
+   * @param level the level to trim to
+   */
+  private void trimToLevel(int level) {
+    // Add a placeholder to maintain the same scroll width.
+    adjustScrollLock();
+
+    // Remove the listViews that are no longer needed.
+    int curLevel = treeNodes.size() - 1;
+    while (curLevel > level) {
+      TreeNode<?> removed = treeNodes.remove(curLevel);
+      removed.cleanup();
+      curLevel--;
+    }
   }
 }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeNodeView.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeNodeView.java
index 73d662b..46399e6 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeNodeView.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeNodeView.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -15,122 +15,616 @@
  */
 package com.google.gwt.bikeshed.tree.client;
 
-import com.google.gwt.bikeshed.list.client.HasCell;
+import com.google.gwt.bikeshed.cells.client.Cell;
+import com.google.gwt.bikeshed.list.client.ListView;
+import com.google.gwt.bikeshed.list.client.impl.SimpleCellListImpl;
+import com.google.gwt.bikeshed.list.shared.ProvidesKey;
+import com.google.gwt.bikeshed.list.shared.Range;
 import com.google.gwt.bikeshed.list.shared.SelectionModel;
 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.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.ui.UIObject;
 
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * A view of a tree node.
- *
- * @param <T> the type that this {@link TreeNodeView} contains
+ * 
+ * @param <T> the type that this view contains
  */
-public class StandardTreeNodeView<T> extends TreeNodeView<T> {
+class StandardTreeNodeView<T> extends UIObject {
 
   /**
-   * Construct a {@link TreeNodeView}.
-   *
-   * @param tree the parent {@link TreeView}
-   * @param parent the parent {@link TreeNodeView}
+   * 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>";
+
+  /**
+   * Style name applied to selected rows.
+   */
+  private static final String STYLENNAME_SELECTED = "gwt-tree-selectedItem";
+
+  /**
+   * Returns the element that parents the cell contents of the node.
+   * 
+   * @param nodeElem the element that represents the node
+   * @return the cell parent within the node
+   */
+  private static Element getCellParent(Element nodeElem) {
+    return nodeElem.getChild(1).cast();
+  }
+
+  /**
+   * Show or hide an element.
+   * 
+   * @param element the element to show or hide
+   * @param show true to show, false to hide
+   */
+  private static void showOrHide(Element element, boolean show) {
+    if (show) {
+      element.getStyle().clearDisplay();
+    } else {
+      element.getStyle().setDisplay(Display.NONE);
+    }
+  }
+
+  /**
+   * The {@link ListView} used to show children.
+   * 
+   * @param <C> the child item type
+   */
+  private static class NodeListView<C> implements ListView<C> {
+
+    private final SimpleCellListImpl<C> impl;
+    private StandardTreeNodeView<?> nodeView;
+    private Map<Object, StandardTreeNodeView<?>> savedViews;
+
+    public NodeListView(final NodeInfo<C> nodeInfo,
+        final StandardTreeNodeView<?> nodeView) {
+      this.nodeView = nodeView;
+
+      impl = new SimpleCellListImpl<C>(this, nodeInfo.getCell(), 100, 50,
+          nodeView.ensureChildContainer(), nodeView.emptyMessageElem,
+          nodeView.showMoreElem, nodeView.showFewerElem) {
+
+        @Override
+        public void setData(List<C> values, int start) {
+          // Ensure that we have a children array.
+          if (nodeView.children == null) {
+            nodeView.children = new ArrayList<StandardTreeNodeView<?>>();
+          }
+
+          // Construct a map of former child views based on their value keys.
+          int len = values.size();
+          int end = start + len;
+          int childCount = nodeView.getChildCount();
+          Map<Object, StandardTreeNodeView<?>> openNodes = new HashMap<Object, StandardTreeNodeView<?>>();
+          for (int i = start; i < end && i < childCount; i++) {
+            StandardTreeNodeView<?> child = nodeView.getChildNode(i);
+            // Ignore child nodes that are closed.
+            if (child.isOpen()) {
+              openNodes.put(child.getValueKey(), child);
+            }
+          }
+
+          // Hide the child container so we can animate it.
+          if (nodeView.tree.isAnimationEnabled()) {
+            nodeView.ensureAnimationFrame().getStyle().setDisplay(Display.NONE);
+          }
+
+          // Trim the saved views down to the children that still exists.
+          ProvidesKey<C> providesKey = nodeInfo.getProvidesKey();
+          savedViews = new HashMap<Object, StandardTreeNodeView<?>>();
+          for (C childValue : values) {
+            // Remove any child elements that correspond to prior children
+            // so the call to setInnerHtml will not destroy them
+            Object key = providesKey.getKey(childValue);
+            StandardTreeNodeView<?> savedView = openNodes.remove(key);
+            if (savedView != null) {
+              savedView.ensureAnimationFrame().removeFromParent();
+              savedViews.put(key, savedView);
+            }
+          }
+
+          // Create the new cells.
+          super.setData(values, start);
+
+          // Create the child TreeNodeViews from the new elements.
+          Element childElem = nodeView.ensureChildContainer().getFirstChildElement();
+          for (int i = start; i < end; i++) {
+            C childValue = values.get(i - start);
+            StandardTreeNodeView<C> child = nodeView.createTreeNodeView(
+                nodeInfo, childElem, childValue, null);
+            StandardTreeNodeView<?> savedChild = savedViews.remove(providesKey.getKey(childValue));
+            // Copy the saved child's state into the new child
+            if (savedChild != null) {
+              child.animationFrame = savedChild.animationFrame;
+              child.contentContainer = savedChild.contentContainer;
+              child.childContainer = savedChild.childContainer;
+              child.children = savedChild.children;
+              child.emptyMessageElem = savedChild.emptyMessageElem;
+              child.nodeInfo = savedChild.nodeInfo;
+              child.nodeInfoLoaded = savedChild.nodeInfoLoaded;
+              child.open = savedChild.open;
+              child.showFewerElem = savedChild.showFewerElem;
+              child.showMoreElem = savedChild.showMoreElem;
+
+              // Swap the node view in the child. We reuse the same NodeListView
+              // so that we don't have to unset and register a new view with the
+              // NodeInfo.
+              savedChild.listView.setNodeView(child);
+
+              // Copy the child container element to the new child
+              child.getElement().appendChild(savedChild.ensureAnimationFrame());
+            }
+
+            if (childCount > i) {
+              if (savedChild == null) {
+                // Cleanup the child node if we aren't going to reuse it.
+                nodeView.children.get(i).cleanup();
+              }
+              nodeView.children.set(i, child);
+            } else {
+              nodeView.children.add(child);
+            }
+            childElem = childElem.getNextSiblingElement();
+          }
+
+          // Clear temporary state.
+          savedViews = null;
+
+          // Animate the child container open.
+          if (nodeView.tree.isAnimationEnabled()) {
+            nodeView.tree.maybeAnimateTreeNode(nodeView);
+          }
+        }
+
+        @Override
+        protected void emitHtml(StringBuilder sb, List<C> values, int start,
+            Cell<C, Void> cell, SelectionModel<? super C> selectionModel) {
+          ProvidesKey<C> providesKey = nodeInfo.getProvidesKey();
+          TreeViewModel model = nodeView.tree.getTreeViewModel();
+          int imageWidth = nodeView.tree.getImageWidth();
+          for (C value : values) {
+            Object key = providesKey.getKey(value);
+            sb.append("<div style=\"position:relative;padding-left:");
+            sb.append(imageWidth);
+            sb.append("px;\">");
+            if (savedViews.get(key) != null) {
+              sb.append(nodeView.tree.getOpenImageHtml());
+            } else if (model.isLeaf(value)) {
+              sb.append(LEAF_IMAGE);
+            } else {
+              sb.append(nodeView.tree.getClosedImageHtml());
+            }
+            if (selectionModel != null && selectionModel.isSelected(value)) {
+              sb.append("<div class='").append(STYLENNAME_SELECTED).append("'>");
+            } else {
+              sb.append("<div>");
+            }
+            cell.render(value, null, sb);
+            sb.append("</div></div>");
+          }
+        }
+
+        @Override
+        protected void removeLastItem() {
+          StandardTreeNodeView<?> child = nodeView.children.remove(nodeView.children.size() - 1);
+          child.cleanup();
+          super.removeLastItem();
+        }
+
+        @Override
+        protected void setSelected(Element elem, boolean selected) {
+          setStyleName(getCellParent(elem), STYLENNAME_SELECTED, selected);
+        }
+      };
+    }
+
+    /**
+     * Cleanup this node view.
+     */
+    public void cleanup() {
+      impl.setSelectionModel(null);
+    }
+
+    public Range getRange() {
+      return impl.getRange();
+    }
+
+    public void setData(int start, int length, List<C> values) {
+      impl.setData(values, start);
+    }
+
+    public void setDataSize(int size, boolean isExact) {
+      impl.setDataSize(size);
+    }
+
+    public void setDelegate(Delegate<C> delegate) {
+      impl.setDelegate(delegate);
+    }
+
+    public void setSelectionModel(final SelectionModel<? super C> selectionModel) {
+      impl.setSelectionModel(selectionModel);
+    }
+
+    /**
+     * Assign this {@link ListView} to a new {@link StandardTreeNodeView}.
+     * 
+     * @param nodeView the new node view
+     */
+    private void setNodeView(StandardTreeNodeView<?> nodeView) {
+      this.nodeView.listView = null;
+      this.nodeView = nodeView;
+      nodeView.listView = this;
+    }
+  }
+
+  /**
+   * True during the time a node should be animated.
+   */
+  private boolean animate;
+
+  /**
+   * A reference to the element that is used to animate nodes. Parent of the
+   * contentContainer.
+   */
+  private Element animationFrame;
+
+  /**
+   * A reference to the element that contains the children. Parent to the actual
+   * child nodes.
+   */
+  private Element childContainer;
+
+  /**
+   * A list of child views.
+   */
+  private List<StandardTreeNodeView<?>> children;
+
+  /**
+   * A reference to the element that contains all content. Parent of the
+   * childContainer and the show/hide elements.
+   */
+  private Element contentContainer;
+
+  /**
+   * The element used when there are no children to display.
+   */
+  private Element emptyMessageElem;
+
+  /**
+   * The list view used to display the nodes.
+   */
+  private NodeListView<?> listView;
+
+  /**
+   * 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 StandardTreeNodeView}.
+   */
+  private final StandardTreeNodeView<?> parentNode;
+
+  /**
+   * The {@link NodeInfo} of the parent node.
+   */
+  private final NodeInfo<T> parentNodeInfo;
+
+  /**
+   * The element used to display less children.
+   */
+  private Element showFewerElem;
+
+  /**
+   * The element used to display more children.
+   */
+  private Element showMoreElem;
+
+  /**
+   * The {@link TreeView} that this node belongs to.
+   */
+  private final StandardTreeView tree;
+
+  /**
+   * This node's value.
+   */
+  private T value;
+
+  /**
+   * Construct a {@link StandardTreeNodeView}.
+   * 
+   * @param tree the parent {@link StandardTreeNodeView}
+   * @param parent the parent {@link StandardTreeNodeView}
    * @param parentNodeInfo the {@link NodeInfo} of the parent
-   * @param elem the outer element of this {@link TreeNodeView}
+   * @param elem the outer element of this {@link StandardTreeNodeView}
    * @param value the value of this node
    */
-  StandardTreeNodeView(final TreeView tree,
+  StandardTreeNodeView(final StandardTreeView tree,
       final StandardTreeNodeView<?> parent, NodeInfo<T> parentNodeInfo,
       Element elem, T value) {
-    super(tree, parent, parentNodeInfo, value);
+    this.tree = tree;
+    this.parentNode = parent;
+    this.parentNodeInfo = parentNodeInfo;
+    this.value = value;
     setElement(elem);
   }
 
-  @Override
-  protected <C> TreeNodeView<C> createTreeNodeView(NodeInfo<C> nodeInfo,
-      Element childElem, C childValue, Void viewData, int idx) {
-    return new StandardTreeNodeView<C>(getTree(), this, nodeInfo, childElem,
+  public int getChildCount() {
+    return children == null ? 0 : children.size();
+  }
+
+  public StandardTreeNodeView<?> getChildNode(int childIndex) {
+    return children.get(childIndex);
+  }
+
+  /**
+   * Check whether or not this node is open.
+   * 
+   * @return true if open, false if closed
+   */
+  public boolean isOpen() {
+    return open;
+  }
+
+  /**
+   * Select this node.
+   */
+  public void select() {
+    SelectionModel<? super T> selectionModel = parentNodeInfo.getSelectionModel();
+    if (selectionModel != null) {
+      selectionModel.setSelected(value, true);
+    }
+  }
+
+  /**
+   * Sets whether this item's children are displayed.
+   * 
+   * @param open whether the item is open
+   */
+  public void setOpen(boolean open) {
+    // Early out.
+    if (this.open == open) {
+      return;
+    }
+
+    this.animate = true;
+    this.open = open;
+    if (open) {
+      if (!nodeInfoLoaded) {
+        nodeInfoLoaded = true;
+        nodeInfo = tree.getTreeViewModel().getNodeInfo(value);
+      }
+
+      // If we don't have any nodeInfo, we must be a leaf node.
+      if (nodeInfo != null) {
+        // Add a loading message.
+        ensureChildContainer().setInnerHTML(tree.getLoadingHtml());
+        showOrHide(showFewerElem, false);
+        showOrHide(showMoreElem, false);
+        showOrHide(emptyMessageElem, false);
+        ensureAnimationFrame().getStyle().setProperty("display", "");
+        onOpen(nodeInfo);
+      }
+    } else {
+      cleanup();
+      tree.maybeAnimateTreeNode(this);
+    }
+
+    // Update the image.
+    updateImage();
+  }
+
+  /**
+   * Unregister the list handler and destroy all child nodes.
+   */
+  protected void cleanup() {
+    // Unregister the list handler.
+    if (listView != null) {
+      listView.cleanup();
+      nodeInfo.unsetView();
+      listView = null;
+    }
+
+    // Recursively kill children.
+    if (children != null) {
+      for (StandardTreeNodeView<?> child : children) {
+        child.cleanup();
+      }
+      children = null;
+    }
+  }
+
+  protected boolean consumeAnimate() {
+    boolean hasAnimate = animate;
+    animate = false;
+    return hasAnimate;
+  }
+
+  /**
+   * Returns an instance of TreeNodeView of the same subclass as the calling
+   * object.
+   * 
+   * @param <C> the data type of the node's children
+   * @param nodeInfo a NodeInfo object describing the child nodes
+   * @param childElem the DOM element used to parent the new TreeNodeView
+   * @param childValue the child's value
+   * @param viewData view data associated with the node
+   * @return a TreeNodeView of suitable type
+   */
+  protected <C> StandardTreeNodeView<C> createTreeNodeView(
+      NodeInfo<C> nodeInfo, Element childElem, C childValue, Void viewData) {
+    return new StandardTreeNodeView<C>(tree, this, nodeInfo, childElem,
         childValue);
   }
 
-  @Override
-  protected <C> void emitHtml(StringBuilder sb, List<C> childValues,
-      List<HasCell<C, ?, Void>> hasCells, List<TreeNodeView<?>> savedViews) {
-    TreeView tree = getTree();
-    TreeViewModel model = tree.getTreeViewModel();
-    int imageWidth = tree.getImageWidth();
-
-    SelectionModel<Object> selectionModel = tree.getSelectionModel();
-
-    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, this)) {
-        sb.append(LEAF_IMAGE);
-      } else {
-        sb.append(tree.getClosedImageHtml(0));
-      }
-      if (selectionModel != null && selectionModel.isSelected(childValue)) {
-        sb.append("<div class='gwt-stree-selectedItem'>");
-      } else {
-        sb.append("<div>");
-      }
-
-      for (int i = 0; i < hasCells.size(); i++) {
-        sb.append("<span __idx='");
-        sb.append(i);
-        sb.append("'>");
-        render(sb, childValue, hasCells.get(i));
-        sb.append("</span>");
-      }
-
-      sb.append("</div></div>");
+  /**
+   * Fire an event to the {@link com.google.gwt.bikeshed.cells.client.Cell}.
+   * 
+   * @param event the native event
+   * @return true if the cell consumes the event, false if not
+   */
+  protected boolean fireEventToCell(NativeEvent event) {
+    if (parentNodeInfo != null) {
+      Element cellParent = getCellParent();
+      Cell<T, Void> parentCell = parentNodeInfo.getCell();
+      parentCell.onBrowserEvent(cellParent, value, null, event,
+          parentNodeInfo.getValueUpdater());
+      return parentCell.consumesEvents();
     }
+    return false;
   }
 
   /**
-   * Ensure that the child container exists and return it.
-   *
-   * @return the child container
+   * Returns the element that parents the cell contents of this node.
    */
-  @Override
-  protected Element ensureChildContainer() {
-    if (getChildContainer() == 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);
-      animFrame.setId("animFrame");
-      setChildContainer(animFrame.appendChild(Document.get().createDivElement()));
-    }
-    return getChildContainer();
-  }
-
-  /**
-   * @return the element that contains the rendered cell
-   */
-  @Override
   protected Element getCellParent() {
-    return getElement().getChild(1).cast();
+    return getCellParent(getElement());
   }
 
   /**
-   * @return the image element
+   * Returns the element corresponding to the open/close image.
+   * 
+   * @return the open/close image element
    */
-  @Override
   protected Element getImageElement() {
     return getElement().getFirstChildElement();
   }
 
-  @Override
-  protected void postClose() {
-    getTree().maybeAnimateTreeNode(this);
+  /**
+   * Returns the key for the value of this node using the parent's
+   * implementation of NodeInfo.getKey().
+   */
+  protected Object getValueKey() {
+    return parentNodeInfo.getProvidesKey().getKey(value);
+  }
+
+  /**
+   * Set up the node when it is opened.
+   * 
+   * @param nodeInfo the {@link NodeInfo} that provides information about the
+   *          child values
+   * @param <C> the child data type of the node
+   */
+  protected <C> void onOpen(final NodeInfo<C> nodeInfo) {
+    NodeListView<C> view = new NodeListView<C>(nodeInfo, this);
+    listView = view;
+    view.setSelectionModel(nodeInfo.getSelectionModel());
+    nodeInfo.setView(view);
+  }
+
+  /**
+   * Update the image based on the current state.
+   */
+  protected void updateImage() {
+    // Early out if this is a root node.
+    if (parentNode == 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();
+
+    Element oldImg = getImageElement();
+    oldImg.getParentElement().replaceChild(imageElem, oldImg);
+  }
+
+  /**
+   * Ensure that the animation frame exists and return it.
+   * 
+   * @return the animation frame
+   */
+  Element ensureAnimationFrame() {
+    if (animationFrame == null) {
+      animationFrame = Document.get().createDivElement();
+      animationFrame.getStyle().setPosition(Position.RELATIVE);
+      animationFrame.getStyle().setOverflow(Overflow.HIDDEN);
+      animationFrame.setId("animFrame");
+      getElement().appendChild(animationFrame);
+    }
+    return animationFrame;
+  }
+
+  /**
+   * Ensure that the child container exists and return it.
+   * 
+   * @return the child container
+   */
+  Element ensureChildContainer() {
+    if (childContainer == null) {
+      childContainer = Document.get().createDivElement();
+      ensureContentContainer().insertFirst(childContainer);
+    }
+    return childContainer;
+  }
+
+  /**
+   * Ensure that the content container exists and return it.
+   * 
+   * @return the content container
+   */
+  Element ensureContentContainer() {
+    if (contentContainer == null) {
+      contentContainer = Document.get().createDivElement();
+      ensureAnimationFrame().appendChild(contentContainer);
+
+      emptyMessageElem = Document.get().createDivElement();
+      emptyMessageElem.setInnerHTML("<i>no data</i>");
+      showOrHide(emptyMessageElem, false);
+      contentContainer.appendChild(emptyMessageElem);
+
+      showMoreElem = Document.get().createPushButtonElement();
+      showMoreElem.setInnerText("Show more");
+      showOrHide(showMoreElem, false);
+      contentContainer.appendChild(showMoreElem);
+
+      showFewerElem = Document.get().createPushButtonElement();
+      showFewerElem.setInnerText("Show fewer");
+      showOrHide(showFewerElem, false);
+      contentContainer.appendChild(showFewerElem);
+    }
+    return contentContainer;
+  }
+
+  Element getShowFewerElement() {
+    return showFewerElem;
+  }
+
+  Element getShowMoreElement() {
+    return showMoreElem;
+  }
+
+  void showFewer() {
+    listView.impl.showFewer();
+  }
+
+  void showMore() {
+    listView.impl.showMore();
   }
 }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeView.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeView.java
index a2a78ff..1be9854 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeView.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeView.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -15,12 +15,13 @@
  */
 package com.google.gwt.bikeshed.tree.client;
 
-import com.google.gwt.dom.client.Document;
+import com.google.gwt.animation.client.Animation;
 import com.google.gwt.dom.client.Element;
 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.Event;
+import com.google.gwt.user.client.ui.AbstractImagePrototype;
 import com.google.gwt.user.client.ui.HasAnimation;
 
 import java.util.ArrayList;
@@ -31,14 +32,49 @@
 public class StandardTreeView extends TreeView implements HasAnimation {
 
   /**
-   * A {@link TreeView.TreeNodeViewAnimation} that reveals the contents of child
-   * nodes.
+   * A node animation.
    */
-  public static class RevealAnimation extends TreeNodeViewAnimation {
+  public abstract static class NodeAnimation extends Animation {
+
+    /**
+     * The default animation delay in milliseconds.
+     */
+    private static final int DEFAULT_ANIMATION_DURATION = 450;
+
+    /**
+     * The duration of the animation.
+     */
+    private int duration = DEFAULT_ANIMATION_DURATION;
+
+    NodeAnimation() {
+    }
+
+    /**
+     * Animate a tree node into its new state.
+     * 
+     * @param node the node to animate
+     * @param isAnimationEnabled true to animate
+     */
+    abstract void animate(StandardTreeNodeView<?> node,
+        boolean isAnimationEnabled);
+
+    public int getDuration() {
+      return duration;
+    }
+
+    public void setDuration(int duration) {
+      this.duration = duration;
+    }
+  }
+
+  /**
+   * A {@link NodeAnimation} that reveals the contents of child nodes.
+   */
+  public static class RevealAnimation extends NodeAnimation {
 
     /**
      * Create a new {@link RevealAnimation}.
-     *
+     * 
      * @return the new animation
      */
     public static RevealAnimation create() {
@@ -46,44 +82,52 @@
     }
 
     /**
-     * The container that holds the child container.
+     * The container that holds the content, includind the children.
      */
-    Element animFrame;
-    /**
-     * The container that holds the children.
-     */
-    Element childContainer;
+    Element contentContainer;
 
     /**
      * The target height when opening, the start height when closing.
      */
     int height;
+
     /**
      * True if the node is opening, false if closing.
      */
     boolean opening;
 
     /**
+     * The container that holds the child container.
+     */
+    private Element animFrame;
+
+    /**
+     * The container that holds the children.
+     */
+    private Element childContainer;
+
+    /**
      * Not instantiable.
      */
     private RevealAnimation() {
     }
 
     /**
-     * Animate a {@link TreeNodeView} into its new state.
-     *
-     * @param node the {@link TreeNodeView} to animate
+     * Animate a {@link StandardTreeNodeView} into its new state.
+     * 
+     * @param node the {@link StandardTreeNodeView} to animate
      * @param isAnimationEnabled true to animate
      */
     @Override
-    public void animate(TreeNodeView<?> node, boolean isAnimationEnabled) {
+    void animate(StandardTreeNodeView<?> node, boolean isAnimationEnabled) {
       // Cancel any pending animations.
       cancel();
 
       // Initialize the fields.
-      this.opening = node.getState();
-      childContainer = node.getChildContainer();
-      animFrame = childContainer.getParentElement();
+      this.opening = node.isOpen();
+      animFrame = node.ensureAnimationFrame();
+      contentContainer = node.ensureContentContainer();
+      childContainer = node.ensureChildContainer();
 
       if (isAnimationEnabled) {
         // Animated.
@@ -111,9 +155,9 @@
       if (opening) {
         animFrame.getStyle().setHeight(1.0, Unit.PX);
         animFrame.getStyle().clearDisplay();
-        height = childContainer.getScrollHeight();
+        height = contentContainer.getScrollHeight();
       } else {
-        height = childContainer.getOffsetHeight();
+        height = contentContainer.getOffsetHeight();
       }
     }
 
@@ -139,18 +183,19 @@
         childContainer.setInnerHTML("");
       }
       animFrame.getStyle().clearHeight();
+      this.contentContainer = null;
       this.childContainer = null;
       this.animFrame = null;
     }
   }
 
   /**
-   * A {@link TreeView.TreeNodeViewAnimation} that slides children into view.
+   * A {@link NodeAnimation} that slides children into view.
    */
   public static class SlideAnimation extends RevealAnimation {
     /**
      * Create a new {@link RevealAnimation}.
-     *
+     * 
      * @return the new animation
      */
     public static SlideAnimation create() {
@@ -165,9 +210,9 @@
 
     @Override
     protected void onComplete() {
-      childContainer.getStyle().clearPosition();
-      childContainer.getStyle().clearTop();
-      childContainer.getStyle().clearWidth();
+      contentContainer.getStyle().clearPosition();
+      contentContainer.getStyle().clearTop();
+      contentContainer.getStyle().clearWidth();
       super.onComplete();
     }
 
@@ -175,11 +220,11 @@
     protected void onStart() {
       super.onStart();
       if (opening) {
-        childContainer.getStyle().setTop(-height, Unit.PX);
+        contentContainer.getStyle().setTop(-height, Unit.PX);
       } else {
-        childContainer.getStyle().setTop(0, Unit.PX);
+        contentContainer.getStyle().setTop(0, Unit.PX);
       }
-      childContainer.getStyle().setPosition(Position.RELATIVE);
+      contentContainer.getStyle().setPosition(Position.RELATIVE);
     }
 
     @Override
@@ -187,38 +232,92 @@
       super.onUpdate(progress);
       if (opening) {
         double curTop = (1.0 - progress) * -height;
-        childContainer.getStyle().setTop(curTop, Unit.PX);
+        contentContainer.getStyle().setTop(curTop, Unit.PX);
       } else {
         double curTop = progress * -height;
-        childContainer.getStyle().setTop(curTop, Unit.PX);
+        contentContainer.getStyle().setTop(curTop, Unit.PX);
       }
     }
   }
 
   /**
+   * The animation.
+   */
+  private NodeAnimation animation;
+
+  /**
+   * The HTML used to generate the closed image.
+   */
+  private String closedImageHtml;
+
+  /**
+   * Indicates whether or not animations are enabled.
+   */
+  private boolean isAnimationEnabled;
+
+  /**
+   * The message displayed while child nodes are loading.
+   */
+  // TODO(jlabanca): I18N loading HTML, or remove the text.
+  private String loadingHtml = "Loading...";
+
+  /**
+   * The HTML used to generate the open image.
+   */
+  private String openImageHtml;
+
+  /**
+   * The hidden root node in the tree.
+   */
+  private StandardTreeNodeView<?> 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> StandardTreeView(TreeViewModel viewModel, T rootValue) {
     super(viewModel);
-    setElement(Document.get().createDivElement());
-    setStyleName("gwt-TreeView");
+    setStyleName("gwt-StandardTreeView");
 
     // We use one animation for the entire tree.
     setAnimation(SlideAnimation.create());
 
     // Add event handlers.
-    sinkEvents(Event.ONCLICK | Event.ONMOUSEDOWN | Event.ONMOUSEUP
-        | Event.ONCHANGE);
+    sinkEvents(Event.ONCLICK | Event.ONCHANGE | Event.MOUSEEVENTS);
 
     // Associate a view with the item.
-    TreeNodeView<T> root = new StandardTreeNodeView<T>(this, null, null,
-        getElement(), rootValue);
-    setRootNode(root);
-    root.setState(true);
+    StandardTreeNodeView<T> root = new StandardTreeNodeView<T>(this, null,
+        null, getElement(), rootValue);
+    rootNode = root;
+    root.setOpen(true);
+  }
+
+  /**
+   * Get the animation used to open and close nodes in this tree if animations
+   * are enabled.
+   * 
+   * @return the animation
+   * @see #isAnimationEnabled()
+   */
+  public NodeAnimation 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 boolean isAnimationEnabled() {
+    return isAnimationEnabled;
   }
 
   @Override
@@ -226,57 +325,126 @@
     super.onBrowserEvent(event);
 
     Element target = event.getEventTarget().cast();
-    TreeNodeView<?> rootNode = getRootTreeNodeView();
 
     ArrayList<Element> chain = new ArrayList<Element>();
-    ArrayList<String> ids = new ArrayList<String>();
-    collectElementChain(chain, ids, getElement(), target);
+    collectElementChain(chain, getElement(), target);
 
-    TreeNodeView<?> nodeView = findItemByChain(chain, 0, rootNode);
+    StandardTreeNodeView<?> nodeView = findItemByChain(chain, 0, rootNode);
     if (nodeView != null && nodeView != rootNode) {
-      if (nodeView.getImageElement().isOrHasChild(target)) {
-        if ("click".equals(event.getType())) {
-          nodeView.setState(!nodeView.getState());
+      if ("click".equals(event.getType())) {
+        // Open the node when the open image is clicked.
+        Element showFewerElem = nodeView.getShowFewerElement();
+        Element showMoreElem = nodeView.getShowMoreElement();
+        if (nodeView.getImageElement().isOrHasChild(target)) {
+          nodeView.setOpen(!nodeView.isOpen());
+          return;
+        } else if (showFewerElem != null && showFewerElem.isOrHasChild(target)) {
+          nodeView.showFewer();
+          return;
+        } else if (showMoreElem != null && showMoreElem.isOrHasChild(target)) {
+          nodeView.showMore();
+          return;
         }
-      } else if (nodeView.getCellParent().isOrHasChild(target)) {
-        nodeView.fireEventToCell(event);
+      }
 
-        // TODO(jgw): Kind of a hacky way to set selection. Need to generalize
-        // this to some sort of keyboard/mouse->selection controller.
-        if (getSelectionModel() != null) {
-          if ("click".equals(event.getType())) {
-            getSelectionModel().setSelected(nodeView.getValue(), true);
-          }
+      // Forward the event to the cell.
+      if (nodeView.getCellParent().isOrHasChild(target)) {
+        boolean consumesEvent = nodeView.fireEventToCell(event);
+        if (!consumesEvent && "click".equals(event.getType())) {
+          nodeView.select();
         }
       }
     }
   }
 
   /**
+   * 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 a {@link NodeAnimation}
+   * @see #setAnimationEnabled(boolean)
+   */
+  public void setAnimation(NodeAnimation 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() {
+    if (closedImageHtml == null) {
+      AbstractImagePrototype proto = AbstractImagePrototype.create(getResources().treeClosed());
+      closedImageHtml = proto.getHTML().replace("style='",
+          "style='position:absolute;left:0px;top:0px;");
+    }
+    return closedImageHtml;
+  }
+
+  /**
+   * @return the HTML to render the open image.
+   */
+  String getOpenImageHtml() {
+    if (openImageHtml == null) {
+      AbstractImagePrototype proto = AbstractImagePrototype.create(getResources().treeOpen());
+      openImageHtml = proto.getHTML().replace("style='",
+          "style='position:absolute;left:0px;top:0px;");
+    }
+    return openImageHtml;
+  }
+
+  /**
+   * Animate the current state of a {@link StandardTreeNodeView} in this tree.
+   * 
+   * @param node the node to animate
+   */
+  void maybeAnimateTreeNode(StandardTreeNodeView<?> node) {
+    if (animation != null) {
+      animation.animate(node, node.consumeAnimate() && isAnimationEnabled());
+    }
+  }
+
+  /**
    * 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) {
+  private void collectElementChain(ArrayList<Element> chain, Element hRoot,
+      Element hElem) {
     if ((hElem == null) || (hElem == hRoot)) {
       return;
     }
 
-    collectElementChain(chain, ids, hRoot, hElem.getParentElement());
+    collectElementChain(chain, hRoot, hElem.getParentElement());
     chain.add(hElem);
-    ids.add(hElem.getId());
   }
 
-  private TreeNodeView<?> findItemByChain(ArrayList<Element> chain, int idx,
-      TreeNodeView<?> parent) {
+  private StandardTreeNodeView<?> findItemByChain(ArrayList<Element> chain,
+      int idx, StandardTreeNodeView<?> 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);
+      StandardTreeNodeView<?> child = parent.getChildNode(i);
       if (child.getElement() == hCurElem) {
-        TreeNodeView<?> retItem = findItemByChain(chain, idx + 1, child);
+        StandardTreeNodeView<?> retItem = findItemByChain(chain, idx + 1, child);
         if (retItem == null) {
           return child;
         }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNode.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNode.java
deleted file mode 100644
index 690b556..0000000
--- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNode.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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();
-
-  boolean isOpen();
-}
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNodeView.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNodeView.java
deleted file mode 100644
index 0def3f8..0000000
--- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNodeView.java
+++ /dev/null
@@ -1,566 +0,0 @@
-/*
- * 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.list.client.HasCell;
-import com.google.gwt.bikeshed.list.client.ListView;
-import com.google.gwt.bikeshed.list.shared.ProvidesKey;
-import com.google.gwt.bikeshed.list.shared.Range;
-import com.google.gwt.bikeshed.list.shared.AbstractListViewAdapter.DefaultRange;
-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.NativeEvent;
-import com.google.gwt.dom.client.Style.Display;
-import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.ui.UIObject;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * A view of a tree node.
- *
- * @param <T> the type that this {@link TreeNodeView} contains
- */
-public abstract class TreeNodeView<T> extends UIObject implements TreeNode<T> {
-
-  /**
-   * A NodeInfo and list of data items of matching type.
-   *
-   * @param <C> the data type
-   */
-  static class SavedInfo<C> {
-    NodeInfo<C> nodeInfo;
-    List<C> values;
-  }
-
-  /**
-   * The element used in place of an image when a node has no children.
-   */
-  public static final String LEAF_IMAGE = "<div style='position:absolute;display:none;'></div>";
-
-  /**
-   * True during the time a node should be animated.
-   */
-  private boolean animate;
-
-  /**
-   * A reference to the element that contains the children.
-   */
-  private Element childContainer;
-
-  /**
-   * A reference to the element that contains the children.
-   */
-  private List<TreeNodeView<?>> children;
-
-  /**
-   * The list registration for the list of children.
-   */
-  private ListView<?> listView;
-
-  /**
-   * 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 SideBySideTreeNodeView}.
-   */
-  private final TreeNodeView<?> parentNode;
-
-  /**
-   * The parent {@link SideBySideTreeNodeView}.
-   */
-  private final NodeInfo<T> parentNodeInfo;
-
-  /**
-   * The NodeInfo for this node along with saved child data values.
-   */
-  private SavedInfo<?> savedInfo;
-
-  /**
-   * The info about this node.
-   */
-  private final TreeView tree;
-
-  /**
-   * This node's value.
-   */
-  private T value;
-
-  public TreeNodeView(TreeView tree, TreeNodeView<?> parent, NodeInfo<T> parentNodeInfo, T value) {
-    this.tree = tree;
-    this.parentNode = parent;
-    this.parentNodeInfo = parentNodeInfo;
-    this.value = value;
-  }
-
-  public int getChildCount() {
-    return children == null ? 0 : children.size();
-  }
-
-  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 parentNode;
-  }
-
-  /**
-   * 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
-   */
-  public T getValue() {
-    return value;
-  }
-
-  /**
-   * Returns true if the node is open.
-   */
-  public boolean isOpen() {
-    return open;
-  }
-
-  /**
-   * 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
-   */
-  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.animate = true;
-    this.open = open;
-    if (open) {
-      if (!nodeInfoLoaded) {
-        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();
-      postClose();
-    }
-
-    // Update the image.
-    updateImage();
-  }
-
-  /**
-   * Unregister the list handler and destroy all child nodes.
-   */
-  protected void cleanup() {
-    // Unregister the list handler.
-    if (listView != null) {
-      nodeInfo.unsetView();
-      listView = null;
-    }
-
-    // Recursively kill children.
-    if (children != null) {
-      for (TreeNodeView<?> child : children) {
-        child.cleanup();
-      }
-      children = null;
-    }
-  }
-
-  protected boolean consumeAnimate() {
-    boolean hasAnimate = animate;
-    animate = false;
-    return hasAnimate;
-  }
-
-  /**
-   * Returns an instance of TreeNodeView of the same subclass as the
-   * calling object.
-   *
-   * @param <C> the data type of the node's children
-   * @param nodeInfo a NodeInfo object describing the child nodes
-   * @param childElem the DOM element used to parent the new TreeNodeView
-   * @param childValue the child's value
-   * @param viewData view data associated with the node
-   * @param idx the index of the child within its parent node
-   * @return a TreeNodeView of suitable type
-   */
-  protected abstract <C> TreeNodeView<C> createTreeNodeView(NodeInfo<C> nodeInfo,
-      Element childElem, C childValue, Void viewData, int idx);
-
-  /**
-   * Write the HTML for a list of child values into the given StringBuilder.
-   *
-   * @param <C> the data type of the child nodes
-   * @param sb a StringBuilder to write to
-   * @param childValues a List of child node values
-   * @param hasCells the cells and values to use for rendering each child value
-   * @param savedViews a List of TreeNodeView instances corresponding to
-   *   the child values; a non-null value indicates a TreeNodeView previously
-   *   associated with a given child value
-   */
-  protected abstract <C> void emitHtml(StringBuilder sb, List<C> childValues,
-      List<HasCell<C, ?, Void>> hasCells, List<TreeNodeView<?>> savedViews);
-
-  /**
-   * Ensure that the animation frame exists and return it.
-   *
-   * @return the animation frame
-   */
-  protected Element ensureAnimationFrame() {
-    return ensureChildContainer().getParentElement();
-  }
-
-  /**
-   * Ensure that the child container exists and return it.
-   *
-   * @return the child container
-   */
-  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;
-  }
-
-  /**
-   * Returns the element that contains this node.
-   * The default implementation returns the value of getElement().
-   */
-  protected Element getContainer() {
-    return getElement();
-  }
-
-  /**
-   * Returns the element corresponding to the open/close image.
-   */
-  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;
-  }
-
-  /**
-   * Returns the key for the value of this node using the parent's
-   * implementation of NodeInfo.getKey().
-   */
-  protected Object getValueKey() {
-    return getParentNodeInfo().getProvidesKey().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
-   */
-  protected <C> void onOpen(final NodeInfo<C> nodeInfo) {
-    // Add a loading message.
-    ensureChildContainer().setInnerHTML(tree.getLoadingHtml());
-    ensureAnimationFrame().getStyle().setProperty("display", "");
-
-    // Create a SavedInfo object to store the NodeInfo and data values
-    final SavedInfo<C> localSavedInfo = new SavedInfo<C>();
-    localSavedInfo.nodeInfo = nodeInfo;
-    localSavedInfo.values = new ArrayList<C>();
-    TreeNodeView.this.savedInfo = localSavedInfo;
-
-    // Get the node info.
-    final ProvidesKey<C> providesKey = nodeInfo.getProvidesKey();
-    ListView<C> view = new ListView<C>() {
-      public Range getRange() {
-        return new DefaultRange(0, 100);
-      }
-
-      public void setData(int start, int length, List<C> values) {
-        // TODO - handle event start and length properly
-        int end = start + length;
-
-        // Ensure savedInfo has a place to store the data values
-        while (localSavedInfo.values.size() < end) {
-          savedInfo.values.add(null);
-        }
-        // Save child values into savedInfo
-        int index = start;
-        for (C childValue : values) {
-          localSavedInfo.values.set(index++, childValue);
-        }
-
-        // Construct a map of former child views based on their value keys.
-        Map<Object, TreeNodeView<?>> map = new HashMap<Object, TreeNodeView<?>>();
-        if (children != null) {
-          for (TreeNodeView<?> child : children) {
-            // Ignore child nodes that are closed
-            if (child.getState()) {
-              map.put(child.getValueKey(), child);
-            }
-          }
-        }
-
-        // Hide the child container so we can animate it.
-        if (tree.isAnimationEnabled()) {
-          ensureAnimationFrame().getStyle().setDisplay(Display.NONE);
-        }
-
-        List<TreeNodeView<?>> savedViews = new ArrayList<TreeNodeView<?>>();
-        for (C childValue : values) {
-          // Remove any child elements that correspond to prior children
-          // so the call to setInnerHtml will not destroy them
-          TreeNodeView<?> savedView = map.get(providesKey.getKey(childValue));
-          if (savedView != null) {
-            savedView.getContainer().getFirstChild().removeFromParent();
-          }
-          savedViews.add(savedView);
-        }
-
-        // Construct the child contents.
-        StringBuilder sb = new StringBuilder();
-        emitHtml(sb, values, nodeInfo.getHasCells(), 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 : values) {
-          TreeNodeView<C> child = createTreeNodeView(nodeInfo, childElem,
-              childValue, null, idx);
-          TreeNodeView<?> savedChild = map.get(providesKey.getKey(childValue));
-          // Copy the saved child's state into the new child
-          if (savedChild != null) {
-            child.children = savedChild.children;
-            child.childContainer = savedChild.childContainer;
-            child.nodeInfo = savedChild.nodeInfo;
-            child.nodeInfoLoaded = savedChild.nodeInfoLoaded;
-            child.open = savedChild.getState();
-
-            // Copy the child container element to the new child
-            // TODO(rice) clean up this expression
-            child.getContainer().appendChild(savedChild.childContainer.getParentElement());
-          }
-
-          children.add(child);
-          childElem = childElem.getNextSiblingElement();
-
-          idx++;
-        }
-
-        // Animate the child container open.
-        if (tree.isAnimationEnabled()) {
-          tree.maybeAnimateTreeNode(TreeNodeView.this);
-        }
-      }
-
-      public void setDataSize(int size, boolean isExact) {
-        if (size == 0 && isExact) {
-          ensureChildContainer().setInnerHTML("<i>no data</i>");
-          if (children != null) {
-            for (TreeNodeView<?> child : children) {
-              child.cleanup();
-            }
-            children = null;
-          }
-        }
-      }
-
-      public void setDelegate(ListView.Delegate<C> delegate) {
-        // Range never actually changes so no need to store the delegate
-      }
-    };
-    nodeInfo.setView(view);
-    this.listView = view;
-  }
-
-  /**
-   * 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() {
-  }
-
-  /**
-   * Refresh any cells that depend on the selection state. The default
-   * implementation works for {@link StandardTreeNodeView} and
-   * {@link SideBySideTreeNodeView}; other subclasses will need to implement
-   * their own versions of this method.
-   */
-  protected void refreshSelection() {
-    refreshSelection(savedInfo);
-  }
-
-  protected void setChildContainer(Element childContainer) {
-    this.childContainer = childContainer;
-  }
-
-  /**
-   * Update the image based on the current state.
-   */
-  protected void updateImage() {
-    // Early out if this is a root node.
-    if (isRootNode()) {
-      return;
-    }
-
-    // Replace the image element with a new one.
-    int imageLeft = getImageLeft();
-    String html = open ? tree.getOpenImageHtml(imageLeft) : tree.getClosedImageHtml(imageLeft);
-    if (nodeInfoLoaded && nodeInfo == null) {
-      html = LEAF_IMAGE;
-    }
-    Element tmp = Document.get().createDivElement();
-    tmp.setInnerHTML(html);
-    Element imageElem = tmp.getFirstChildElement();
-    getElement().replaceChild(imageElem, getImageElement());
-  }
-
-  TreeView getTree() {
-    return tree;
-  }
-
-  <C, X> void render(StringBuilder sb, C childValue,
-      HasCell<C, X, Void> hc) {
-    hc.getCell().render(hc.getValue(childValue), null, sb);
-  }
-
-  /**
-   * Refresh any cells that depend on the selection state.  Note that this
-   * implementation is dependent on the particular DOM structure used by
-   * {@link StandardTreeNodeView} and {@link SideBySideTreeNodeView}.
-   *
-   * @param <C> the child data type
-   * @param savedInfo a SavedInfo object containing a NodeInfo instance
-   * and a list of saved child data values
-   */
-  private <C> void refreshSelection(SavedInfo<C> savedInfo) {
-    List<HasCell<C, ?, Void>> hasCells = savedInfo.nodeInfo.getHasCells();
-    Element outerDiv = childContainer.getFirstChildElement();
-    int index = 0;
-    while (outerDiv != null) {
-      C childValue = savedInfo.values.get(index);
-      Element span = outerDiv.getFirstChildElement().getNextSiblingElement().getFirstChildElement();
-
-      for (HasCell<C, ?, Void> hasCell : hasCells) {
-        if (hasCell.dependsOnSelection()) {
-          StringBuilder sb = new StringBuilder();
-          render(sb, childValue, hasCell);
-          span.setInnerHTML(sb.toString());
-        }
-        span = span.getNextSiblingElement();
-      }
-      outerDiv = outerDiv.getNextSibling().cast();
-      index++;
-    }
-  }
-}
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 55d9f16..089c4a6 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeView.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeView.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -15,58 +15,22 @@
  */
 package com.google.gwt.bikeshed.tree.client;
 
-import com.google.gwt.animation.client.Animation;
-import com.google.gwt.bikeshed.list.shared.SelectionModel;
-import com.google.gwt.bikeshed.list.shared.SelectionModel.SelectionChangeEvent;
-import com.google.gwt.bikeshed.list.shared.SelectionModel.SelectionChangeHandler;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.resources.client.ClientBundle;
 import com.google.gwt.resources.client.ImageResource;
-import com.google.gwt.user.client.ui.AbstractImagePrototype;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.SimplePanel;
 import com.google.gwt.user.client.ui.Widget;
 
 /**
  * A view of a tree.
  */
-public abstract class TreeView extends Widget {
-
-  /**
-   * An Animation of a {@link TreeNodeView}.
-   */
-  public abstract static class TreeNodeViewAnimation extends Animation {
-
-    /**
-     * The default animation delay in milliseconds.
-     */
-    private static final int DEFAULT_ANIMATION_DURATION = 450;
-
-    /**
-     * The duration of the animation.
-     */
-    private int duration = DEFAULT_ANIMATION_DURATION;
-
-    /**
-     * Animate a {@link TreeNodeView} into its new state.
-     *
-     * @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;
-    }
-
-    public void setDuration(int duration) {
-      this.duration = duration;
-    }
-  }
+public abstract class TreeView extends Composite {
 
   /**
    * A ClientBundle that provides images for this widget.
    */
-  interface Resources extends ClientBundle {
+  public static interface Resources extends ClientBundle {
 
     /**
      * An image indicating a closed branch.
@@ -79,49 +43,12 @@
     ImageResource treeOpen();
   }
 
-  private class TreeSelectionHandler implements SelectionChangeHandler {
-    public void onSelectionChange(SelectionChangeEvent event) {
-      refreshSelection();
-    }
-  }
-
   private static final Resources DEFAULT_RESOURCES = GWT.create(Resources.class);
 
   /**
-   * The animation.
+   * The {@link Resources} used in the tree.
    */
-  private TreeNodeViewAnimation animation;
-
-  /**
-   * The HTML used to generate the closed image.
-   */
-  private String closedImageHtml;
-
-  /**
-   * Indicates whether or not animations are enabled.
-   */
-  private boolean isAnimationEnabled;
-
-  /**
-   * The message displayed while child nodes are loading.
-   */
-  // TODO(jlabanca): I18N this, or remove the text
-  private String loadingHtml = "Loading...";
-
-  /**
-   * The HTML used to generate the open image.
-   */
-  private String openImageHtml;
-
-  /**
-   * The hidden root node in the tree.
-   */
-  private TreeNodeView<?> rootNode;
-
-  /**
-   * The {@link SelectionModel} for the tree.
-   */
-  private SelectionModel<Object> selectionModel;
+  private Resources resources = DEFAULT_RESOURCES;
 
   /**
    * The {@link TreeViewModel} that backs the tree.
@@ -130,96 +57,26 @@
 
   /**
    * Construct a new {@link TreeView}.
-   *
+   * 
    * @param viewModel the {@link TreeViewModel} that backs the tree
    */
+  // TODO(jlabanca): Should we nuke this class?
   public TreeView(TreeViewModel viewModel) {
+    this(viewModel, new SimplePanel());
+  }
+
+  protected TreeView(TreeViewModel viewModel, Widget widget) {
     this.viewModel = viewModel;
-  }
-
-  /**
-   * Get the animation used to open and close nodes in this tree if animations
-   * are enabled.
-   *
-   * @return the animation
-   * @see #isAnimationEnabled()
-   */
-  public TreeNodeViewAnimation getAnimation() {
-    return animation;
-  }
-
-  public TreeNode<?> getRootNode() {
-    return rootNode;
-  }
-
-  /**
-   * Returns the {@link SelectionModel} containing the selection state for
-   * this tree, or null if none is present.
-   */
-  public SelectionModel<Object> getSelectionModel() {
-    return selectionModel;
+    initWidget(widget);
   }
 
   public TreeViewModel getTreeViewModel() {
     return viewModel;
   }
 
-  public boolean isAnimationEnabled() {
-    return isAnimationEnabled;
-  }
-
-  /**
-   * Refresh any visible cells of this tree that depend on the selection state.
-   */
-  public void refreshSelection() {
-    refreshSelection(rootNode);
-  }
-
-  /**
-   * 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 a {@link TreeNodeViewAnimation}
-   * @see #setAnimationEnabled(boolean)
-   */
-  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();
-    }
-  }
-
-  public void setSelectionModel(SelectionModel<Object> selectionModel) {
-    this.selectionModel = selectionModel;
-    // Attach a selection handler.
-    if (selectionModel != null) {
-      selectionModel.addSelectionChangeHandler(new TreeSelectionHandler());
-    }
-  }
-
-  /**
-   * @return the HTML to render the closed image.
-   */
-  protected String getClosedImageHtml(int left) {
-    if (closedImageHtml == null) {
-      AbstractImagePrototype proto =
-        AbstractImagePrototype.create(DEFAULT_RESOURCES.treeClosed());
-      // CHECKSTYLE_OFF
-      closedImageHtml = proto.getHTML().replace("style='",
-          "style='position:absolute;left:" + left + "px;top:0px;");
-      // CHECKSTYLE_ON
-    }
-    return closedImageHtml;
-  }
-
   /**
    * Get the width required for the images.
-   *
+   * 
    * @return the maximum width required for images.
    */
   protected int getImageWidth() {
@@ -228,67 +85,11 @@
   }
 
   /**
-   * Get the HTML string that is displayed while nodes wait for their children
-   * to load.
-   *
-   * @return the loading HTML string
+   * Get the {@link Resources} used in the tree.
+   * 
+   * @return the resources
    */
-  protected String getLoadingHtml() {
-    return loadingHtml;
-  }
-
-  /**
-   * @return the HTML to render the open image.
-   */
-  protected String getOpenImageHtml(int left) {
-    if (openImageHtml == null) {
-      AbstractImagePrototype proto =
-        AbstractImagePrototype.create(DEFAULT_RESOURCES.treeOpen());
-      // CHECKSTYLE_OFF
-      openImageHtml = proto.getHTML().replace("style='",
-          "style='position:absolute;left:" + left + "px;top:0px;");
-      // CHECKSTYLE_ON
-    }
-    return openImageHtml;
-  }
-
-  protected TreeNodeView<?> getRootTreeNodeView() {
-    return rootNode;
-  }
-
-  /**
-   * Animate the current state of a {@link TreeNodeView} in this tree.
-   *
-   * @param node the node to animate
-   */
-  protected void maybeAnimateTreeNode(TreeNodeView<?> node) {
-    if (animation != null) {
-      animation.animate(node, node.consumeAnimate() && isAnimationEnabled());
-    }
-  }
-
-  /**
-   * Set the HTML string that will be displayed when a node is waiting for its
-   * child nodes to load.
-   *
-   * @param loadingHtml the HTML string
-   */
-  protected void setLoadingHtml(String loadingHtml) {
-    this.loadingHtml = loadingHtml;
-  }
-
-  protected void setRootNode(TreeNodeView<?> rootNode) {
-    this.rootNode = rootNode;
-  }
-
-  private void refreshSelection(TreeNodeView<?> node) {
-    node.refreshSelection();
-    int count = node.getChildCount();
-    for (int i = 0; i < count; i++) {
-      TreeNodeView<?> child = node.getChildTreeNodeView(i);
-      if (child.isOpen()) {
-        refreshSelection(child);
-      }
-    }
+  protected Resources getResources() {
+    return resources;
   }
 }
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 332b6dc..85df9db 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeViewModel.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeViewModel.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -16,17 +16,12 @@
 package com.google.gwt.bikeshed.tree.client;
 
 import com.google.gwt.bikeshed.cells.client.Cell;
-import com.google.gwt.bikeshed.cells.client.FieldUpdater;
 import com.google.gwt.bikeshed.cells.client.ValueUpdater;
-import com.google.gwt.bikeshed.list.client.HasCell;
 import com.google.gwt.bikeshed.list.client.ListView;
 import com.google.gwt.bikeshed.list.shared.AbstractListViewAdapter;
 import com.google.gwt.bikeshed.list.shared.ProvidesKey;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.dom.client.NativeEvent;
-
-import java.util.ArrayList;
-import java.util.List;
+import com.google.gwt.bikeshed.list.shared.SelectionModel;
+import com.google.gwt.bikeshed.list.shared.SingleSelectionModel;
 
 /**
  * A model of a tree.
@@ -37,87 +32,57 @@
    * Default implementation of {@link NodeInfo}.
    */
   class DefaultNodeInfo<T> implements NodeInfo<T> {
-    private List<HasCell<T, ?, Void>> hasCells = new ArrayList<HasCell<T, ?, Void>>();
+
+    private Cell<T, Void> cell;
     private AbstractListViewAdapter<T> listViewAdapter;
+    private SelectionModel<? super T> selectionModel;
+    private ValueUpdater<T, Void> valueUpdater;
     private ListView<T> view;
 
     /**
      * Construct a new {@link DefaultNodeInfo}.
-     *
+     * 
      * @param adapter the {@link AbstractListViewAdapter} that provides the
      *          child values
      * @param cell the {@link Cell} used to render the child values
-     * @param dependsOnSelection true if the contents of the cell may require
-     *          an update when the selection changes
      */
     public DefaultNodeInfo(AbstractListViewAdapter<T> adapter,
-        Cell<T, Void> cell, boolean dependsOnSelection) {
-      this(adapter, cell, dependsOnSelection, null);
+        Cell<T, Void> cell) {
+      this(adapter, cell, new SingleSelectionModel<T>(), null);
     }
 
     /**
-     * Construct a new {@link DefaultNodeInfo} with a single cell and a
-     * {@link ValueUpdater}.
-     *
+     * Construct a new {@link DefaultNodeInfo}.
+     * 
      * @param adapter the {@link AbstractListViewAdapter} that provides the
      *          child values
-     * @param cell the {@link Cell} used to render the child values
-     * @param dependsOnSelection true if the contents of the cell may require
-     *          an update when the selection changes
+     * @param cell the {@link Cell} used to render the child values update when
+     *          the selection changes
      * @param valueUpdater the {@link ValueUpdater}
      */
     public DefaultNodeInfo(AbstractListViewAdapter<T> adapter,
-        final Cell<T, Void> cell, final boolean dependsOnSelection,
+        final Cell<T, Void> cell, SelectionModel<? super T> selectionModel,
         final ValueUpdater<T, Void> valueUpdater) {
-      hasCells.add(new HasCell<T, T, Void>() {
-        public boolean dependsOnSelection() {
-          return dependsOnSelection;
-        }
-
-        public Cell<T, Void> getCell() {
-          return cell;
-        }
-
-        public FieldUpdater<T, T, Void> getFieldUpdater() {
-          return valueUpdater == null ? null : new FieldUpdater<T, T, Void>() {
-            public void update(int index, T object, T value, Void viewData) {
-              valueUpdater.update(value, viewData);
-            }
-          };
-        }
-
-        public T getValue(T object) {
-          return object;
-        }
-      });
       this.listViewAdapter = adapter;
+      this.cell = cell;
+      this.selectionModel = selectionModel;
+      this.valueUpdater = valueUpdater;
     }
 
-    public DefaultNodeInfo(AbstractListViewAdapter<T> adapter,
-        List<HasCell<T, ?, Void>> hasCells) {
-      this.hasCells.addAll(hasCells);
-      this.listViewAdapter = adapter;
-    }
-
-    public List<HasCell<T, ?, Void>> getHasCells() {
-      return hasCells;
+    public Cell<T, Void> getCell() {
+      return cell;
     }
 
     public ProvidesKey<T> getProvidesKey() {
       return listViewAdapter;
     }
 
-    public void onBrowserEvent(Element elem, final T object, NativeEvent event) {
-      Element target = event.getEventTarget().cast();
-      String idxString = "";
-      while ((target != null)
-          && ((idxString = target.getAttribute("__idx")).length() == 0)) {
-        target = target.getParentElement();
-      }
-      if (idxString.length() > 0) {
-        int idx = Integer.parseInt(idxString);
-        dispatch(target, object, event, hasCells.get(idx));
-      }
+    public SelectionModel<? super T> getSelectionModel() {
+      return selectionModel;
+    }
+
+    public ValueUpdater<T, Void> getValueUpdater() {
+      return valueUpdater;
     }
 
     public void setView(ListView<T> view) {
@@ -128,39 +93,56 @@
     public void unsetView() {
       listViewAdapter.removeView(view);
     }
-
-    private <X> void dispatch(Element target, final T object,
-        NativeEvent event, HasCell<T, X, Void> hc) {
-      final FieldUpdater<T, X, Void> fieldUpdater = hc.getFieldUpdater();
-      hc.getCell().onBrowserEvent(target, hc.getValue(object), null, event,
-          fieldUpdater == null ? null : new ValueUpdater<X, Void>() {
-            public void update(X value, Void viewData) {
-              fieldUpdater.update(0, object, value, viewData);
-            }
-          });
-    }
   }
 
   /**
-   * The info needed to create a {@link TreeNodeView}.
+   * The info needed to create the children of a tree node.
    */
   interface NodeInfo<T> {
 
-    List<HasCell<T, ?, Void>> getHasCells();
+    /**
+     * Get the {@link Cell} used to render the children of this node.
+     * 
+     * @return the {@link Cell}
+     */
+    Cell<T, Void> getCell();
 
+    /**
+     * Return the key provider for children of this node.
+     * 
+     * @return the {@link ProvidesKey}
+     */
     ProvidesKey<T> getProvidesKey();
 
     /**
-     * Handle an event that is fired on one of the children of this item.
-     *
-     * @param elem the parent element of the item
-     * @param object the data value of the item
-     * @param event the event that was fired
+     * Get the {@link SelectionModel} used for the children of this node. To
+     * unify selection across all items of the same type, or across the entire
+     * tree, return the same instance of {@link SelectionModel} from all
+     * {@link NodeInfo}.
+     * 
+     * @return the {@link SelectionModel}
      */
-    void onBrowserEvent(Element elem, final T object, NativeEvent event);
+    SelectionModel<? super T> getSelectionModel();
 
+    /**
+     * Get the value updater associated with the cell.
+     * 
+     * @return the value updater
+     */
+    ValueUpdater<T, Void> getValueUpdater();
+
+    /**
+     * Set the view that is listening to this {@link NodeInfo}. The
+     * implementation should attach the view to the source of data.
+     * 
+     * @param view the {@link ListView}
+     */
     void setView(ListView<T> view);
 
+    /**
+     * Unset the view from the {@link NodeInfo}. The implementation should
+     * detach the view from the source of data.
+     */
     void unsetView();
   }
 
@@ -168,20 +150,18 @@
    * Get the {@link NodeInfo} that will provide the {@link ProvidesKey},
    * {@link Cell}, and {@link ListView}s to retrieve and display the children of
    * the specified value.
-   *
+   * 
    * @param value the value in the parent node
-   * @param treeNode the {@link TreeNode} that contains the value
    * @return the {@link NodeInfo}
    */
-  <T> NodeInfo<?> getNodeInfo(T value, TreeNode<T> treeNode);
+  <T> NodeInfo<?> getNodeInfo(T value);
 
   /**
    * Check if the value is known to be a leaf node.
-   *
+   * 
    * @param value the value at the node
-   * @param treeNode the {@link TreeNode} that contains the value
-   *
+   * 
    * @return true if the node is known to be a leaf node, false otherwise
    */
-  boolean isLeaf(Object value, TreeNode<?> treeNode);
+  boolean isLeaf(Object value);
 }
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/BasicTreeRecipe.java b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/BasicTreeRecipe.java
index 1fd42e9..b163ded 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/BasicTreeRecipe.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/BasicTreeRecipe.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.sample.bikeshed.cookbook.client;
 
+import com.google.gwt.bikeshed.list.shared.MultiSelectionModel;
 import com.google.gwt.bikeshed.list.shared.SelectionModel;
 import com.google.gwt.bikeshed.list.shared.SelectionModel.SelectionChangeEvent;
 import com.google.gwt.bikeshed.tree.client.StandardTreeView;
@@ -37,17 +38,15 @@
     FlowPanel p = new FlowPanel();
 
     final Label label = new Label();
-    final MultiSelectionModel<Object> selectionModel = new MultiSelectionModel<Object>();
+    final MultiSelectionModel<String> selectionModel = new MultiSelectionModel<String>();
     selectionModel.addSelectionChangeHandler(new SelectionModel.SelectionChangeHandler() {
       public void onSelectionChange(SelectionChangeEvent event) {
-        label.setText("Selected "
-            + selectionModel.getSelectedSet().toString());
+        label.setText("Selected " + selectionModel.getSelectedSet().toString());
       }
     });
 
     StandardTreeView stree = new StandardTreeView(new MyTreeViewModel(
         selectionModel), "...");
-    stree.setSelectionModel(selectionModel);
     stree.setAnimationEnabled(true);
 
     p.add(stree);
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/Cookbook.java b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/Cookbook.java
index 8d12fd1..5c32123 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/Cookbook.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/Cookbook.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -18,9 +18,9 @@
 import com.google.gwt.bikeshed.cells.client.Cell;
 import com.google.gwt.bikeshed.list.shared.ListViewAdapter;
 import com.google.gwt.bikeshed.list.shared.SelectionModel;
+import com.google.gwt.bikeshed.list.shared.SingleSelectionModel;
 import com.google.gwt.bikeshed.list.shared.SelectionModel.SelectionChangeEvent;
 import com.google.gwt.bikeshed.tree.client.StandardTreeView;
-import com.google.gwt.bikeshed.tree.client.TreeNode;
 import com.google.gwt.bikeshed.tree.client.TreeViewModel;
 import com.google.gwt.core.client.EntryPoint;
 import com.google.gwt.core.client.GWT;
@@ -58,21 +58,27 @@
 
   private static final class RecipeTreeModel implements TreeViewModel {
     private ListViewAdapter<Category> adapter = new ListViewAdapter<Category>();
+    private SelectionModel<Object> selectionModel;
 
-    public <T> NodeInfo<?> getNodeInfo(T value, TreeNode<T> treeNode) {
+    public RecipeTreeModel(SelectionModel<Object> selectionModel) {
+      this.selectionModel = selectionModel;
+    }
+
+    public <T> NodeInfo<?> getNodeInfo(T value) {
       if (value == null) {
         // Categories at the root.
-        return new DefaultNodeInfo<Category>(adapter, new CategoryCell(), false);
+        return new DefaultNodeInfo<Category>(adapter, new CategoryCell(),
+            selectionModel, null);
       } else if (value instanceof Category) {
         // Demos for each category.
         Category category = (Category) value;
         return new DefaultNodeInfo<Recipe>(new ListViewAdapter<Recipe>(
-            category.getRecipes()), new RecipeCell(), false);
+            category.getRecipes()), new RecipeCell(), selectionModel, null);
       }
       return null;
     }
 
-    public boolean isLeaf(Object value, TreeNode<?> treeNode) {
+    public boolean isLeaf(Object value) {
       // The root and categories have children.
       if (value == null || value instanceof Category) {
         return false;
@@ -89,17 +95,18 @@
   @UiField StandardTreeView recipeTree;
   @UiField LayoutPanel container;
 
-  private RecipeTreeModel recipeTreeModel = new RecipeTreeModel();
-  private SingleSelectionModel<Object> treeSelection;
+  private RecipeTreeModel recipeTreeModel;
   private SimpleCellListRecipe defaultRecipe;
   private Recipe curRecipe;
 
   public void onModuleLoad() {
+    // Initialize the UI.
+    final SingleSelectionModel<Object> treeSelection = new SingleSelectionModel<Object>();
+    recipeTreeModel = new RecipeTreeModel(treeSelection);
     RootLayoutPanel.get().add(binder.createAndBindUi(this));
     createRecipes(recipeTreeModel.adapter.getList());
 
-    treeSelection = new SingleSelectionModel<Object>();
-    recipeTree.setSelectionModel(treeSelection);
+    // Select a recipe on selection.
     treeSelection.addSelectionChangeHandler(new SelectionModel.SelectionChangeHandler() {
       public void onSelectionChange(SelectionChangeEvent event) {
         Object o = treeSelection.getSelectedObject();
@@ -120,21 +127,13 @@
   private void createRecipes(List<Category> cats) {
     defaultRecipe = new SimpleCellListRecipe();
 
-    cats.add(new Category("Lists", new Recipe[] {
-        defaultRecipe
-    }));
+    cats.add(new Category("Lists", new Recipe[] {defaultRecipe}));
     cats.add(new Category("Tables", new Recipe[] {
-        new BasicTableRecipe(),
-        new EditableTableRecipe(),
-    }));
+        new BasicTableRecipe(), new EditableTableRecipe(),}));
     cats.add(new Category("Trees", new Recipe[] {
-        new BasicTreeRecipe(),
-        new SideBySideTreeRecipe(),
-    }));
+        new BasicTreeRecipe(), new SideBySideTreeRecipe(),}));
     cats.add(new Category("Other", new Recipe[] {
-        new ValidationRecipe(),
-        new MailRecipe(),
-    }));
+        new ValidationRecipe(), new MailRecipe(),}));
   }
 
   private void showRecipe(Recipe recipe) {
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MyTreeViewModel.java b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MyTreeViewModel.java
index 6783775..93fe842 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MyTreeViewModel.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MyTreeViewModel.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -18,13 +18,13 @@
 import com.google.gwt.bikeshed.cells.client.ButtonCell;
 import com.google.gwt.bikeshed.cells.client.Cell;
 import com.google.gwt.bikeshed.cells.client.CheckboxCell;
+import com.google.gwt.bikeshed.cells.client.CompositeCell;
 import com.google.gwt.bikeshed.cells.client.FieldUpdater;
 import com.google.gwt.bikeshed.cells.client.ValueUpdater;
 import com.google.gwt.bikeshed.list.client.HasCell;
 import com.google.gwt.bikeshed.list.client.ListView;
 import com.google.gwt.bikeshed.list.shared.AbstractListViewAdapter;
 import com.google.gwt.bikeshed.list.shared.SelectionModel;
-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;
@@ -39,7 +39,8 @@
  */
 public class MyTreeViewModel implements TreeViewModel {
 
-  private static class IntegerListViewAdapter extends AbstractListViewAdapter<Integer> {
+  private static class IntegerListViewAdapter extends
+      AbstractListViewAdapter<Integer> {
     int wordLength;
 
     public IntegerListViewAdapter(int wordLength) {
@@ -55,7 +56,8 @@
     }
   }
 
-  private static class StringListViewAdapter extends AbstractListViewAdapter<String> {
+  private static class StringListViewAdapter extends
+      AbstractListViewAdapter<String> {
     String value;
 
     public StringListViewAdapter(final String value) {
@@ -103,17 +105,19 @@
     }
   };
 
-  private List<HasCell<String, ?, Void>> hasCells =
-    new ArrayList<HasCell<String, ?, Void>>();
+  private CompositeCell<String, Void> compositeCell = new CompositeCell<String, Void>();
+  private SelectionModel<String> selectionModel;
 
-  public MyTreeViewModel(final SelectionModel<Object> selectionModel) {
-    hasCells.add(new HasCell<String, Boolean, Void>() {
-      public boolean dependsOnSelection() {
-        return true;
-      }
-
+  public MyTreeViewModel(final SelectionModel<String> selectionModel) {
+    this.selectionModel = selectionModel;
+    compositeCell.addHasCell(new HasCell<String, Boolean, Void>() {
       public Cell<Boolean, Void> getCell() {
-        return new CheckboxCell();
+        return new CheckboxCell() {
+          @Override
+          public boolean dependsOnSelection() {
+            return true;
+          }
+        };
       }
 
       public FieldUpdater<String, Boolean, Void> getFieldUpdater() {
@@ -129,11 +133,7 @@
         return selectionModel.isSelected(object);
       }
     });
-    hasCells.add(new HasCell<String, String, Void>() {
-      public boolean dependsOnSelection() {
-        return false;
-      }
-
+    compositeCell.addHasCell(new HasCell<String, String, Void>() {
       public Cell<String, Void> getCell() {
         return ButtonCell.getInstance();
       }
@@ -153,7 +153,7 @@
     });
   }
 
-  public <T> NodeInfo<?> getNodeInfo(T value, TreeNode<T> treeNode) {
+  public <T> NodeInfo<?> getNodeInfo(T value) {
     if (value instanceof String) {
       return getNodeInfoHelper((String) value);
     }
@@ -163,17 +163,20 @@
     throw new IllegalArgumentException("Unsupported object type: " + type);
   }
 
-  public boolean isLeaf(Object value, TreeNode<?> parentNode) {
+  public boolean isLeaf(Object value) {
     return value instanceof Integer;
   }
 
   private NodeInfo<?> getNodeInfoHelper(final String value) {
     if (value.endsWith("...")) {
-      AbstractListViewAdapter<String> adapter = new StringListViewAdapter(value.toString());
-      return new DefaultNodeInfo<String>(adapter, hasCells);
+      AbstractListViewAdapter<String> adapter = new StringListViewAdapter(
+          value.toString());
+      return new DefaultNodeInfo<String>(adapter, compositeCell,
+          selectionModel, null);
     } else {
-      AbstractListViewAdapter<Integer> adapter = new IntegerListViewAdapter(value.length());
-      return new DefaultNodeInfo<Integer>(adapter, INTEGER_CELL, false,
+      AbstractListViewAdapter<Integer> adapter = new IntegerListViewAdapter(
+          value.length());
+      return new DefaultNodeInfo<Integer>(adapter, INTEGER_CELL, null,
           new ValueUpdater<Integer, Void>() {
             public void update(Integer value, Void viewData) {
               Window.alert("Integer = " + value);
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SideBySideTreeRecipe.java b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SideBySideTreeRecipe.java
index 8e7e234..5c18d2e 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SideBySideTreeRecipe.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SideBySideTreeRecipe.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.sample.bikeshed.cookbook.client;
 
+import com.google.gwt.bikeshed.list.shared.MultiSelectionModel;
 import com.google.gwt.bikeshed.list.shared.SelectionModel;
 import com.google.gwt.bikeshed.list.shared.SelectionModel.SelectionChangeEvent;
 import com.google.gwt.bikeshed.tree.client.SideBySideTreeView;
@@ -37,7 +38,7 @@
     FlowPanel p = new FlowPanel();
 
     final Label label = new Label();
-    final MultiSelectionModel<Object> selectionModel = new MultiSelectionModel<Object>();
+    final MultiSelectionModel<String> selectionModel = new MultiSelectionModel<String>();
     selectionModel.addSelectionChangeHandler(new SelectionModel.SelectionChangeHandler() {
       public void onSelectionChange(SelectionChangeEvent event) {
         label.setText("Selected " + selectionModel.getSelectedSet().toString());
@@ -45,8 +46,8 @@
     });
 
     SideBySideTreeView sstree = new SideBySideTreeView(new MyTreeViewModel(
-        selectionModel), "...", 100, 4);
-    sstree.setSelectionModel(selectionModel);
+        selectionModel), "...");
+    sstree.setAnimationEnabled(true);
     sstree.setHeight("200px");
 
     p.add(sstree);
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/StocksDesktop.java b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/StocksDesktop.java
index cbc7b94..d7e66ee 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/StocksDesktop.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/StocksDesktop.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -21,7 +21,6 @@
 import com.google.gwt.bikeshed.list.shared.ListViewAdapter;
 import com.google.gwt.bikeshed.list.shared.Range;
 import com.google.gwt.bikeshed.tree.client.SideBySideTreeView;
-import com.google.gwt.bikeshed.tree.client.TreeNode;
 import com.google.gwt.core.client.EntryPoint;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.logical.shared.CloseEvent;
@@ -54,7 +53,8 @@
  */
 public class StocksDesktop implements EntryPoint, Updater {
 
-  interface Binder extends UiBinder<Widget, StocksDesktop> { }
+  interface Binder extends UiBinder<Widget, StocksDesktop> {
+  }
 
   private static final Binder binder = GWT.create(Binder.class);
 
@@ -67,13 +67,19 @@
     return NumberFormat.getCurrencyFormat("USD").format(price / 100.0);
   }
 
-  @UiField Label cashLabel;
+  @UiField
+  Label cashLabel;
 
-  @UiField FavoritesWidget favoritesWidget;
-  @UiField Label netWorthLabel;
-  @UiField PlayerScoresWidget playerScoresWidget;
-  @UiField StockQueryWidget queryWidget;
-  @UiField SideBySideTreeView transactionTree;
+  @UiField
+  FavoritesWidget favoritesWidget;
+  @UiField
+  Label netWorthLabel;
+  @UiField
+  PlayerScoresWidget playerScoresWidget;
+  @UiField
+  StockQueryWidget queryWidget;
+  @UiField
+  SideBySideTreeView transactionTree;
 
   /**
    * The popup used to purchase stock.
@@ -84,8 +90,7 @@
   private AsyncListViewAdapter<StockQuote> favoritesListViewAdapter;
   private AsyncListViewAdapter<PlayerInfo> playerScoresListViewAdapter;
   private AsyncListViewAdapter<StockQuote> searchListViewAdapter;
-  private Map<String, ListViewAdapter<Transaction>> transactionListViewAdaptersByTicker =
-    new HashMap<String, ListViewAdapter<Transaction>>();
+  private Map<String, ListViewAdapter<Transaction>> transactionListViewAdaptersByTicker = new HashMap<String, ListViewAdapter<Transaction>>();
   private ListViewAdapter<Transaction> transactionListViewAdapter;
   private List<Transaction> transactions;
 
@@ -145,19 +150,22 @@
 
     // Hook up handlers to columns and the buy/sell popup.
     Columns.favoriteColumn.setFieldUpdater(new FieldUpdater<StockQuote, Boolean, Void>() {
-      public void update(int index, StockQuote object, Boolean value, Void viewData) {
+      public void update(int index, StockQuote object, Boolean value,
+          Void viewData) {
         setFavorite(object.getTicker(), value);
       }
     });
 
     Columns.buyColumn.setFieldUpdater(new FieldUpdater<StockQuote, String, Void>() {
-      public void update(int index, StockQuote quote, String value, Void viewData) {
+      public void update(int index, StockQuote quote, String value,
+          Void viewData) {
         buy(quote);
       }
     });
 
     Columns.sellColumn.setFieldUpdater(new FieldUpdater<StockQuote, String, Void>() {
-      public void update(int index, StockQuote quote, String value, Void viewData) {
+      public void update(int index, StockQuote quote, String value,
+          Void viewData) {
         sell(quote);
       }
     });
@@ -176,7 +184,7 @@
 
   /**
    * Process the {@link StockResponse} from the server.
-   *
+   * 
    * @param response the stock response
    */
   public void processStockResponse(StockResponse response) {
@@ -212,7 +220,7 @@
 
   /**
    * Set or unset a ticker symbol as a 'favorite'.
-   *
+   * 
    * @param ticker the ticker symbol
    * @param favorite if true, make the stock a favorite
    */
@@ -230,7 +238,8 @@
             }
           });
     } else {
-      dataService.removeFavorite(ticker, favoritesListViewAdapter.getRanges()[0],
+      dataService.removeFavorite(ticker,
+          favoritesListViewAdapter.getRanges()[0],
           new AsyncCallback<StockResponse>() {
             public void onFailure(Throwable caught) {
               handleRpcError(caught, "Error removing favorite");
@@ -289,9 +298,7 @@
     Range[] searchRanges = searchListViewAdapter.getRanges();
     Range[] favoritesRanges = favoritesListViewAdapter.getRanges();
 
-    String sectorName = getSectorName();
-    SectorListViewAdapter sectorListViewAdapter = sectorName != null
-        ? treeModel.getSectorListViewAdapter(sectorName) : null;
+    SectorListViewAdapter sectorListViewAdapter = treeModel.getSectorListViewAdapter();
     Range[] sectorRanges = sectorListViewAdapter == null ? null
         : sectorListViewAdapter.getRanges();
 
@@ -303,8 +310,8 @@
     String searchQuery = queryWidget.getSearchQuery();
 
     StockRequest request = new StockRequest(searchQuery,
-        sectorListViewAdapter != null ? sectorListViewAdapter.getSector() : null,
-        searchRanges[0], favoritesRanges[0], sectorRanges != null
+        sectorListViewAdapter != null ? sectorListViewAdapter.getSector()
+            : null, searchRanges[0], favoritesRanges[0], sectorRanges != null
             && sectorRanges.length > 0 ? sectorRanges[0] : null);
     dataService.getStockQuotes(request, new AsyncCallback<StockResponse>() {
       public void onFailure(Throwable caught) {
@@ -336,26 +343,15 @@
 
   @UiFactory
   SideBySideTreeView createTransactionTree() {
-    return new SideBySideTreeView(treeModel, null, 200);
-  }
-
-  // Hack - walk the transaction tree to find the current viewed sector
-  private String getSectorName() {
-    int children = transactionTree.getRootNode().getChildCount();
-    for (int i = 0; i < children; i++) {
-      TreeNode<?> childNode = transactionTree.getRootNode().getChildNode(i);
-      if (childNode.isOpen()) {
-        return (String) childNode.getValue();
-      }
-    }
-
-    return null;
+    SideBySideTreeView treeView = new SideBySideTreeView(treeModel, null);
+    treeView.setAnimationEnabled(true);
+    return treeView;
   }
 
   /**
    * Display a message to the user when an RPC call fails.
-   *
-   * @param caughtbad the exception
+   * 
+   * @param caught the exception
    * @param displayMessage the message to display to the user, or null to
    *          display a default message
    * @return true if recoverable, false if not
@@ -395,7 +391,7 @@
     // Update the sector list.
     StockQuoteList sectorList = response.getSector();
     if (sectorList != null) {
-      SectorListViewAdapter sectorListViewAdapter = treeModel.getSectorListViewAdapter(getSectorName());
+      SectorListViewAdapter sectorListViewAdapter = treeModel.getSectorListViewAdapter();
       if (sectorListViewAdapter != null) {
         sectorListViewAdapter.updateDataSize(response.getNumSector(), true);
         sectorListViewAdapter.updateViewData(sectorList.getStartIndex(),
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/TransactionTreeViewModel.java b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/TransactionTreeViewModel.java
index c213e10..943370c 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/TransactionTreeViewModel.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/TransactionTreeViewModel.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -23,7 +23,8 @@
 import com.google.gwt.bikeshed.list.shared.AbstractListViewAdapter;
 import com.google.gwt.bikeshed.list.shared.AsyncListViewAdapter;
 import com.google.gwt.bikeshed.list.shared.ListViewAdapter;
-import com.google.gwt.bikeshed.tree.client.TreeNode;
+import com.google.gwt.bikeshed.list.shared.ProvidesKey;
+import com.google.gwt.bikeshed.list.shared.SingleSelectionModel;
 import com.google.gwt.bikeshed.tree.client.TreeViewModel;
 import com.google.gwt.sample.bikeshed.stocks.shared.StockQuote;
 import com.google.gwt.sample.bikeshed.stocks.shared.Transaction;
@@ -57,13 +58,9 @@
     }
   }
 
-  static class TransactionCell extends Cell<Transaction, Void> {
-    @Override
-    public void render(Transaction value, Void viewData, StringBuilder sb) {
-      sb.append(value.toString());
-    }
-  }
-
+  /**
+   * A {@link Cell} used to render a {@link StockQuote}.
+   */
   private static final Cell<StockQuote, Void> STOCK_QUOTE_CELL = new Cell<StockQuote, Void>() {
     @Override
     public void render(StockQuote value, Void viewData, StringBuilder sb) {
@@ -71,21 +68,52 @@
     }
   };
 
-  private static final Cell<Transaction, Void> TRANSACTION_CELL = new TransactionCell();
+  /**
+   * A {@link Cell} used to render a {@link Transaction}.
+   */
+  private static final Cell<Transaction, Void> TRANSACTION_CELL = new Cell<Transaction, Void>() {
+    @Override
+    public void render(Transaction value, Void viewData, StringBuilder sb) {
+      sb.append(value.toString());
+    }
+  };
+
+  /**
+   * The last {@link StockQuote} that was opened.
+   */
+  private StockQuote lastStockQuote;
+
+  /**
+   * The last Sector that was opened.
+   */
+  private String lastSector;
 
   private Map<String, SectorListViewAdapter> sectorListViewAdapters = new HashMap<String, SectorListViewAdapter>();
   private AbstractListViewAdapter<StockQuote> stockQuoteListViewAdapter;
   private ListViewAdapter<String> topLevelListViewAdapter = new ListViewAdapter<String>();
+  private SingleSelectionModel<Object> selectionModel = new SingleSelectionModel<Object>();
 
   private Map<String, ListViewAdapter<Transaction>> transactionListViewAdaptersByTicker;
 
   private Updater updater;
 
-  public TransactionTreeViewModel(Updater updater,
+  public TransactionTreeViewModel(
+      Updater updater,
       AbstractListViewAdapter<StockQuote> stockQuoteListViewAdapter,
       Map<String, ListViewAdapter<Transaction>> transactionListViewAdaptersByTicker) {
+    this.selectionModel.setKeyProvider(new ProvidesKey<Object>() {
+      public Object getKey(Object item) {
+        if (item instanceof StockQuote) {
+          return StockQuote.KEY_PROVIDER.getKey((StockQuote) item);
+        } else {
+          return item;
+        }
+      }
+    });
     this.updater = updater;
     this.stockQuoteListViewAdapter = stockQuoteListViewAdapter;
+    
+    // Setup the sector list.
     List<String> topLevelList = topLevelListViewAdapter.getList();
     topLevelList.add("Favorites");
     topLevelList.add("Dow Jones Industrials");
@@ -93,60 +121,73 @@
     this.transactionListViewAdaptersByTicker = transactionListViewAdaptersByTicker;
   }
 
-  public <T> NodeInfo<?> getNodeInfo(T value, final TreeNode<T> treeNode) {
+  public <T> NodeInfo<?> getNodeInfo(T value) {
     if (value == null) {
+      // Return list of sectors.
       return new TreeViewModel.DefaultNodeInfo<String>(topLevelListViewAdapter,
-          TextCell.getInstance(), false);
+          TextCell.getInstance(), selectionModel, null);
     } else if ("Favorites".equals(value)) {
-      return new TreeViewModel.DefaultNodeInfo<StockQuote>(stockQuoteListViewAdapter,
-          STOCK_QUOTE_CELL, false);
+      // Return favorites. 
+      return new TreeViewModel.DefaultNodeInfo<StockQuote>(
+          stockQuoteListViewAdapter, STOCK_QUOTE_CELL, selectionModel, null);
     } else if ("History".equals(value)) {
-      String ticker = ((StockQuote) treeNode.getParentNode().getValue()).getTicker();
+      // Return history of the current stock quote.
+      String ticker = lastStockQuote.getTicker();
       ListViewAdapter<Transaction> adapter = transactionListViewAdaptersByTicker.get(ticker);
       if (adapter == null) {
         adapter = new ListViewAdapter<Transaction>();
         transactionListViewAdaptersByTicker.put(ticker, adapter);
       }
       return new TreeViewModel.DefaultNodeInfo<Transaction>(adapter,
-          TRANSACTION_CELL, false);
+          TRANSACTION_CELL, selectionModel, null);
     } else if ("Actions".equals(value)) {
+      // Return the actions for the current stock quote.
       ListViewAdapter<String> adapter = new ListViewAdapter<String>();
       List<String> list = adapter.getList();
       list.add("Buy");
       list.add("Sell");
       return new TreeViewModel.DefaultNodeInfo<String>(adapter,
-          ButtonCell.getInstance(), false, new ValueUpdater<String, Void>() {
+          ButtonCell.getInstance(), selectionModel,
+          new ValueUpdater<String, Void>() {
             public void update(String value, Void viewData) {
-              StockQuote stockQuote = (StockQuote) treeNode.getParentNode().getValue();
               if ("Buy".equals(value)) {
-                updater.buy(stockQuote);
+                updater.buy(lastStockQuote);
               } else {
-                updater.sell(stockQuote);
+                updater.sell(lastStockQuote);
               }
             }
           });
     } else if (value instanceof String) {
-      SectorListViewAdapter adapter = new SectorListViewAdapter((String) value);
-      sectorListViewAdapters.put((String) value, adapter);
+      // Return the stocks for a given sector.
+      lastSector = (String) value;
+      SectorListViewAdapter adapter = new SectorListViewAdapter(lastSector);
+      sectorListViewAdapters.put(lastSector, adapter);
       return new TreeViewModel.DefaultNodeInfo<StockQuote>(adapter,
-          STOCK_QUOTE_CELL, false);
+          STOCK_QUOTE_CELL, selectionModel, null);
     } else if (value instanceof StockQuote) {
+      // Return the submenu for a stock quote.
+      lastStockQuote = (StockQuote) value;
       ListViewAdapter<String> adapter = new ListViewAdapter<String>();
       List<String> list = adapter.getList();
       list.add("Actions");
       list.add("History");
       return new TreeViewModel.DefaultNodeInfo<String>(adapter,
-          TextCell.getInstance(), false);
+          TextCell.getInstance(), selectionModel, null);
     }
 
     throw new IllegalArgumentException(value.toString());
   }
 
-  public SectorListViewAdapter getSectorListViewAdapter(String value) {
-    return sectorListViewAdapters.get(value);
+  /**
+   * Get the {@link SectorListViewAdapter} for the last selected sector.
+   * 
+   * @return the {@link SectorListViewAdapter}
+   */
+  public SectorListViewAdapter getSectorListViewAdapter() {
+    return lastSector == null ? null : sectorListViewAdapters.get(lastSector);
   }
 
-  public boolean isLeaf(Object value, final TreeNode<?> parentNode) {
+  public boolean isLeaf(Object value) {
     if (value instanceof Transaction || "Buy".equals(value)
         || "Sell".equals(value)) {
       return true;
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/server/StockServiceImpl.java b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/server/StockServiceImpl.java
index a9a6bb1..c7db58b 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/server/StockServiceImpl.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/server/StockServiceImpl.java
@@ -360,7 +360,7 @@
       }
     }
 
-    return new Result(toRet, toRet.size());
+    return new Result(toRet, symbols.size());
   }
 
   // If a query is alpha-only ([A-Za-z]+), return stocks for which:
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/style/client/Styles.java b/bikeshed/src/com/google/gwt/sample/bikeshed/style/client/Styles.java
index 0e823a0..c4d0816 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/style/client/Styles.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/style/client/Styles.java
@@ -20,8 +20,14 @@
 import com.google.gwt.resources.client.CssResource;
 import com.google.gwt.resources.client.CssResource.NotStrict;
 
+/**
+ * The Styles used in bikeshed.
+ */
 public class Styles {
 
+  /**
+   * Common styles.
+   */
   public interface Common extends CssResource {
     String box();
     String header();
@@ -31,6 +37,9 @@
     String table();
   }
 
+  /**
+   * Shared resources.
+   */
   public interface Resources extends ClientBundle {
     @NotStrict
     @Source("common.css")
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/style/client/common.css b/bikeshed/src/com/google/gwt/sample/bikeshed/style/client/common.css
index 5551e36..5162d1e 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/style/client/common.css
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/style/client/common.css
@@ -71,18 +71,19 @@
   background-color: rgb(56, 117, 215);
 }
 
-/* ss tree */
-.gwt-sstree {
+/** Applies to StandardTreeView **/
+.gwt-tree-selectedItem {
+  background-color: rgb(56, 117, 215);
+}
+
+
+/** Applies to SideBySideTreeView **/
+.gwt-SideBySideTreeView {
 }
 
 .gwt-sstree-column {
-  overflow-y: scroll;
-  overflow-x: auto;
-  position: relative;
-}
-
-.gwt-sstree-selectedItem {
-  background-color: rgb(56, 117, 215);
+  border-right: 1px solid #aaa;
+  border-left: 1px solid #aaa;
 }
 
 .gwt-sstree-evenRow {
@@ -93,6 +94,10 @@
   background-color: rgb(220, 220, 220);
 }
 
+.gwt-sstree-selectedItem {
+  background-color: rgb(56, 117, 215);
+}
+
 /* date picker */
 .gwt-DatePicker {
   border: 1px solid #A2BBDD;