com.extjs.gxt.ui.client.widget.treepanel.TreePanel.java Source code

Java tutorial

Introduction

Here is the source code for com.extjs.gxt.ui.client.widget.treepanel.TreePanel.java

Source

/*
 * Sencha GXT 2.3.1 - Sencha for GWT
 * Copyright(c) 2007-2013, Sencha, Inc.
 * licensing@sencha.com
 * 
 * http://www.sencha.com/products/gxt/license/
 */
package com.extjs.gxt.ui.client.widget.treepanel;

import com.extjs.gxt.ui.client.GXT;
import com.extjs.gxt.ui.client.aria.FocusFrame;
import com.extjs.gxt.ui.client.core.DomHelper;
import com.extjs.gxt.ui.client.core.El;
import com.extjs.gxt.ui.client.core.FastMap;
import com.extjs.gxt.ui.client.core.XDOM;
import com.extjs.gxt.ui.client.data.*;
import com.extjs.gxt.ui.client.event.*;
import com.extjs.gxt.ui.client.store.StoreEvent;
import com.extjs.gxt.ui.client.store.StoreListener;
import com.extjs.gxt.ui.client.store.TreeStore;
import com.extjs.gxt.ui.client.store.TreeStoreEvent;
import com.extjs.gxt.ui.client.util.DelayedTask;
import com.extjs.gxt.ui.client.util.KeyNav;
import com.extjs.gxt.ui.client.util.Rectangle;
import com.extjs.gxt.ui.client.util.SafeGxt;
import com.extjs.gxt.ui.client.widget.BoxComponent;
import com.extjs.gxt.ui.client.widget.treepanel.TreePanelView.TreeViewRenderMode;
import com.google.gwt.dom.client.Document;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.AbstractImagePrototype;
import com.google.gwt.user.client.ui.Accessibility;
import com.google.gwt.user.client.ui.impl.FocusImpl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * A hierarchical tree widget bound directly to a @link {@link TreeStore}.
 * <code>TreePanel</code> contains no child widgets, rather, the tree is
 * rendered based on the models contained in the tree store. Once bound, the
 * tree will remain in sync with the bound tree store.
 * 
 * <p />
 * The text of each node can be specified in a couple of different ways. First,
 * a display property can be set using {@link #setDisplayProperty(String)}. The
 * is useful when the item's text is contained within the model's data. Second,
 * a model string provider can be specified using
 * {@link #setLabelProvider(ModelStringProvider)}.
 * 
 * <p />
 * With state enabled, TreePanel will save and restore the expand state of the
 * nodes in the tree. A <code>ModelKeyProvider</code> must specified with the
 * <code>TreeStore</code> this tree is bound to. Save and restore works with
 * both local, and asynchronous loading of children.
 * 
 * <dl>
 * <dt><b>Events:</b></dt>
 * 
 * <dd><b>BeforeExpand</b> : TreePanelEvent(treePanel, item)<br>
 * <div>Fires before a node is expanded. Listeners can cancel the action by
 * calling {@link BaseEvent#setCancelled(boolean)}.</div>
 * <ul>
 * <li>treePanel : the source tree</li>
 * <li>item : this</li>
 * </ul>
 * </dd>
 * 
 * <dd><b>BeforeCollapse</b> : TreePanelEvent(treePanel, item)<br>
 * <div>Fires before a node is collapsed. Listeners can cancel the action by
 * calling {@link BaseEvent#setCancelled(boolean)}.</div>
 * <ul>
 * <li>treePanel : the source tree</li>
 * <li>item : this</li>
 * </ul>
 * </dd>
 * 
 * <dd><b>Expand</b> : TreePanelEvent(treePanel, item)<br>
 * <div>Fires after a node has been expanded.</div>
 * <ul>
 * <li>treePanel : the source tree</li>
 * <li>item : this</li>
 * </ul>
 * </dd>
 * 
 * <dd><b>Collapse</b> : TreePanelEvent(treePanel, item)<br>
 * <div>Fires after a node is collapsed.</div>
 * <ul>
 * <li>treePanel : the source tree</li>
 * <li>item : this</li>
 * </ul>
 * </dd>
 * 
 * <dd><b>BeforeCheckChange</b> : TreePanelEvent(treePanel, item)<br>
 * <div>Fires before a node's check state is changed. Listeners can cancel the
 * action by calling {@link BaseEvent#setCancelled(boolean)}.</div>
 * <ul>
 * <li>treePanel : the source tree</li>
 * <li>item : this</li>
 * <li>checked : the checked state.</li>
 * </ul>
 * </dd>
 * 
 * <dd><b>CheckChange</b> : TreePanelEvent(treePanel, item)<br>
 * <div>Fires after a item's check state changes.</div>
 * <ul>
 * <li>treePanel : the source tree</li>
 * <li>item : this</li>
 * <li>checked : the checked state.</li>
 * </ul>
 * </dd>
 * 
 * <dd><b>CheckChanged</b> : CheckChangeEvent(provider, checkedeSelection)<br>
 * <div>Fires after the tree's overall checked state changes.</div>
 * <ul>
 * <li>provider : the source tree</li>
 * <li>checkedSelection : the tree checked item</li>
 * </ul>
 * </dd>
 * 
 * </dl>
 * 
 * @param <M> the model type
 */
public class TreePanel<M extends ModelData> extends BoxComponent implements CheckProvider<M> {

    /**
     * Check cascade enum.
     */
    public enum CheckCascade {
        /**
         * Checks cascade to all child nodes.
         */
        CHILDREN,
        /**
         * Checks to not cascade.
         */
        NONE,
        /**
         * Checks cascade to all parent nodes.
         */
        PARENTS;
    }

    /**
     * Check nodes enum.
     */
    public enum CheckNodes {
        /**
         * Check boxes for both leafs and parent nodes.
         */
        BOTH,
        /**
         * Check boxes for only leaf nodes.
         */
        LEAF,
        /**
         * Check boxes for only parent nodes.
         */
        PARENT;
    }

    /**
     * Joint enum.
     */
    public enum Joint {
        COLLAPSED(1), EXPANDED(2), NONE(0);

        private int value;

        private Joint(int value) {
            this.value = value;
        }

        public int value() {
            return value;
        }
    }

    /**
     * Maintains the internal state of nodes contained in the tree. Should not
     * need to be referenced in typical usage.
     */
    public class TreeNode {

        protected Element element, container, joint, check, text, icon, elContainer;
        protected String id;
        protected M m;

        private boolean childrenRendered;
        private boolean expand, checked, expandDeep;
        private boolean expanded;
        private boolean leaf = true;
        private boolean loaded;
        private boolean loading;

        TreeNode(String id, M m) {
            this.id = id;
            this.m = m;
        }

        public void clearElements() {
            joint = null;
            check = null;
            icon = null;
            text = null;
        }

        public Element getElement() {
            if (element == null) {
                element = (Element) Document.get().getElementById(id);
            }
            return element;
        }

        public int getItemCount() {
            return store.getChildCount(m);
        }

        public M getModel() {
            return m;
        }

        public TreeNode getParent() {
            M p = store.getParent(m);
            return findNode(p);
        }

        public int indexOf(TreeNode child) {
            M c = child.getModel();
            return store.indexOf(c);
        }

        public boolean isExpanded() {
            return expanded;
        }

        public boolean isLeaf() {
            return !hasChildren(m);
        }

        public void setExpanded(boolean expand) {
            TreePanel.this.setExpanded(m, expand);
        }

        public void setLeaf(boolean leaf) {
            this.leaf = leaf;
            TreePanel.this.refresh(m);
        }
    }

    protected Map<M, String> cache;
    protected String displayProperty;
    protected boolean filtering;
    protected String itemSelector = ".x-tree3-node";
    protected TreeLoader<M> loader;
    protected Map<String, TreeNode> nodes = new FastMap<TreeNode>();
    protected TreePanelSelectionModel<M> sm;
    protected TreeStore<M> store;
    protected boolean focusConstrainScheduled = false;

    protected TreePanelView<M> view = new TreePanelView<M>();
    private boolean autoExpand;
    private boolean autoLoad;
    private boolean autoSelect;
    private boolean caching = true;
    private boolean checkable;
    private CheckNodes checkNodes = CheckNodes.BOTH;
    private CheckCascade checkStyle = CheckCascade.PARENTS;
    private boolean expandOnFilter = true;
    private ModelIconProvider<M> iconProvider;
    private ModelStringProvider<M> labelProvider;
    private ModelProcessor<M> modelProcessor;
    private StoreListener<M> storeListener = new StoreListener<M>() {
        @Override
        public void storeAdd(StoreEvent<M> se) {
            onAdd((TreeStoreEvent<M>) se);
        }

        @Override
        public void storeClear(StoreEvent<M> se) {
            onClear((TreeStoreEvent<M>) se);
        }

        @Override
        public void storeDataChanged(StoreEvent<M> se) {
            onDataChanged((TreeStoreEvent<M>) se);
        }

        @Override
        public void storeFilter(StoreEvent<M> se) {
            onFilter((TreeStoreEvent<M>) se);
        }

        @Override
        public void storeRemove(StoreEvent<M> se) {
            onRemove((TreeStoreEvent<M>) se);
        }

        @Override
        public void storeUpdate(StoreEvent<M> se) {
            onUpdate((TreeStoreEvent<M>) se);
        }
    };
    private TreeStyle style = new TreeStyle();
    private boolean trackMouseOver = true;
    private DelayedTask updateTask, cleanTask;
    private Boolean useKeyProvider = null;

    protected final FocusImpl focusImpl = FocusImpl.getFocusImplForPanel();

    protected El focusEl;

    /**
     * Creates a new tree panel.
     * 
     * @param store the tree store
     */
    public TreePanel(TreeStore<M> store) {
        this.store = store;
        this.loader = store.getLoader();

        store.addStoreListener(storeListener);

        baseStyle = "x-tree3";
        setSelectionModel(new TreePanelSelectionModel<M>());
        view.bind(this, store);
    }

    public void addCheckListener(CheckChangedListener<M> listener) {
        addListener(Events.CheckChanged, listener);
    }

    /**
     * Collapses all nodes.
     */
    public void collapseAll() {
        for (M child : store.getRootItems()) {
            setExpanded(child, false, true);
        }
    }

    /**
     * Expands all nodes.
     */
    public void expandAll() {
        for (M child : store.getRootItems()) {
            setExpanded(child, true, true);
        }
    }

    /**
     * Returns the tree node for the given target.
     * 
     * @param target the target element
     * @return the tree node or null if no match
     */
    public TreeNode findNode(Element target) {
        Element item = fly(target).findParentElement(itemSelector, 10);
        if (item != null) {
            String id = item.getId();
            TreeNode node = nodes.get(id);
            return node;
        }
        return null;
    }

    @Override
    public void focus() {
        focusImpl.focus(focusEl.dom);
    }

    public List<M> getCheckedSelection() {
        List<M> checked = new ArrayList<M>();
        for (TreeNode n : nodes.values()) {
            if (n.checked) {
                checked.add(n.m);
            }
        }
        return checked;
    }

    /**
     * Returns the child nodes value which determines what node types have a check
     * box. Only applies when check boxes have been enabled (
     * {@link #setCheckable(boolean)}.
     * 
     * @return the child nodes value
     */
    public CheckNodes getCheckNodes() {
        return checkNodes;
    }

    /**
     * The check cascade style value which determines if check box changes cascade
     * to parent and children.
     * 
     * @return the check cascade style
     */
    public CheckCascade getCheckStyle() {
        return checkStyle;
    }

    /**
     * Returns the display property.
     * 
     * @return the display property
     */
    public String getDisplayProperty() {
        return displayProperty;
    }

    /**
     * Returns the model icon provider.
     * 
     * @return the icon provider
     */
    public ModelIconProvider<M> getIconProvider() {
        return iconProvider;
    }

    /**
     * Returns the model processor.
     * 
     * @return the model processor
     */
    public ModelProcessor<M> getModelProcessor() {
        return modelProcessor;
    }

    /**
     * Returns the tree's selection model.
     * 
     * @return the selection model
     */
    public TreePanelSelectionModel<M> getSelectionModel() {
        return sm;
    }

    /**
     * Returns the tree's store.
     * 
     * @return the store
     */
    public TreeStore<M> getStore() {
        return store;
    }

    /**
     * Returns the tree style.
     * 
     * @return the tree style
     */
    public TreeStyle getStyle() {
        return style;
    }

    /**
     * Returns the tree's view.
     * 
     * @return the view
     */
    public TreePanelView<M> getView() {
        return view;
    }

    /**
     * Returns true if auto expand is enabled.
     * 
     * @return the auto expand state
     */
    public boolean isAutoExpand() {
        return autoExpand;
    }

    /**
     * Returns true if auto load is enabled.
     * 
     * @return the auto load state
     */
    public boolean isAutoLoad() {
        return autoLoad;
    }

    /**
     * Returns true if select on load is enabled.
     * 
     * @return the auto select state
     */
    public boolean isAutoSelect() {
        return autoSelect;
    }

    /**
     * Returns true when a loader is queried for it's children each time a node is
     * expanded. Only applies when using a loader with the tree store.
     * 
     * @return true if caching
     */
    public boolean isCaching() {
        return caching;
    }

    /**
     * Returns true if check boxes are enabled.
     * 
     * @return the check box state
     */
    public boolean isCheckable() {
        return checkable;
    }

    public boolean isChecked(M model) {
        TreeNode node = findNode(model);
        if (node != null && isCheckable(node)) {
            return node.checked;
        }
        return false;
    }

    /**
     * Returns true if the model is expanded.
     * 
     * @param model the model
     * @return true if expanded
     */
    public boolean isExpanded(M model) {
        TreeNode node = findNode(model);
        return node.expanded;
    }

    /**
     * Returns the if expand all and collapse all is enabled on filter changes.
     * 
     * @return the expand all collapse all state
     */
    public boolean isExpandOnFilter() {
        return expandOnFilter;
    }

    /**
     * Returns true if the model is a leaf node. The leaf state allows a tree item
     * to specify if it has children before the children have been realized.
     * 
     * @param model the model
     * @return the leaf state
     */
    public boolean isLeaf(M model) {
        TreeNode node = findNode(model);
        return node != null && node.isLeaf();
    }

    /**
     * Returns true if nodes are highlighted on mouse over.
     * 
     * @return true if enabled
     */
    public boolean isTrackMouseOver() {
        return trackMouseOver;
    }

    @Override
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public void onComponentEvent(ComponentEvent ce) {
        super.onComponentEvent(ce);
        TreePanelEvent<M> tpe = (TreePanelEvent) ce;

        int type = ce.getEventTypeInt();
        switch (type) {
        case Event.ONCLICK:
            onClick(tpe);
            break;
        case Event.ONDBLCLICK:
            onDoubleClick(tpe);
            break;
        case Event.ONSCROLL:
            onScroll(tpe);
            break;
        case Event.ONFOCUS:
            onFocus(ce);
            break;
        }

        view.onEvent(tpe);
    }

    @Override
    public void recalculate() {
        super.recalculate();
        update();
    }

    public void removeCheckListener(CheckChangedListener<M> listener) {
        removeListener(Events.CheckChanged, listener);
    }

    /**
     * Scrolls the tree to ensure the given model is visible.
     * 
     * @param model the model to scroll into view
     */
    public void scrollIntoView(M model) {
        TreeNode node = findNode(model);
        if (node != null) {
            Element c = getView().getElementContainer(node);
            if (c != null) {
                scrollIntoView(c);
            }
        }
    }

    /**
     * If set to true, all non leaf nodes will be expanded automatically (defaults
     * to false).
     * 
     * @param autoExpand the auto expand state to set.
     */
    public void setAutoExpand(boolean autoExpand) {
        this.autoExpand = autoExpand;
    }

    /**
     * Sets whether all children should automatically be loaded recursively
     * (defaults to false). Useful when the tree must be fully populated when
     * initially rendered.
     * 
     * @param autoLoad true to auto load
     */
    public void setAutoLoad(boolean autoLoad) {
        this.autoLoad = autoLoad;
    }

    /**
     * True to select the first model after the store's data changes (defaults to
     * false).
     * 
     * @param autoSelect true to auto select
     */
    public void setAutoSelect(boolean autoSelect) {
        this.autoSelect = autoSelect;
    }

    /**
     * Sets whether the children should be cached after first being retrieved from
     * the store (defaults to true). When <code>false</code>, a load request will
     * be made each time a node is expanded.
     * 
     * @param caching the caching state
     */
    public void setCaching(boolean caching) {
        this.caching = caching;
    }

    /**
     * Sets whether check boxes are used in the tree.
     * 
     * @param checkable true for check boxes
     */
    public void setCheckable(boolean checkable) {
        this.checkable = checkable;
    }

    /**
     * Sets the check state of the item. The checked state will only be set for
     * nodes that have been rendered, {@link #setAutoLoad(boolean)} can be used to
     * render all children.
     * 
     * @param item the item
     * @param checked true for checked
     */
    public void setChecked(M item, boolean checked) {
        if (!checkable)
            return;
        TreeNode node = findNode(item);
        if (node != null) {
            if (node.checked == checked) {
                onCheckCascade(item, checked);
                return;
            }

            boolean leaf = node.isLeaf();
            if ((!leaf && checkNodes == CheckNodes.LEAF) || (leaf && checkNodes == CheckNodes.PARENT)) {
                return;
            }

            TreePanelEvent<M> evt = new TreePanelEvent<M>(this, item);
            evt.setChecked(checked);

            if (fireEvent(Events.BeforeCheckChange, evt)) {
                node.checked = checked;

                view.onCheckChange(node, checkable, checked);

                fireEvent(Events.CheckChange, evt);

                CheckChangedEvent<M> cce = new CheckChangedEvent<M>(this, (M) null);
                fireEvent(Events.CheckChanged, cce);

                onCheckCascade(item, checked);
            }
        }
    }

    public void setCheckedSelection(List<M> selection) {
        for (M m : store.getAllItems()) {
            setChecked(m, selection != null && selection.contains(m));
        }
    }

    /**
     * Sets which tree items will display a check box (defaults to BOTH).
     * <p>
     * Valid values are:
     * <ul>
     * <li>BOTH - both nodes and leafs</li>
     * <li>PARENT - only nodes with children</li>
     * <li>LEAF - only leafs</li>
     * </ul>
     * 
     * @param checkNodes the child nodes value
     */
    public void setCheckNodes(CheckNodes checkNodes) {
        this.checkNodes = checkNodes;
        if (rendered) {
            for (M m : store.getAllItems()) {
                refresh(m);
            }
        }
    }

    /**
     * Sets the cascading behavior for check tree (defaults to PARENTS). When
     * using CHILDREN, it is important to note that the cascade will only be
     * applied to rendered nodes. {@link #setAutoLoad(boolean)} can be used to
     * fully render the tree on render.
     * <p>
     * Valid values are:
     * <ul>
     * <li>NONE - no cascading</li>
     * <li>PARENTS - cascade to parents</li>
     * <li>CHILDREN - cascade to children</li>
     * </ul>
     * 
     * @param checkStyle the child style
     */
    public void setCheckStyle(CheckCascade checkStyle) {
        this.checkStyle = checkStyle;
    }

    /**
     * Sets the display property name used to the item's text. As an alternative,
     * a <code>ModelStringProvider</code> can be specified using
     * {@link #setLabelProvider(ModelStringProvider)}.
     * 
     * @param displayProperty the property name
     */
    public void setDisplayProperty(String displayProperty) {
        this.displayProperty = displayProperty;
    }

    /**
     * Sets the item's expand state.
     * 
     * @param model the model
     * @param expand true to expand
     */
    public void setExpanded(M model, boolean expand) {
        setExpanded(model, expand, false);
    }

    /**
     * Sets the item's expand state.
     * 
     * @param model the model
     * @param expand true to expand
     * @param deep true to expand all children recursively
     */
    public void setExpanded(M model, boolean expand, boolean deep) {
        if (expand) {
            // make parents visible
            List<M> list = new ArrayList<M>();
            M p = model;
            while ((p = store.getParent(p)) != null) {
                TreeNode n = findNode(p);
                if (n != null && !n.isExpanded()) {
                    list.add(p);
                }
            }
            for (int i = list.size() - 1; i >= 0; i--) {
                M item = list.get(i);
                setExpanded(item, expand, false);
            }
        }
        TreeNode node = findNode(model);
        if (node != null) {
            if (!rendered) {
                node.expand = expand;
                return;
            }
            if (expand) {
                onExpand(model, node, deep);
            } else {
                onCollapse(model, node, deep);
            }
        }
    }

    /**
     * Sets whether the tree should expand all and collapse all when filters are
     * applied (defaults to true).
     * 
     * @param expandOnFilter true to expand and collapse on filter changes
     */
    public void setExpandOnFilter(boolean expandOnFilter) {
        this.expandOnFilter = expandOnFilter;
    }

    /**
     * Sets the tree's model icon provider which provides the icon style for each
     * model.
     * 
     * @param iconProvider the icon provider
     */
    public void setIconProvider(ModelIconProvider<M> iconProvider) {
        this.iconProvider = iconProvider;
    }

    /**
     * Sets the tree's model string provider for the text description of each
     * node. If a a display property has been specified, it will be passed to the
     * string provider. If a property has not been specified, <code>null</code>
     * will be passed.
     * 
     * @param labelProvider the label provider
     */
    public void setLabelProvider(ModelStringProvider<M> labelProvider) {
        this.labelProvider = labelProvider;
    }

    /**
     * Sets the item's leaf state. The leaf state allows control of the expand
     * icon before the children have been realized.
     * 
     * @param model the model
     * @param leaf the leaf state
     */
    public void setLeaf(M model, boolean leaf) {
        TreeNode t = findNode(model);
        if (t != null) {
            t.setLeaf(leaf);
        }
    }

    /**
     * Sets the treepanels's model processor.
     * 
     * @see ModelProcessor
     * @param modelProcessor
     */
    public void setModelProcessor(ModelProcessor<M> modelProcessor) {
        this.modelProcessor = modelProcessor;
    }

    /**
     * Sets the tree's selection model.
     * 
     * @param sm the selection model
     */
    public void setSelectionModel(TreePanelSelectionModel<M> sm) {
        if (this.sm != null) {
            this.sm.bindTree(null);
        }
        this.sm = sm;
        if (sm != null) {
            sm.bindTree(this);
        }
    }

    /**
     * Sets the tree style.
     * 
     * @param style the tree style
     */
    public void setStyle(TreeStyle style) {
        this.style = style;
    }

    /**
     * True to highlight nodes when the mouse is over (defaults to true).
     * 
     * @param trackMouseOver true to highlight nodes on mouse over
     */
    public void setTrackMouseOver(boolean trackMouseOver) {
        this.trackMouseOver = trackMouseOver;
    }

    /**
     * Sets the tree's view. Only needs to be called when customizing the tree's
     * presentation.
     * 
     * @param view the view
     */
    public void setView(TreePanelView<M> view) {
        this.view = view;
        view.bind(this, store);
    }

    /**
     * Toggles the model's expand state.
     * 
     * @param model the model
     */
    public void toggle(M model) {
        TreeNode node = findNode(model);
        if (node != null) {
            setExpanded(model, !node.expanded);
        }
    }

    protected Joint calcualteJoint(M model) {
        if (model == null) {
            return Joint.NONE;
        }
        TreeNode node = findNode(model);
        return node.isLeaf() ? Joint.NONE : node.expanded ? Joint.EXPANDED : Joint.COLLAPSED;
    }

    protected AbstractImagePrototype calculateIconStyle(M model) {
        AbstractImagePrototype style = null;
        if (iconProvider != null) {
            AbstractImagePrototype iconStyle = iconProvider.getIcon(model);
            if (iconStyle != null) {
                return iconStyle;
            }
        }
        TreeNode node = findNode(model);
        TreeStyle ts = getStyle();
        if (!node.isLeaf()) {
            if (isExpanded(model)) {
                style = ts.getNodeOpenIcon();
            } else {
                style = ts.getNodeCloseIcon();
            }
        } else {
            style = ts.getLeafIcon();
        }
        return style;
    }

    protected void clean() {
        if (cleanTask == null) {
            cleanTask = new DelayedTask(new Listener<BaseEvent>() {
                public void handleEvent(BaseEvent be) {
                    doClean();
                }
            });
        }
        cleanTask.delay(view.getCleanDelay());
    }

    protected void clear() {
        fly(getContainer(null)).removeChildren();
        nodes.clear();
        if (cache != null) {
            cache.clear();
        }
        if (isAttached()) {
            moveFocus(getContainer(null));
        }
    }

    @Override
    protected ComponentEvent createComponentEvent(Event event) {
        TreePanelEvent<M> tpe = new TreePanelEvent<M>(this, event);
        return tpe;
    }

    protected void doClean() {
        int count = getVisibleRowCount();
        if (count > 0) {
            List<M> rows = getChildModel(store.getRootItems(), true);
            int[] vr = getVisibleRows(rows, count);
            vr[0] -= view.getCacheSize();
            vr[1] += view.getCacheSize();

            int i = 0;

            // if first is less than 0, all rows have been rendered
            // so lets clean the end...
            if (vr[0] <= 0) {
                i = vr[1] + 1;
            }
            for (int len = rows.size(); i < len; i++) {
                // if current row is outside of first and last and
                // has content, update the innerHTML to nothing
                if (i < vr[0] || i > vr[1]) {
                    cleanNode(findNode(rows.get(i)));
                }
            }
        }

    }

    protected void doUpdate() {
        int count = getVisibleRowCount();
        if (count > 0) {
            List<M> rootItems = store.getRootItems();
            List<M> visible = getChildModel(rootItems, true);
            int[] vr = getVisibleRows(visible, count);

            for (int i = vr[0]; i <= vr[1]; i++) {
                if (!isRowRendered(i, visible)) {
                    M parent = store.getParent(visible.get(i));
                    String html = renderChild(parent, visible.get(i), store.getDepth(parent),
                            TreeViewRenderMode.BODY);
                    findNode(visible.get(i)).getElement().getFirstChildElement().setInnerHTML(html);
                }
            }
            clean();
        }

    }

    protected TreeNode findNode(M m) {
        if (m == null || useKeyProvider == null)
            return null;
        return nodes.get(useKeyProvider ? generateModelId(m) : cache.get(m));
    }

    protected String generateModelId(M m) {
        return getId() + "_"
                + (store.getKeyProvider() != null ? store.getKeyProvider().getKey(m) : XDOM.getUniqueId());
    }

    protected List<M> getChildModel(List<M> l, boolean onlyVisible) {
        List<M> list = new ArrayList<M>();
        for (M m : l) {
            list.add(m);
            if (!onlyVisible || findNode(m).isExpanded()) {
                findChildren(m, list, onlyVisible);
            }
        }
        return list;
    }

    protected Element getContainer(M model) {
        if (model == null) {
            if (getElement().getFirstChildElement() != null
                    && getElement().getFirstChildElement().getTagName().equals("TABLE")) {
                return fly((Element) getElement()).selectNode("td").dom;
            } else {
                return getElement();
            }
        }
        TreeNode node = findNode(model);
        if (node != null) {
            return view.getContainer(node);
        }
        return null;
    }

    protected SafeHtml getText(M model) {
        if (labelProvider != null) {
            return labelProvider.getStringValue(model, displayProperty);
        } else if (displayProperty != null) {
            return SafeGxt.fromNullableString((String) model.get(displayProperty));
        }
        return SafeHtmlUtils.EMPTY_SAFE_HTML;
    }

    protected int getVisibleRowCount() {
        int rh = view.getCalculatedRowHeight();
        int visibleHeight = el().getHeight(true);
        return (int) ((visibleHeight < 1) ? 0 : Math.ceil(visibleHeight / rh));
    }

    protected boolean hasChildren(M model) {
        TreeNode node = findNode(model);
        if (loader != null && !node.loaded) {
            return loader.hasChildren(node.m);
        }
        if (!node.leaf || store.hasChildren(node.m)) {
            return true;
        }
        return false;
    }

    protected boolean isCheckable(TreeNode node) {
        boolean leaf = node.isLeaf();
        boolean check = checkable;
        switch (checkNodes) {
        case LEAF:
            if (!leaf) {
                check = false;
            }
            break;
        case PARENT:
            if (leaf) {
                check = false;
            }
        }
        return check;
    }

    protected void moveFocus(Element selectedElem) {
        if (selectedElem == null)
            return;
        int containerLeft = getAbsoluteLeft();
        int containerTop = getAbsoluteTop();

        int left = selectedElem.getAbsoluteLeft() - containerLeft;
        int top = selectedElem.getAbsoluteTop() - containerTop;

        int width = selectedElem.getOffsetWidth();
        int height = selectedElem.getOffsetHeight();

        if (width == 0 || height == 0) {
            focusEl.setLeftTop(0, 0);
            return;
        }
        focusEl.setLeftTop(left, top);
    }

    @Override
    protected void notifyShow() {
        super.notifyShow();
        update();
    }

    protected void onAdd(TreeStoreEvent<M> se) {
        for (M child : se.getChildren()) {
            register(child);
        }
        if (rendered) {
            M parent = se.getParent();
            TreeNode pn = findNode(parent);
            if (parent == null || (pn != null && pn.childrenRendered)) {
                StringBuilder sb = new StringBuilder();

                for (M child : se.getChildren()) {
                    sb.append(renderChild(parent, child, store.getDepth(parent), TreeViewRenderMode.MAIN));
                }
                int index = se.getIndex();
                if (index == 0) {
                    DomHelper.insertFirst(getContainer(parent), sb.toString());
                } else if (index == store.getChildCount(parent) - se.getChildren().size()) {
                    DomHelper.insertHtml("beforeEnd", getContainer(parent), sb.toString());
                } else {
                    DomHelper.insertBefore(fly(getContainer(parent)).getChild(index).dom, sb.toString());
                }

            }
            refresh(parent);
            update();
        }
    }

    @Override
    protected void onAttach() {
        super.onAttach();
        update();
    }

    protected void onCheckCascade(M model, boolean checked) {
        switch (getCheckStyle()) {
        case PARENTS:
            if (checked) {
                M p = store.getParent(model);
                while (p != null) {
                    setChecked(p, true);
                    p = store.getParent(p);
                }
            } else {
                for (M child : store.getChildren(model)) {
                    setChecked(child, false);
                }
            }
            break;
        case CHILDREN:
            for (M child : store.getChildren(model)) {
                setChecked(child, checked);
            }
        }
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected void onCheckClick(TreePanelEvent tpe, TreeNode node) {
        tpe.stopEvent();
        setChecked((M) tpe.getItem(), !node.checked);
    }

    protected void onClear(TreeStoreEvent<M> se) {
        clear();
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected void onClick(TreePanelEvent tpe) {
        TreeNode node = tpe.getNode();
        if (node != null) {
            Element jointEl = view.getJointElement(node);
            if (jointEl != null && tpe.within(jointEl)) {
                toggle((M) tpe.getItem());
            } else if (GXT.isHighContrastMode) {
                Rectangle r = El.fly(jointEl).getBounds();
                if (r.contains(tpe.getClientX(), tpe.getClientY())) {
                    toggle((M) tpe.getItem());
                }
            }
            Element checkEl = view.getCheckElement(node);
            if (checkable && checkEl != null && tpe.within(checkEl)) {
                onCheckClick(tpe, node);
            }

            focusEl.setXY(tpe.getEvent().getClientX(), tpe.getEvent().getClientY());
            focus();
        }
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected void onCollapse(M model, TreeNode node, boolean deep) {
        TreePanelEvent<M> tpe = new TreePanelEvent<M>(this);
        tpe.setItem(model);
        tpe.setNode(node);
        if (node.expanded && fireEvent(Events.BeforeCollapse, tpe)) {
            node.expanded = false;
            // collapse
            view.collapse(node);

            if (isStateful() && store.getKeyProvider() != null) {
                Map<String, Object> state = getState();
                List<String> expanded = (List) state.get("expanded");
                String id = store.getKeyProvider().getKey(model);
                if (expanded != null && expanded.contains(id)) {
                    expanded.remove(id);
                    saveState();
                }
            }

            List<M> l = new ArrayList<M>();
            l.add(node.m);

            update();
            cleanCollapsed(node.m);

            moveFocus(node.getElement());
            fireEvent(Events.Collapse, tpe);
        }
        if (deep) {
            setExpandChildren(model, false);
        }

    }

    protected void onDataChanged(TreeStoreEvent<M> se) {
        if (!isRendered()) {
            return;
        }

        M p = se.getParent();
        if (p == null) {
            clear();
            renderChildren(null);

            if (autoSelect) {
                M m = store.getChild(0);
                if (m != null) {
                    List<M> sel = new ArrayList<M>();
                    sel.add(m);
                    getSelectionModel().setSelection(sel);
                }
            }
            statefulExpand(store.getRootItems());
        } else {
            TreeNode n = findNode(p);
            n.loaded = true;
            n.loading = false;
            if (n.childrenRendered) {
                getContainer(p).setInnerHTML("");
            }

            renderChildren(p);

            if (n.expand && !n.isLeaf()) {
                n.expand = false;
                boolean c = caching;
                caching = true;
                boolean deep = n.expandDeep;
                n.expandDeep = false;
                setExpanded(p, true, deep);
                caching = c;
            } else {
                refresh(p);
            }
        }
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected void onDoubleClick(TreePanelEvent tpe) {
        TreeNode node = tpe.getNode();
        if (node != null) {
            setExpanded(node.m, !node.expanded);
        }
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected void onExpand(M model, TreeNode node, boolean deep) {
        TreePanelEvent<M> tpe = new TreePanelEvent<M>(this);
        tpe.setItem(model);
        tpe.setNode(node);

        if (!node.isLeaf()) {
            // if we are loading, ignore it
            if (node.loading) {
                return;
            }
            // if we have a loader and node is not loaded make
            // load request and exit method
            if (!node.expanded && loader != null && (!node.loaded || !caching) && !filtering) {
                store.removeAll(model);
                node.expand = true;
                node.expandDeep = deep;
                node.loading = true;
                view.onLoading(node);
                loader.loadChildren(model);
                return;
            }
            if (!node.expanded && fireEvent(Events.BeforeExpand, tpe)) {
                node.expanded = true;

                if (!node.childrenRendered) {
                    renderChildren(model);
                    node.childrenRendered = true;
                }
                // expand
                view.expand(node);

                if (isStateful() && store.getKeyProvider() != null) {
                    Map<String, Object> state = getState();
                    List<String> expanded = (List) state.get("expanded");
                    if (expanded == null) {
                        expanded = new ArrayList<String>();
                        state.put("expanded", expanded);
                    }
                    String id = store.getKeyProvider().getKey(model);
                    if (!expanded.contains(id)) {
                        expanded.add(id);
                        saveState();
                    }
                }

                update();
                fireEvent(Events.Expand, tpe);
            }

            if (deep) {
                setExpandChildren(model, true);
            } else {
                statefulExpand(store.getChildren(model));
            }
        }

    }

    protected void onFilter(TreeStoreEvent<M> se) {
        if (isRendered()) {
            filtering = store.isFiltered();
            clear();
            renderChildren(null);

            if (expandOnFilter && store.isFiltered()) {
                expandAll();
            }
            update();
        }
    }

    protected void onFocus(ComponentEvent ce) {
        FocusFrame.get().frame(this);
    }

    protected void onRemove(TreeStoreEvent<M> se) {
        TreeNode node = findNode(se.getChild());
        if (node != null) {
            if (node.getElement() != null) {
                El.fly(node.getElement()).removeFromParent();
            }
            unregister(se.getChild());
            for (M child : se.getChildren()) {
                unregister(child);
            }
            TreeNode p = findNode(se.getParent());
            if (p != null && p.expanded && p.getItemCount() == 0) {
                setExpanded(p.m, false);
            } else if (p != null && p.getItemCount() == 0) {
                refresh(se.getParent());
            }
        }
    }

    @Override
    protected void onRender(Element target, int index) {
        super.onRender(target, index);
        String s = view.getTemplate(null, null, null, null, false, false, null, 0, TreeViewRenderMode.CONTAINER);
        setElement(XDOM.create(s), target, index);

        el().show();
        el().setStyleAttribute("overflow", "auto");

        if ((GXT.isIE6 || GXT.isIE7) && GXT.isStrict) {
            el().makePositionable();
        }

        el().setTabIndex(0);
        el().setElementAttribute("hideFocus", "true");

        if (store.getRootItems().size() == 0 && loader != null) {
            loader.load();
        } else {
            renderChildren(null);
            if (autoSelect) {
                getSelectionModel().select(0, false);
            }
            statefulExpand(store.getRootItems());
        }

        if (GXT.isFocusManagerEnabled()) {
            Accessibility.setRole(getElement(), Accessibility.ROLE_TREE);
            new KeyNav<ComponentEvent>(this) {
                @Override
                public void onDown(ComponentEvent ce) {
                    if (sm.getSelectedItems().size() == 0 && store.getRootItems().size() > 0) {
                        sm.select(store.getRootItems().get(0), false);
                    }
                }
            };
        } else {
            // JAWS does not work when disabling text selection
            disableTextSelection(true);
        }

        ensureFocusElement();

        sinkEvents(Event.ONFOCUS | Event.ONSCROLL | Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS
                | Event.KEYEVENTS);
    }

    protected void onResize(int width, int height) {
        super.onResize(width, height);
        update();
    }

    protected void onScroll(TreePanelEvent<M> tpe) {
        update();
    }

    protected void onUpdate(TreeStoreEvent<M> se) {
        refresh(se.getModel());
    }

    protected M prepareData(M model) {
        if (modelProcessor != null) {
            boolean silent = false;
            if (model instanceof BaseModel) {
                silent = ((BaseModel) model).isSilent();
                ((BaseModel) model).setSilent(true);
            }

            M m = modelProcessor.prepareData(model);

            if (model instanceof BaseModel) {
                ((BaseModel) model).setSilent(silent);
            }

            return m;
        }
        return model;
    }

    protected void refresh(M model) {
        if (rendered) {
            TreeNode node = findNode(model);
            if (node != null && node.getElement() != null) {
                view.onIconStyleChange(node, calculateIconStyle(model));
                view.onJointChange(node, calcualteJoint(model));
                view.onTextChange(node, getText(model));
                boolean checkable = isCheckable(node);
                setChecked(node.m, node.checked && checkable);
            }
        }
    }

    protected String register(M m) {
        if (useKeyProvider == null) {
            if (store.getKeyProvider() == null) {
                useKeyProvider = false;
            } else {
                useKeyProvider = true;
            }
        }
        if (!useKeyProvider) {
            if (cache == null) {
                cache = new HashMap<M, String>();
            }
            String id = cache.get(m);
            if (id == null) {
                id = generateModelId(m);
                cache.put(m, id);
                nodes.put(id, new TreeNode(id, m));
            }
            return id;
        }
        String id = generateModelId(m);
        if (!nodes.containsKey(id)) {
            nodes.put(id, new TreeNode(id, m));
        }
        return id;
    }

    protected String renderChild(M parent, M child, int depth, TreeViewRenderMode renderMode) {
        child = prepareData(child);
        String id = register(child);
        TreeNode node = findNode(child);
        return view.getTemplate(child, id, getText(child), calculateIconStyle(child), isCheckable(node),
                node.checked, calcualteJoint(child), depth, renderMode);
    }

    protected void renderChildren(M parent) {
        StringBuilder markup = new StringBuilder();
        int depth = store.getDepth(parent);
        List<M> children = parent == null ? store.getRootItems() : store.getChildren(parent);
        if (children.size() == 0) {
            return;
        }

        for (M child : children) {
            register(child);
        }
        for (int i = 0; i < children.size(); i++) {
            markup.append(renderChild(parent, children.get(i), depth, TreeViewRenderMode.MAIN));
        }
        Element container = getContainer(parent);
        container.setInnerHTML(markup.toString());

        for (int i = 0; i < children.size(); i++) {
            M child = children.get(i);
            TreeNode node = findNode(child);

            if (autoExpand) {
                setExpanded(child, true);
            } else if (node.expand && !node.isLeaf()) {
                node.expand = false;
                setExpanded(child, true);
            } else if (loader != null) {
                if (autoLoad) {
                    if (store.isFiltered()) {
                        renderChildren(child);
                    } else {
                        if (loader.hasChildren(child)) {
                            loader.loadChildren(child);
                        }
                    }
                }
            } else if (autoLoad) {
                renderChildren(child);
            }
        }

        TreeNode n = findNode(parent);
        if (n != null) {
            onCheckCascade(n.m, n.checked);
            n.childrenRendered = true;
        }
        if (parent == null) {
            ensureFocusElement();
        }
        update();
    }

    protected void unregister(M m) {
        if (m != null && useKeyProvider != null) {
            TreeNode n = null;
            if (useKeyProvider) {
                n = nodes.remove(generateModelId(m));
            } else {
                n = nodes.remove(cache.remove(m));
            }
            if (n != null) {
                n.clearElements();
            }
        }
    }

    protected void update() {
        if (updateTask == null) {
            updateTask = new DelayedTask(new Listener<BaseEvent>() {
                public void handleEvent(BaseEvent be) {
                    doUpdate();
                }
            });
        }
        updateTask.delay(view.getScrollDelay());
    }

    private void cleanCollapsed(M parent) {
        List<M> list = store.getChildren(parent, true);
        for (M m : list) {
            TreeNode node = findNode(m);
            if (node != null && node.element != null) {
                cleanNode(node);
            }
        }
    }

    private void cleanNode(TreeNode node) {
        if (node != null && node.element != null) {
            node.clearElements();
            fly((Element) node.getElement().getFirstChildElement()).removeChildren();
        }
    }

    private void ensureFocusElement() {
        if (focusEl != null) {
            focusEl.removeFromParent();
        }
        focusEl = new El(DOM.asOld(focusImpl.createFocusable()));
        focusEl.dom.getStyle().setProperty("outline", "none");
        getElement().appendChild(focusEl.dom);
        if (focusEl.dom.hasChildNodes()) {
            focusEl.dom.getFirstChildElement().getStyle().setProperty("outline", "none");
            com.google.gwt.dom.client.Style focusElStyle = focusEl.dom.getFirstChildElement().getStyle();
            focusElStyle.setProperty("borderWidth", "0px");
            focusElStyle.setProperty("fontSize", "1px");
            focusElStyle.setPropertyPx("lineHeight", 1);
        }
        focusEl.setLeft(0);
        focusEl.setTop(0);
        focusEl.makePositionable(true);
        focusEl.addEventsSunk(Event.FOCUSEVENTS);
    }

    private void findChildren(M parent, List<M> list, boolean onlyVisible) {
        for (M child : store.getChildren(parent)) {
            list.add(child);
            if (!onlyVisible || findNode(child).isExpanded()) {
                findChildren(child, list, onlyVisible);
            }
        }
    }

    private int[] getVisibleRows(List<M> visible, int count) {
        int sc = el().getScrollTop();
        int start = (int) (sc == 0 ? 0 : Math.floor(sc / view.getCalculatedRowHeight()) - 1);
        int first = Math.max(start, 0);
        int last = Math.min(start + count + 2, visible.size() - 1);
        return new int[] { first, last };
    }

    private boolean isRowRendered(int i, List<M> visible) {
        Element e = findNode(visible.get(i)).getElement();
        return e != null && e.getFirstChild().hasChildNodes();
    }

    /**
     * {@link El#scrollIntoView(Element, boolean)} does not handle the new tree
     * structure that includes a inner table element with auto height. The GWT
     * {@link Element#scrollIntoView()} version does handle this use case
     * properly. However, GWT also scrolls horizontally as needed. We do not want
     * this behavior and it is not configurable. Given that, this code is a copy
     * paste from GWT with the horizontal / left calculations removed.
     * El.scrollIntoView is not being updated to ensure we do not break any
     * existing calls to the method.
     * 
     * @param elem the element to scroll into view
     */
    private native void scrollIntoView(Element elem) /*-{
                                                     var top = elem.offsetTop;
                                                     var width = elem.offsetWidth, height = elem.offsetHeight;
                                                         
                                                     if (elem.parentNode != elem.offsetParent) {
                                                     top -= elem.parentNode.offsetTop;
                                                     }
                                                         
                                                     var cur = elem.parentNode;
                                                     while (cur && (cur.nodeType == 1)) {
                                                         
                                                     if (top < cur.scrollTop) {
                                                     cur.scrollTop = top;
                                                     }
                                                     if (top + height > cur.scrollTop + cur.clientHeight) {
                                                     cur.scrollTop = (top + height) - cur.clientHeight;
                                                     }
                                                         
                                                     var offsetTop = cur.offsetTop;
                                                     if (cur.parentNode != cur.offsetParent) {
                                                         
                                                     offsetTop -= cur.parentNode.offsetTop;
                                                     }
                                                         
                                                     top += offsetTop - cur.scrollTop;
                                                     cur = cur.parentNode;
                                                     }
                                                     }-*/;

    private void setExpandChildren(M m, boolean expand) {
        for (M child : store.getChildren(m)) {
            setExpanded(child, expand, true);
        }
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private void statefulExpand(List<M> children) {
        if (isStateful() && store.getKeyProvider() != null) {
            List<String> expanded = (List) getState().get("expanded");
            if (expanded != null && expanded.size() > 0) {
                for (M child : children) {
                    String id = store.getKeyProvider().getKey(child);
                    if (expanded.contains(id)) {
                        setExpanded(child, true);
                    }
                }
            }
        }
    }

}