com.google.gwt.user.cellview.client.CellTree.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.user.cellview.client.CellTree.java

Source

/*
 * 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.user.cellview.client;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

import com.google.gwt.animation.client.Animation;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.BrowserEvents;
import com.google.gwt.dom.client.Element;
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.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.CssResource.ImportedWithPrefix;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.resources.client.ImageResource.ImageOptions;
import com.google.gwt.resources.client.ImageResource.RepeatStyle;
import com.google.gwt.safecss.shared.SafeStyles;
import com.google.gwt.safecss.shared.SafeStylesBuilder;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.safehtml.client.SafeHtmlTemplates.Template;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.cellview.client.CellTreeNodeView.TreeNodeImpl;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.AbstractImagePrototype;
import com.google.gwt.user.client.ui.Focusable;
import com.google.gwt.user.client.ui.HasAnimation;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.view.client.TreeViewModel;

/**
 * A view of a tree.
 *
 * <p>
 * This widget will <em>only</em> work in standards mode, which requires that
 * the HTML page in which it is run have an explicit &lt;!DOCTYPE&gt;
 * declaration.
 * </p>
 *
 * <p>
 * <h3>Examples</h3>
 * <dl>
 * <dt>Trivial example</dt>
 * <dd>{@example com.google.gwt.examples.cellview.CellTreeExample}</dd>
 * <dt>Complex example</dt>
 * <dd>{@example com.google.gwt.examples.cellview.CellTreeExample2}</dd>
 * </dl>
 */
public class CellTree extends AbstractCellTree implements HasAnimation, Focusable {
    /**
     * The default number of children to show under a tree node.
     */
    private static final int DEFAULT_LIST_SIZE = 25;

    private static Resources DEFAULT_RESOURCES;

    private static Template template;

    private static Resources getDefaultResources() {
        if (DEFAULT_RESOURCES == null) {
            DEFAULT_RESOURCES = GWT.create(Resources.class);
        }
        return DEFAULT_RESOURCES;
    }

    /**
     * A boolean indicating whether or not a cell is being edited.
     */
    boolean cellIsEditing;

    /**
     * A boolean indicating that the widget has focus.
     */
    boolean isFocused;

    /**
     * Set to true while the elements are being refreshed. Events are ignored
     * during this time.
     */
    boolean isRefreshing;

    /**
     * The hidden root node in the tree. Visible for testing.
     */
    final CellTreeNodeView<?> rootNode;

    private char accessKey = 0;

    /**
     * The animation.
     */
    private NodeAnimation animation;

    /**
     * The HTML used to generate the closed image.
     */
    private final SafeHtml closedImageHtml;

    /**
     * The HTML used to generate the closed image for the top items.
     */
    private final SafeHtml closedImageTopHtml;

    /**
     * The default number of children to display under each node.
     */
    private int defaultNodeSize = DEFAULT_LIST_SIZE;

    /**
     * The maximum width of the open and closed images.
     */
    private final int imageWidth;

    /**
     * Indicates whether or not animations are enabled.
     */
    private boolean isAnimationEnabled;

    /**
     * The {@link CellTreeNodeView} whose children are currently being selected
     * using the keyboard.
     */
    private CellTreeNodeView<?> keyboardSelectedNode;

    /**
     * The HTML used to generate the loading image.
     */
    private final SafeHtml loadingImageHtml;

    /**
     * The HTML used to generate the open image.
     */
    private final SafeHtml openImageHtml;

    /**
     * The HTML used to generate the open image for the top items.
     */
    private final SafeHtml openImageTopHtml;

    /**
     * The styles used by this widget.
     */
    private final Style style;

    private int tabIndex;

    /**
     * Construct a new {@link CellTree}.
     *
     * @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> CellTree(TreeViewModel viewModel, T rootValue) {
        this(viewModel, rootValue, getDefaultResources());
    }

    /**
     * Construct a new {@link CellTree}.
     *
     * @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 resources
     *            the resources used to render the tree
     */
    public <T> CellTree(TreeViewModel viewModel, T rootValue, Resources resources) {
        super(viewModel);
        if (template == null) {
            template = GWT.create(Template.class);
        }
        this.style = resources.cellTreeStyle();
        this.style.ensureInjected();
        initWidget(new SimplePanel());
        setStyleName(this.style.cellTreeWidget());
        // Initialize the open and close images strings.
        ImageResource treeOpen = resources.cellTreeOpenItem();
        ImageResource treeClosed = resources.cellTreeClosedItem();
        ImageResource treeLoading = resources.cellTreeLoading();
        openImageHtml = getImageHtml(treeOpen, false);
        closedImageHtml = getImageHtml(treeClosed, false);
        openImageTopHtml = getImageHtml(treeOpen, true);
        closedImageTopHtml = getImageHtml(treeClosed, true);
        loadingImageHtml = getImageHtml(treeLoading, false);
        imageWidth = Math.max(Math.max(treeOpen.getWidth(), treeClosed.getWidth()), treeLoading.getWidth());
        // We use one animation for the entire tree.
        setAnimation(SlideAnimation.create());
        // Add event handlers.
        Set<String> eventTypes = new HashSet<String>();
        eventTypes.add(BrowserEvents.FOCUS);
        eventTypes.add(BrowserEvents.BLUR);
        eventTypes.add(BrowserEvents.KEYDOWN);
        eventTypes.add(BrowserEvents.KEYUP);
        eventTypes.add(BrowserEvents.MOUSEDOWN);
        eventTypes.add(BrowserEvents.CLICK);
        CellBasedWidgetImpl.get().sinkEvents(this, eventTypes);
        // Associate a view with the item.
        CellTreeNodeView<T> root = new CellTreeNodeView<T>(this, null, null, getElement(), rootValue);
        keyboardSelectedNode = rootNode = root;
        root.setOpen(true, false);
    }

    /**
     * Get the animation used to open and close nodes in this tree if animations
     * are enabled.
     *
     * @return the animation
     * @see #isAnimationEnabled()
     * @see #setAnimation(NodeAnimation)
     */
    public NodeAnimation getAnimation() {
        return animation;
    }

    /**
     * Get the default maximum number of children to display under each tree
     * node.
     *
     * @return the default node size
     * @see #setDefaultNodeSize(int)
     */
    public int getDefaultNodeSize() {
        return defaultNodeSize;
    }

    @Override
    public TreeNode getRootTreeNode() {
        return rootNode.getTreeNode();
    }

    public int getTabIndex() {
        return tabIndex;
    }

    public Element getTreeNodeElement(TreeNode node) {
        if (node instanceof TreeNodeImpl) {
            return ((TreeNodeImpl) node).nodeView.getElement();
        } else {
            return null;
        }
    }

    public boolean isAnimationEnabled() {
        return isAnimationEnabled;
    }

    @Override
    public void onBrowserEvent(Event event) {
        CellBasedWidgetImpl.get().onBrowserEvent(this, event);
        if (isRefreshing) {
            // Ignore spurious events (onblur) while replacing elements.
            return;
        }
        super.onBrowserEvent(event);
        String eventType = event.getType();
        if (BrowserEvents.FOCUS.equals(eventType)) {
            // Remember the focus state.
            isFocused = true;
            onFocus();
        } else if (BrowserEvents.BLUR.equals(eventType)) {
            // Remember the blur state.
            isFocused = false;
            onBlur();
        } else if (BrowserEvents.KEYDOWN.equals(eventType) && !cellIsEditing) {
            int keyCode = event.getKeyCode();
            switch (keyCode) {
            // Handle keyboard navigation.
            case KeyCodes.KEY_DOWN:
            case KeyCodes.KEY_UP:
            case KeyCodes.KEY_RIGHT:
            case KeyCodes.KEY_LEFT:
                handleKeyNavigation(keyCode);
                // Prevent scrollbars from scrolling.
                event.preventDefault();
                return;
            case 32:
                // Prevent scrollbars from scrolling.
                event.preventDefault();
            }
        }
        final Element target = event.getEventTarget().cast();
        ArrayList<Element> chain = new ArrayList<Element>();
        collectElementChain(chain, getElement(), target);
        final boolean isMouseDown = BrowserEvents.MOUSEDOWN.equals(eventType);
        final boolean isClick = BrowserEvents.CLICK.equals(eventType);
        final CellTreeNodeView<?> nodeView = findItemByChain(chain, 0, rootNode);
        if (nodeView != null) {
            if (isMouseDown) {
                Element showMoreElem = nodeView.getShowMoreElement();
                if (nodeView.getImageElement().isOrHasChild(target)) {
                    // Open the node when the open image is clicked.
                    nodeView.setOpen(!nodeView.isOpen(), true);
                    return;
                } else if (showMoreElem != null && showMoreElem.isOrHasChild(target)) {
                    // Show more rows when clicked.
                    nodeView.showMore();
                    return;
                }
            }
            // Forward the event to the cell
            if (nodeView != rootNode && nodeView.getSelectionElement().isOrHasChild(target)) {
                // Move the keyboard focus to the clicked item.
                if (isClick) {
                    /*
                     * If the selected element is natively focusable, then we do
                     * not want to steal focus away from it.
                     */
                    boolean isFocusable = CellBasedWidgetImpl.get().isFocusable(target);
                    isFocused = isFocused || isFocusable;
                    keyboardSelect(nodeView, !isFocusable);
                }
                nodeView.fireEventToCell(event);
            }
        }
    }

    /**
     * {@inheritDoc}
     *
     * <p>
     * Setting the key to (int) 0 will disable the access key.
     * </p>
     *
     * @see #getAccessKey()
     */
    public void setAccessKey(char key) {
        this.accessKey = key;
        keyboardSelectedNode.setKeyboardSelected(true, false);
    }

    /**
     * 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)
     * @see #getAnimation()
     */
    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 default number of children to display beneath each child node. If
     * more nodes are available, a button will appear at the end of the list
     * allowing the user to show more items. Changing this value will not affect
     * tree nodes that are already open.
     *
     * @param defaultNodeSize
     *            the max
     * @see #getDefaultNodeSize()
     */
    public void setDefaultNodeSize(int defaultNodeSize) {
        this.defaultNodeSize = defaultNodeSize;
    }

    public void setFocus(boolean focused) {
        keyboardSelectedNode.setKeyboardSelected(true, true);
    }

    public void setTabIndex(int index) {
        this.tabIndex = index;
        keyboardSelectedNode.setKeyboardSelected(true, false);
    }

    /**
     * Collects parents going up the element tree, terminated at the tree root.
     */
    private void collectElementChain(ArrayList<Element> chain, Element hRoot, Element hElem) {
        if ((hElem == null) || (hElem == hRoot)) {
            return;
        }
        collectElementChain(chain, hRoot, hElem.getParentElement());
        chain.add(hElem);
    }

    private CellTreeNodeView<?> findItemByChain(ArrayList<Element> chain, int idx, CellTreeNodeView<?> parent) {
        if (idx == chain.size()) {
            return parent;
        }
        Element hCurElem = chain.get(idx);
        for (int i = 0, n = parent.getChildCount(); i < n; ++i) {
            CellTreeNodeView<?> child = parent.getChildNode(i);
            if (child.getElement() == hCurElem) {
                CellTreeNodeView<?> retItem = findItemByChain(chain, idx + 1, child);
                if (retItem == null) {
                    return child;
                }
                return retItem;
            }
        }
        return findItemByChain(chain, idx + 1, parent);
    }

    /**
     * Get the HTML representation of an image.
     *
     * @param res
     *            the {@link ImageResource} to render as HTML
     * @param isTop
     *            true if the image is for a top level element.
     * @return the rendered HTML
     */
    private SafeHtml getImageHtml(ImageResource res, boolean isTop) {
        // Build the classes.
        StringBuilder classesBuilder = new StringBuilder(style.cellTreeItemImage());
        if (isTop) {
            classesBuilder.append(" ").append(style.cellTreeTopItemImage());
        }
        // Build the css.
        SafeStylesBuilder cssBuilder = new SafeStylesBuilder();
        if (LocaleInfo.getCurrentLocale().isRTL()) {
            cssBuilder.appendTrustedString("right: 0px;");
        } else {
            cssBuilder.appendTrustedString("left: 0px;");
        }
        cssBuilder.appendTrustedString("width: " + res.getWidth() + "px;");
        cssBuilder.appendTrustedString("height: " + res.getHeight() + "px;");
        AbstractImagePrototype proto = AbstractImagePrototype.create(res);
        SafeHtml image = SafeHtmlUtils.fromTrustedString(proto.getHTML());
        return template.imageWrapper(classesBuilder.toString(), cssBuilder.toSafeStyles(), image);
    }

    /**
     * Handle keyboard navigation.
     *
     * @param keyCode
     *            the key code that was pressed
     */
    private void handleKeyNavigation(int keyCode) {
        CellTreeNodeView<?> parent = keyboardSelectedNode.getParentNode();
        int parentChildCount = (parent == null) ? 0 : parent.getChildCount();
        int index = keyboardSelectedNode.getIndex();
        int childCount = keyboardSelectedNode.getChildCount();
        switch (keyCode) {
        case KeyCodes.KEY_DOWN:
            if (keyboardSelectedNode.isOpen() && childCount > 0) {
                // Select first child.
                keyboardSelect(keyboardSelectedNode.getChildNode(0), true);
            } else if (index < parentChildCount - 1) {
                // Next sibling.
                keyboardSelect(parent.getChildNode(index + 1), true);
            } else {
                // Next available sibling of parent hierarchy.
                CellTreeNodeView<?> curParent = parent;
                CellTreeNodeView<?> nextSibling = null;
                while (curParent != null && curParent != rootNode) {
                    CellTreeNodeView<?> grandparent = curParent.getParentNode();
                    if (grandparent == null) {
                        break;
                    }
                    int curParentIndex = grandparent.indexOf(curParent);
                    if (curParentIndex < grandparent.getChildCount() - 1) {
                        nextSibling = grandparent.getChildNode(curParentIndex + 1);
                        break;
                    }
                    curParent = grandparent;
                }
                if (nextSibling != null) {
                    keyboardSelect(nextSibling, true);
                }
            }
            break;
        case KeyCodes.KEY_UP:
            if (index > 0) {
                // Deepest node of previous sibling hierarchy.
                CellTreeNodeView<?> prevSibling = parent.getChildNode(index - 1);
                if (prevSibling.isOpen() && prevSibling.getChildCount() > 0) {
                    prevSibling = prevSibling.getChildNode(prevSibling.getChildCount() - 1);
                }
                keyboardSelect(prevSibling, true);
            } else if (parent != null && parent != rootNode) {
                // Parent.
                keyboardSelect(parent, true);
            }
            break;
        case KeyCodes.KEY_RIGHT:
            if (LocaleInfo.getCurrentLocale().isRTL()) {
                keyboardNavigateShallow();
            } else {
                keyboardNavigateDeep();
            }
            break;
        case KeyCodes.KEY_LEFT:
            if (LocaleInfo.getCurrentLocale().isRTL()) {
                keyboardNavigateDeep();
            } else {
                keyboardNavigateShallow();
            }
            break;
        }
    }

    /**
     * Navigate to a deeper node. If the node is closed, open it. If it is open,
     * move to the first child.
     */
    private void keyboardNavigateDeep() {
        if (!keyboardSelectedNode.isLeaf()) {
            boolean isOpen = keyboardSelectedNode.isOpen();
            if (isOpen && keyboardSelectedNode.getChildCount() > 0) {
                // First child.
                keyboardSelect(keyboardSelectedNode.getChildNode(0), true);
            } else if (!isOpen) {
                // Open the node.
                keyboardSelectedNode.setOpen(true, true);
            }
        }
    }

    /**
     * Navigate to a shallower node. If the node is open, close it. If it is
     * closed, move to the parent.
     */
    private void keyboardNavigateShallow() {
        CellTreeNodeView<?> parent = keyboardSelectedNode.getParentNode();
        if (keyboardSelectedNode.isOpen()) {
            // Close the node.
            keyboardSelectedNode.setOpen(false, true);
        } else if (parent != null && parent != rootNode) {
            // Select the parent.
            keyboardSelect(parent, true);
        }
    }

    /**
     * Get the access key.
     *
     * @return the access key, or -1 if not set
     * @see #setAccessKey(char)
     */
    protected char getAccessKey() {
        return accessKey;
    }

    /**
     * Called when the keyboard selected node loses focus.
     */
    protected void onBlur() {
        keyboardSelectedNode.setKeyboardSelectedStyle(false);
    }

    /**
     * Called when the keyboard selected node gains focus.
     */
    protected void onFocus() {
        keyboardSelectedNode.setKeyboardSelectedStyle(true);
    }

    /**
     * Cancel a pending animation.
     */
    void cancelTreeNodeAnimation() {
        animation.cancel();
    }

    /**
     * Get the HTML to render the closed image.
     *
     * @param isTop
     *            true if the top element, false if not
     * @return the HTML string
     */
    SafeHtml getClosedImageHtml(boolean isTop) {
        return isTop ? closedImageTopHtml : closedImageHtml;
    }

    /**
     * Get the width required for the images.
     *
     * @return the maximum width required for images.
     */
    int getImageWidth() {
        return imageWidth;
    }

    /**
     * Return the node that has keyboard selection.
     */
    CellTreeNodeView<?> getKeyboardSelectedNode() {
        return keyboardSelectedNode;
    }

    /**
     * Return the HTML to render the loading image.
     */
    SafeHtml getLoadingImageHtml() {
        return loadingImageHtml;
    }

    /**
     * Get the HTML to render the open image.
     *
     * @param isTop
     *            true if the top element, false if not
     * @return the HTML string
     */
    SafeHtml getOpenImageHtml(boolean isTop) {
        return isTop ? openImageTopHtml : openImageHtml;
    }

    /**
     * Return the Style used by the tree.
     */
    Style getStyle() {
        return style;
    }

    /**
     * Select a node using the keyboard.
     *
     * @param node
     *            the new node to select
     * @param stealFocus
     *            true to steal focus, false not to
     */
    void keyboardSelect(CellTreeNodeView<?> node, boolean stealFocus) {
        if (isKeyboardSelectionDisabled()) {
            return;
        }
        // Deselect the old node if it not destroyed.
        if (keyboardSelectedNode != null && !keyboardSelectedNode.isDestroyed()) {
            keyboardSelectedNode.setKeyboardSelected(false, false);
        }
        keyboardSelectedNode = node;
        keyboardSelectedNode.setKeyboardSelected(true, stealFocus);
    }

    /**
     * Animate the current state of a {@link CellTreeNodeView} in this tree.
     *
     * @param node
     *            the node to animate
     */
    void maybeAnimateTreeNode(CellTreeNodeView<?> node) {
        if (animation != null) {
            animation.animate(node, node.consumeAnimate() && isAnimationEnabled() && !node.isRootNode());
        }
    }

    /**
     * If this widget has focus, reset it.
     */
    void resetFocus() {
        CellBasedWidgetImpl.get().resetFocus(new Scheduler.ScheduledCommand() {
            public void execute() {
                if (isFocused && !keyboardSelectedNode.isDestroyed() && !keyboardSelectedNode.resetFocusOnCell()) {
                    keyboardSelectedNode.setKeyboardSelected(true, true);
                }
            }
        });
    }

    /**
     * Resources that match the GWT standard style theme.
     */
    public interface BasicResources extends Resources {
        @ImageOptions(flipRtl = true)
        ImageResource cellTreeClosedItem();

        @ImageOptions(flipRtl = true)
        @Source("cellTreeLoadingBasic.gif")
        ImageResource cellTreeLoading();

        @ImageOptions(flipRtl = true)
        ImageResource cellTreeOpenItem();

        /**
         * The styles used in this widget.
         */
        @Source(BasicStyle.DEFAULT_CSS)
        BasicStyle cellTreeStyle();
    }

    /**
     * A node animation.
     */
    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 in milliseconds.
         */
        private int duration = DEFAULT_ANIMATION_DURATION;

        NodeAnimation() {
        }

        /**
         * Return the duration of the animation in milliseconds.
         *
         * @see #setDuration(int)
         */
        public int getDuration() {
            return duration;
        }

        /**
         * Set the duration of the animation in milliseconds.
         *
         * @param duration
         *            the duration in milliseconds
         * @see #getDuration()
         */
        public void setDuration(int duration) {
            this.duration = duration;
        }

        /**
         * Animate a tree node into its new state.
         *
         * @param node
         *            the node to animate
         * @param isAnimationEnabled
         *            true to animate
         */
        abstract void animate(CellTreeNodeView<?> node, boolean isAnimationEnabled);
    }

    /**
     * A ClientBundle that provides images for this widget.
     */
    public interface Resources extends ClientBundle {
        /**
         * An image indicating a closed branch.
         */
        @ImageOptions(flipRtl = true)
        @Source("cellTreeClosedArrow.png")
        ImageResource cellTreeClosedItem();

        /**
         * An image indicating that a node is loading.
         */
        @ImageOptions(flipRtl = true)
        ImageResource cellTreeLoading();

        /**
         * An image indicating an open branch.
         */
        @ImageOptions(flipRtl = true)
        @Source("cellTreeOpenArrow.png")
        ImageResource cellTreeOpenItem();

        /**
         * The background used for selected items.
         */
        @ImageOptions(repeatStyle = RepeatStyle.Horizontal, flipRtl = true)
        ImageResource cellTreeSelectedBackground();

        /**
         * The styles used in this widget.
         */
        @Source(Style.DEFAULT_CSS)
        Style cellTreeStyle();
    }

    /**
     * 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() {
            return new RevealAnimation();
        }

        /**
         * The container that holds the content, includind the children.
         */
        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() {
        }

        /**
         * 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();
            animFrame.getStyle().clearPosition();
            this.contentContainer = null;
            this.childContainer = null;
            this.animFrame = null;
        }

        @Override
        protected void onComplete() {
            cleanup();
        }

        @Override
        protected void onStart() {
            if (opening) {
                animFrame.getStyle().setHeight(1.0, Unit.PX);
                animFrame.getStyle().setPosition(Position.RELATIVE);
                animFrame.getStyle().clearDisplay();
                height = contentContainer.getScrollHeight();
            } else {
                height = contentContainer.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);
            }
            // Remind IE6 that we want the overflow to be hidden.
            animFrame.getStyle().setOverflow(Overflow.HIDDEN);
            animFrame.getStyle().setPosition(Position.RELATIVE);
        }

        /**
         * Animate a {@link CellTreeNodeView} into its new state.
         *
         * @param node
         *            the {@link CellTreeNodeView} to animate
         * @param isAnimationEnabled
         *            true to animate
         */
        @Override
        void animate(CellTreeNodeView<?> node, boolean isAnimationEnabled) {
            // Cancel any pending animations.
            cancel();
            // Initialize the fields.
            this.opening = node.isOpen();
            animFrame = node.ensureAnimationFrame();
            contentContainer = node.ensureContentContainer();
            childContainer = node.ensureChildContainer();
            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();
            }
        }
    }

    /**
     * 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() {
            return new SlideAnimation();
        }

        /**
         * Not instantiable.
         */
        private SlideAnimation() {
        }

        @Override
        protected void onComplete() {
            contentContainer.getStyle().clearPosition();
            contentContainer.getStyle().clearTop();
            contentContainer.getStyle().clearWidth();
            super.onComplete();
        }

        @Override
        protected void onStart() {
            super.onStart();
            if (opening) {
                contentContainer.getStyle().setTop(-height, Unit.PX);
            } else {
                contentContainer.getStyle().setTop(0, Unit.PX);
            }
            contentContainer.getStyle().setPosition(Position.RELATIVE);
        }

        @Override
        protected void onUpdate(double progress) {
            super.onUpdate(progress);
            if (opening) {
                double curTop = (1.0 - progress) * -height;
                contentContainer.getStyle().setTop(curTop, Unit.PX);
            } else {
                double curTop = progress * -height;
                contentContainer.getStyle().setTop(curTop, Unit.PX);
            }
        }
    }

    /**
     * Styles used by this widget.
     */
    @ImportedWithPrefix("gwt-CellTree")
    public interface Style extends CssResource {
        /**
         * The path to the default CSS styles used by this resource.
         */
        String DEFAULT_CSS = "com/google/gwt/user/cellview/client/CellTree.css";

        /**
         * Applied to the empty message.
         */
        String cellTreeEmptyMessage();

        /**
         * Applied to tree items.
         */
        String cellTreeItem();

        /**
         * Applied to open/close icon.
         */
        String cellTreeItemImage();

        /**
         * Applied to the wrapper around the image and value.
         */
        String cellTreeItemImageValue();

        /**
         * Applied to the value in an item.
         */
        String cellTreeItemValue();

        /**
         * Applied to the keyboard selected item.
         */
        String cellTreeKeyboardSelectedItem();

        /**
         * Applied to open tree items.
         */
        String cellTreeOpenItem();

        /**
         * Applied to selected tree items.
         */
        String cellTreeSelectedItem();

        /**
         * Applied to the show more button.
         */
        String cellTreeShowMoreButton();

        /**
         * Applied to top level items.
         */
        String cellTreeTopItem();

        /**
         * Applied to open/close icon at the top level.
         */
        String cellTreeTopItemImage();

        /**
         * Applied to the wrapper around the image and value of top level items.
         */
        String cellTreeTopItemImageValue();

        /**
         * Applied to the widget.
         */
        String cellTreeWidget();
    }

    /**
     * Styles used by {@link BasicResources}.
     */
    @ImportedWithPrefix("gwt-CellTree")
    interface BasicStyle extends Style {
        /**
         * The path to the default CSS styles used by this resource.
         */
        String DEFAULT_CSS = "com/google/gwt/user/cellview/client/CellTreeBasic.css";
    }

    interface Template extends SafeHtmlTemplates {
        @Template("<div class=\"{0}\" style=\"{1}position:absolute;\">{2}</div>")
        SafeHtml imageWrapper(String classes, SafeStyles cssLayout, SafeHtml image);
    }
}