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]);