Adds animations to the TreeView.  Adds user configurable loading message while children are pending.  Cleans up child nodes when a node is closed to prevent memory leak.


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7621 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/bikeshed/src/com/google/gwt/sample/tree/client/MyTreeViewModel.java b/bikeshed/src/com/google/gwt/sample/tree/client/MyTreeViewModel.java
index aff8791..d0bda7a 100644
--- a/bikeshed/src/com/google/gwt/sample/tree/client/MyTreeViewModel.java
+++ b/bikeshed/src/com/google/gwt/sample/tree/client/MyTreeViewModel.java
@@ -17,11 +17,11 @@
 
 import com.google.gwt.cells.client.ButtonCell;
 import com.google.gwt.cells.client.Cell;
-import com.google.gwt.cells.client.TextCell;
 import com.google.gwt.cells.client.ValueUpdater;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.list.shared.AsyncListModel;
 import com.google.gwt.list.shared.ListModel;
+import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
@@ -54,12 +54,24 @@
               value.length() - 3) : value;
           dataService.getNext(prefix, new AsyncCallback<List<String>>() {
             public void onFailure(Throwable caught) {
-              Window.alert("Error: " + caught);
+              String message = caught.getMessage();
+              if (message.contains("Not logged in")) {
+                // Force the user to login.
+                Window.Location.reload();
+              } else {
+                Window.alert("ERROR: " + caught.getMessage());
+              }
             }
 
-            public void onSuccess(List<String> result) {
-              listModel.updateDataSize(result.size(), true);
-              listModel.updateViewData(0, result.size(), result);
+            public void onSuccess(final List<String> result) {
+              // Use a timer to simulate network delay.
+              new Timer() {
+                @Override
+                public void run() {
+                  listModel.updateDataSize(result.size(), true);
+                  listModel.updateViewData(0, result.size(), result);
+                }
+              }.schedule(500);
             }
           });
         }
@@ -79,11 +91,6 @@
     }
   };
 
-  /**
-   * The cell used to render strings.
-   */
-  private static final Cell<String> STRING_CELL = new TextCell();
-
   public <T> NodeInfo<?> getNodeInfo(T value, TreeNodeView<T> treeNodeView) {
     if (value instanceof String) {
       return getNodeInfoHelper((String) value);
diff --git a/bikeshed/src/com/google/gwt/sample/tree/client/TreeEntryPoint.java b/bikeshed/src/com/google/gwt/sample/tree/client/TreeEntryPoint.java
index 467f534..90d1372 100644
--- a/bikeshed/src/com/google/gwt/sample/tree/client/TreeEntryPoint.java
+++ b/bikeshed/src/com/google/gwt/sample/tree/client/TreeEntryPoint.java
@@ -25,6 +25,7 @@
 
   public void onModuleLoad() {
     TreeView tree = new TreeView(new MyTreeViewModel(), "...");
+    tree.setAnimationEnabled(true);
     RootPanel.get().add(tree);
   }
 }
diff --git a/bikeshed/src/com/google/gwt/sample/tree/client/TreeNodeView.java b/bikeshed/src/com/google/gwt/sample/tree/client/TreeNodeView.java
index 87f8576..e159107 100644
--- a/bikeshed/src/com/google/gwt/sample/tree/client/TreeNodeView.java
+++ b/bikeshed/src/com/google/gwt/sample/tree/client/TreeNodeView.java
@@ -19,6 +19,9 @@
 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.list.shared.ListEvent;
 import com.google.gwt.list.shared.ListHandler;
 import com.google.gwt.list.shared.ListModel;
@@ -186,19 +189,14 @@
         nodeInfo = tree.getTreeViewModel().getNodeInfo(value, this);
         nodeInfoLoaded = true;
       }
+
+      // If we don't have any nodeInfo, we must be a leaf node.
       if (nodeInfo != null) {
         onOpen(nodeInfo);
       }
     } else {
-      // Unregister the list handler.
-      if (listReg != null) {
-        listReg.removeHandler();
-        listReg = null;
-      }
-
-      // Remove the children.
-      childContainer.setInnerHTML("");
-      children.clear();
+      cleanup();
+      tree.maybeAnimateTreeNode(this);
     }
 
     // Update the image.
@@ -222,6 +220,13 @@
   }
 
   /**
+   * @return the element that contains the children
+   */
+  Element getChildContainer() {
+    return childContainer;
+  }
+
+  /**
    * @return the image element
    */
   Element getImageElement() {
@@ -233,14 +238,57 @@
   }
 
   /**
-   * Set the {@link Element} that will contain the children. Used by
-   * {@link TreeView}.
-   * 
-   * @param elem the child container element
+   * Cleanup this node and all its children. This node can still be used.
    */
-  void initChildContainer(Element elem) {
-    assert this.childContainer == null : "childContainer already initialized.";
-    this.childContainer = elem;
+  private void cleanup() {
+    // Unregister the list handler.
+    if (listReg != null) {
+      listReg.removeHandler();
+      listReg = null;
+    }
+
+    // Recursively kill chidren.
+    if (children != null) {
+      for (TreeNodeView<?> child : children) {
+        child.cleanup();
+      }
+      children = null;
+    }
+  }
+
+  /**
+   * Ensure that the animation frame exists and return it.
+   * 
+   * @return the animation frame
+   */
+  private Element ensureAnimationFrame() {
+    return ensureChildContainer().getParentElement();
+  }
+
+  /**
+   * Ensure that the child container exists and return it.
+   * 
+   * @return the child container
+   */
+  private Element ensureChildContainer() {
+    if (childContainer == null) {
+      // If this is a root node or the element does not exist, create it.
+      Element animFrame = getElement().appendChild(
+          Document.get().createDivElement());
+      animFrame.getStyle().setPosition(Position.RELATIVE);
+      animFrame.getStyle().setOverflow(Overflow.HIDDEN);
+      childContainer = animFrame.appendChild(Document.get().createDivElement());
+    }
+    return childContainer;
+  }
+
+  /**
+   * Check if this is a root node at the top of the tree.
+   * 
+   * @return true if a root node, false if not
+   */
+  private boolean isRootNode() {
+    return getParentTreeNodeView() == null;
   }
 
   /**
@@ -251,12 +299,19 @@
    * @param <C> the child data type of the node.
    */
   private <C> void onOpen(final NodeInfo<C> nodeInfo) {
+    // Add a loading message.
+    ensureChildContainer().setInnerHTML(tree.getLoadingHtml());
+    ensureAnimationFrame().getStyle().setProperty("display", "");
+
     // Get the node info.
     ListModel<C> listModel = nodeInfo.getListModel();
     listReg = listModel.addListHandler(new ListHandler<C>() {
       public void onDataChanged(ListEvent<C> event) {
         // TODO - handle event start and length
 
+        // Hide the child container so we can animate it.
+        ensureAnimationFrame().getStyle().setDisplay(Display.NONE);
+
         // Construct the child contents.
         TreeViewModel model = tree.getTreeViewModel();
         int imageWidth = tree.getImageWidth();
@@ -277,13 +332,6 @@
           sb.append("</div>");
           sb.append("</div>");
         }
-
-        // Replace contents of the child container.
-        if (childContainer == null) {
-          Element elem = getElement();
-          initChildContainer(Document.get().createDivElement());
-          elem.appendChild(childContainer);
-        }
         childContainer.setInnerHTML(sb.toString());
 
         // Create the child TreeNodeViews from the new elements.
@@ -295,6 +343,9 @@
           children.add(child);
           childElem = childElem.getNextSiblingElement();
         }
+
+        // Animate the child container open.
+        tree.maybeAnimateTreeNode(TreeNodeView.this);
       }
 
       public void onSizeChanged(SizeChangeEvent event) {
@@ -319,7 +370,7 @@
    */
   private void updateImage() {
     // Early out if this is a root node.
-    if (getParentTreeNodeView() == null) {
+    if (isRootNode()) {
       return;
     }
 
diff --git a/bikeshed/src/com/google/gwt/sample/tree/client/TreeView.java b/bikeshed/src/com/google/gwt/sample/tree/client/TreeView.java
index d31d99b..b640ef4 100644
--- a/bikeshed/src/com/google/gwt/sample/tree/client/TreeView.java
+++ b/bikeshed/src/com/google/gwt/sample/tree/client/TreeView.java
@@ -15,16 +15,20 @@
  */
 package com.google.gwt.sample.tree.client;
 
+import com.google.gwt.animation.client.Animation;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Style.Display;
 import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
 import com.google.gwt.resources.client.ClientBundle;
 import com.google.gwt.resources.client.ImageResource;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.AbstractImagePrototype;
+import com.google.gwt.user.client.ui.HasAnimation;
 import com.google.gwt.user.client.ui.Widget;
 
 import java.util.ArrayList;
@@ -32,7 +36,7 @@
 /**
  * A view of a tree.
  */
-public class TreeView extends Widget {
+public class TreeView extends Widget implements HasAnimation {
 
   private static final Resources DEFAULT_RESOURCES = GWT.create(Resources.class);
 
@@ -53,11 +57,239 @@
   }
 
   /**
+   * The animation used for {@link TreeNodeView}.
+   */
+  public abstract static class TreeNodeAnimation 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;
+
+    /**
+     * Not instantiable.
+     */
+    private TreeNodeAnimation() {
+    }
+
+    /**
+     * Get the duration of animations in milliseconds.
+     * 
+     * @return the animation duration
+     */
+    public int getDuration() {
+      return duration;
+    }
+
+    /**
+     * Set the animation duration in milliseconds.
+     * 
+     * @param duration the duration
+     */
+    public void setDuration(int duration) {
+      this.duration = duration;
+    }
+
+    /**
+     * Animate a {@link TreeNodeView} into its new state.
+     * 
+     * @param node the {@link TreeNodeView} to animate
+     * @param isAnimationEnabled true to animate
+     */
+    abstract void animate(TreeNodeView<?> node, boolean isAnimationEnabled);
+  }
+
+  /**
+   * A {@link TreeNodeAnimation} that reveals the contents of child nodes.
+   */
+  public static class RevealAnimation extends TreeNodeAnimation {
+
+    /**
+     * Create a new {@link RevealAnimation}.
+     * 
+     * @return the new animation
+     */
+    public static RevealAnimation create() {
+      return new RevealAnimation();
+    }
+
+    /**
+     * The container that holds the child container.
+     */
+    Element animFrame;
+    /**
+     * The container that holds the children.
+     */
+    Element childContainer;
+
+    /**
+     * The target height when opening, the start height when closing.
+     */
+    int height;
+    /**
+     * True if the node is opening, false if closing.
+     */
+    boolean opening;
+
+    /**
+     * Not instantiable.
+     */
+    private RevealAnimation() {
+    }
+
+    @Override
+    protected void onComplete() {
+      cleanup();
+    }
+
+    @Override
+    protected void onStart() {
+      if (opening) {
+        animFrame.getStyle().setHeight(1.0, Unit.PX);
+        animFrame.getStyle().clearDisplay();
+        height = childContainer.getScrollHeight();
+      } else {
+        height = childContainer.getOffsetHeight();
+      }
+    }
+
+    @Override
+    protected void onUpdate(double progress) {
+      if (opening) {
+        double curHeight = progress * height;
+        animFrame.getStyle().setHeight(curHeight, Unit.PX);
+      } else {
+        double curHeight = (1.0 - progress) * height;
+        animFrame.getStyle().setHeight(curHeight, Unit.PX);
+      }
+    }
+
+    /**
+     * Animate a {@link TreeNodeView} into its new state.
+     * 
+     * @param node the {@link TreeNodeView} to animate
+     * @param isAnimationEnabled true to animate
+     */
+    @Override
+    void animate(TreeNodeView<?> node, boolean isAnimationEnabled) {
+      // Cancel any pending animations.
+      cancel();
+
+      // Initialize the fields.
+      this.opening = node.getState();
+      childContainer = node.getChildContainer();
+      animFrame = childContainer.getParentElement();
+
+      if (isAnimationEnabled) {
+        // Animated.
+        int duration = getDuration();
+        int childCount = childContainer.getChildCount();
+        if (childCount < 4) {
+          // Reduce the duration if there are less than four items or it will
+          // look really slow.
+          duration = (int) ((childCount / 4.0) * duration);
+        }
+        run(duration);
+      } else {
+        // Non animated.
+        cleanup();
+      }
+    }
+
+    /**
+     * Put the node back into a clean state and clear fields.
+     */
+    private void cleanup() {
+      if (opening) {
+        animFrame.getStyle().clearDisplay();
+      } else {
+        animFrame.getStyle().setDisplay(Display.NONE);
+        childContainer.setInnerHTML("");
+      }
+      animFrame.getStyle().clearHeight();
+      this.childContainer = null;
+      this.animFrame = null;
+    }
+  }
+
+  /**
+   * A {@link TreeNodeAnimation} that slides children into view.
+   */
+  public static class SlideAnimation extends RevealAnimation {
+    /**
+     * Create a new {@link RevealAnimation}.
+     * 
+     * @return the new animation
+     */
+    public static SlideAnimation create() {
+      return new SlideAnimation();
+    }
+
+    /**
+     * Not instantiable.
+     */
+    private SlideAnimation() {
+    }
+
+    @Override
+    protected void onComplete() {
+      childContainer.getStyle().clearPosition();
+      childContainer.getStyle().clearTop();
+      childContainer.getStyle().clearWidth();
+      super.onComplete();
+    }
+
+    @Override
+    protected void onStart() {
+      super.onStart();
+      if (opening) {
+        childContainer.getStyle().setTop(-height, Unit.PX);
+      } else {
+        childContainer.getStyle().setTop(0, Unit.PX);
+      }
+      childContainer.getStyle().setPosition(Position.RELATIVE);
+    }
+
+    @Override
+    protected void onUpdate(double progress) {
+      super.onUpdate(progress);
+      if (opening) {
+        double curTop = (1.0 - progress) * -height;
+        childContainer.getStyle().setTop(curTop, Unit.PX);
+      } else {
+        double curTop = progress * -height;
+        childContainer.getStyle().setTop(curTop, Unit.PX);
+      }
+    }
+  }
+
+  /**
+   * We use one animation for the entire {@link TreeView}.
+   */
+  private TreeNodeAnimation animation = SlideAnimation.create();
+
+  /**
    * 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 {@link TreeViewModel} that backs the tree.
    */
   private TreeViewModel model;
@@ -88,7 +320,6 @@
     this.model = viewModel;
     this.resources = DEFAULT_RESOURCES;
     setElement(Document.get().createDivElement());
-    getElement().getStyle().setPosition(Position.RELATIVE);
     setStyleName("gwt-TreeView");
 
     // Add event handlers.
@@ -96,14 +327,38 @@
 
     // Associate a view with the item.
     rootNode = new TreeNodeView<T>(this, null, null, getElement(), rootValue);
-    rootNode.initChildContainer(getElement());
     rootNode.setState(true);
   }
 
+  /**
+   * Get the animation used to open and close nodes in this tree if animations
+   * are enabled.
+   * 
+   * @return the animation
+   * @see #isAnimationEnabled()
+   */
+  public TreeNodeAnimation getAnimation() {
+    return animation;
+  }
+
+  /**
+   * Get the HTML string that is displayed while nodes wait for their children
+   * to load.
+   * 
+   * @return the loading HTML string
+   */
+  public String getLoadingHtml() {
+    return loadingHtml;
+  }
+
   public TreeViewModel getTreeViewModel() {
     return model;
   }
 
+  public boolean isAnimationEnabled() {
+    return isAnimationEnabled;
+  }
+
   @Override
   public void onBrowserEvent(Event event) {
     super.onBrowserEvent(event);
@@ -121,6 +376,35 @@
   }
 
   /**
+   * Set the animation used to open and close nodes in this tree. You must call
+   * {@link #setAnimationEnabled(boolean)} to enable or disable animation.
+   * 
+   * @param animation the animation
+   * @see #setAnimationEnabled(boolean)
+   */
+  public void setAnimation(TreeNodeAnimation 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() {
@@ -155,6 +439,15 @@
   }
 
   /**
+   * Animate the current state of a {@link TreeNodeView} in this tree.
+   * 
+   * @param node the node to animate
+   */
+  void maybeAnimateTreeNode(TreeNodeView<?> node) {
+    animation.animate(node, isAnimationEnabled);
+  }
+
+  /**
    * Collects parents going up the element tree, terminated at the tree root.
    */
   private void collectElementChain(ArrayList<Element> chain, Element hRoot,
diff --git a/bikeshed/src/com/google/gwt/sample/tree/server/Dictionary.java b/bikeshed/src/com/google/gwt/sample/tree/server/Dictionary.java
index a1872ee..0a1b0eb 100644
--- a/bikeshed/src/com/google/gwt/sample/tree/server/Dictionary.java
+++ b/bikeshed/src/com/google/gwt/sample/tree/server/Dictionary.java
@@ -341,15 +341,15 @@
       "amplitudes", "amply", "ampoules", "amps", "ampule", "ampules", "ampuls",
       "amputate", "amputated", "amputating", "amputation", "amputations",
       "amputee", "amputees", "amuck", "amulet", "amulets", "amuse", "amused",
-      "amusement", "amusements", "amuses", "amusing", "amusingly"
-  };
-  
+      "amusement", "amusements", "amuses", "amusing", "amusingly", "bad",
+      "beard", "before"};
+
   public static List<String> getNext(String prefix) {
     String[] s = new String[26];
     boolean[] b = new boolean[26];
-    
+
     List<String> toRet = new ArrayList<String>();
-    
+
     // slowwww..
     for (String word : words) {
       if (word.equals(prefix)) {
@@ -364,7 +364,7 @@
         }
       }
     }
-    
+
     for (int i = 0; i < 26; i++) {
       if (s[i] != null) {
         toRet.add(s[i]);