/**
 * Used as a view by {@link Ext.tree.Panel TreePanel}.
 */
Ext.define('Ext.tree.View', {
    extend: 'Ext.view.Table',
    alias: 'widget.treeview',
 
    config: {
        /**
         * @cfg selectionModel
         * @inheritdoc
         */
        selectionModel: {
            type: 'treemodel'
        }
    },
    
    /**
     * @property {Boolean} isTreeView
     * `true` in this class to identify an object as an instantiated TreeView, or subclass thereof.
     */
    isTreeView: true,
 
    /**
     * @cfg loadingCls
     * @inheritdoc
     */
    loadingCls: Ext.baseCSSPrefix + 'grid-tree-loading',
    expandedCls: Ext.baseCSSPrefix + 'grid-tree-node-expanded',
    leafCls: Ext.baseCSSPrefix + 'grid-tree-node-leaf',
 
    expanderSelector: '.' + Ext.baseCSSPrefix + 'tree-expander',
    checkboxSelector: '.' + Ext.baseCSSPrefix + 'tree-checkbox',
    expanderIconOverCls: Ext.baseCSSPrefix + 'tree-expander-over',
 
    // Class to add to the node wrap element used to hold nodes when a parent is being
    // collapsed or expanded. During the animation, UI interaction is forbidden by testing
    // for an ancestor node with this class.
    nodeAnimWrapCls: Ext.baseCSSPrefix + 'tree-animator-wrap',
 
    /**
     * @cfg loadMask
     * @inheritdoc
     */
    loadMask: false,
 
    /**
     * @cfg {Boolean} rootVisible
     * False to hide the root node.
     */
    rootVisible: true,
 
    /**
     * @cfg {Boolean} animate
     * True to enable animated expand/collapse (defaults to the value of
     * {@link Ext#enableFx Ext.enableFx})
     */
 
    expandDuration: 250,
    collapseDuration: 250,
 
    /**
     * @cfg {Boolean} toggleOnDblClick
     * True to toggle expand or collapse with a double click.
     */
    toggleOnDblClick: true,
 
    /**
     * @cfg stripeRows
     * @inheritdoc
     */
    stripeRows: false,
 
    /* eslint-disable indent */
    // treeRowTpl which is inserted into the rowTpl chain before the base rowTpl.
    // Sets tree-specific classes and attributes
    treeRowTpl: [
        '{%',
            'this.processRowValues(values);',
            'this.nextTpl.applyOut(values, out, parent);',
        '%}', {
            priority: 10,
            processRowValues: function(rowValues) {
                var record = rowValues.record,
                    view = rowValues.view;
 
                // We always need to set the qtip/qtitle, because they may have been
                // emptied, which means we still need to flush that change to the DOM
                // so the old values are overwritten
                rowValues.rowAttr['data-qtip'] = record.get('qtip') || '';
                rowValues.rowAttr['data-qtitle'] = record.get('qtitle') || '';
                
                // aria-level is 1-based
                rowValues.rowAttr['aria-level'] = record.getDepth() + 1;
 
                if (record.isLeaf()) {
                    rowValues.rowClasses.push(view.leafCls);
                }
                else {
                    if (record.isExpanded()) {
                        rowValues.rowClasses.push(view.expandedCls);
                        rowValues.rowAttr['aria-expanded'] = true;
                    }
                    else {
                        rowValues.rowAttr['aria-expanded'] = false;
                    }
                }
                
                if (record.isLoading()) {
                    rowValues.rowClasses.push(view.loadingCls);
                }
            }
        }
    ],
    /* eslint-enable indent */
 
    /**
     * @event afteritemexpand
     * Fires after an item has been visually expanded and is visible in the tree.
     * @param {Ext.data.NodeInterface} node The node that was expanded
     * @param {Number} index The index of the node
     * @param {HTMLElement} item The HTML element for the node that was expanded
     */
 
    /**
     * @event afteritemcollapse
     * Fires after an item has been visually collapsed and is no longer visible in the tree.
     * @param {Ext.data.NodeInterface} node The node that was collapsed
     * @param {Number} index The index of the node
     * @param {HTMLElement} item The HTML element for the node that was collapsed
     */
 
    /**
     * @event nodedragover
     * Fires when a tree node is being targeted for a drag drop, return false to signal
     * drop not allowed.
     * @param {Ext.data.NodeInterface} targetNode The target node
     * @param {String} position The drop position, "before", "after" or "append",
     * @param {Object} dragData Data relating to the drag operation
     * @param {Ext.event.Event} e The event object for the drag
     */
 
    initComponent: function() {
        var me = this;
 
        if (me.bufferedRenderer) {
            me.animate = false;
        }
        else if (me.initialConfig.animate === undefined) {
            me.animate = Ext.enableFx;
        }
 
        me.store = me.panel.getStore();
        me.onRootChange(me.store.getRoot());
 
        me.animQueue = {};
        me.animWraps = {};
 
        me.callParent();
        me.store.setRootVisible(me.rootVisible);
        me.addRowTpl(me.lookupTpl('treeRowTpl'));
    },
 
    onFillComplete: function(treeStore, fillRoot, newNodes) {
        var me = this,
            store = me.store,
            start = store.indexOf(newNodes[0]);
 
        // Always update the current node, since the load may be triggered
        // by .load() directly instead of .expand() on the node
        fillRoot.triggerUIUpdate();
 
        // In the cases of expand, the records might not be in the store yet,
        // so jump out early and expand will handle it later
        if (!newNodes.length || start === -1) {
            return;
        }
 
        // Insert new nodes into the view
        me.onAdd(me.store, newNodes, start);
 
        me.refreshPartner();
    },
 
    refreshPartner: function() {
        var partner = this.lockingPartner;
        
        if (partner) {
            partner.refresh();
        }
    },
 
    afterRender: function() {
        var me = this;
        me.callParent();
        me.el.on({
            scope: me,
            delegate: me.expanderSelector,
            mouseover: me.onExpanderMouseOver,
            mouseout: me.onExpanderMouseOut
        });
    },
 
    processUIEvent: function(e) {
        // If the clicked node is part of an animation, ignore the click.
        // This is because during a collapse animation, the associated Records
        // will already have been removed from the Store, and the event is not processable.
        if (e.getTarget('.' + this.nodeAnimWrapCls, this.el)) {
            return false;
        }
        
        return this.callParent([e]);
    },
 
    setRootNode: function(node) {
        this.node = node;
    },
 
    getChecked: function() {
        var checked = [];
        
        this.node.cascade(function(rec) {
            if (rec.get('checked')) {
                checked.push(rec);
            }
        });
        
        return checked;
    },
 
    isItemChecked: function(rec) {
        return rec.get('checked');
    },
 
    /**
     * @private
     */
    createAnimWrap: function(record, index) {
        var me = this,
            node = me.getNode(record),
            tmpEl;
 
        tmpEl = Ext.fly(node).insertSibling({
            role: 'presentation',
            tag: 'div',
            cls: me.nodeAnimWrapCls
        }, 'after');
 
        return {
            record: record,
            node: node,
            el: tmpEl,
            expanding: false,
            collapsing: false,
            animateEl: tmpEl,
            targetEl: tmpEl
        };
    },
 
    /**
     * @private
     * Returns the animation wrapper element for the specified parent node, used to wrap
     * the child nodes as they slide up or down during expand/collapse.
     *
     * @param parent The parent node to be expanded or collapsed
     *
     * @param [bubble=true] If the passed parent node does not already have a wrap element created,
     * by default this function will bubble up to each parent node looking for a valid wrap element
     * to reuse, returning the first one it finds. This is the appropriate behavior, e.g.,
     * for the collapse direction, so that the entire expanded set of branch nodes can collapse
     * as a single unit.
     *
     * However for expanding each parent node should instead always create its own animation wrap
     * if one doesn't exist, so that its children can expand independently of any other nodes --
     * this is crucial when executing the "expand all" behavior. If multiple nodes attempt to reuse
     * the same ancestor wrap element concurrently during expansion it will lead to problems
     * as the first animation to complete will delete the wrap el out from under other running
     * animations. For that reason, when expanding you should always pass `bubble: false`
     * to be on the safe side.
     *
     * If the passed parent has no wrap (or there is no valid ancestor wrap after bubbling),
     * this function will return null and the calling code should then call {@link #createAnimWrap}
     * if needed.
     *
     * @return {Ext.dom.Element} The wrapping element as created in {@link #createAnimWrap}, or null
     */
    getAnimWrap: function(parent, bubble) {
        if (!this.animate) {
            return null;
        }
 
        // eslint-disable-next-line vars-on-top
        var wraps = this.animWraps,
            wrap = wraps[parent.internalId];
 
        if (bubble !== false) {
            while (!wrap && parent) {
                parent = parent.parentNode;
                
                if (parent) {
                    wrap = wraps[parent.internalId];
                }
            }
        }
        
        return wrap;
    },
 
    doAdd: function(records, index) {
        var me = this,
            record = records[0],
            parent = record.parentNode,
            all = me.all,
            relativeIndex,
            animWrap = me.getAnimWrap(parent),
            targetEl, childNodes, len, result, children;
 
        if (!animWrap || !animWrap.expanding) {
            return me.callParent([records, index]);
        }
 
        // If we are adding records which have a parent that is currently expanding
        // lets add them to the animation wrap
        result = me.bufferRender(records, index, true);
        children = result.children;
 
        // We need the parent that has the animWrap, not the node's parent
        parent = animWrap.record;
 
        // If there is an anim wrap we do our special magic logic
        targetEl = animWrap.targetEl;
        childNodes = targetEl.dom.childNodes;
        len = childNodes.length;
 
        // The relative index is the index in the full flat collection
        // minus the index of the wraps parent
        relativeIndex = index - me.indexInStore(parent) - 1;
 
        // If we are adding records to the wrap that have a higher relative index
        // then there are currently children
        // it means we have to append the nodes to the wrap
        if (!len || relativeIndex >= len) {
            targetEl.appendChild(result.fragment, true);
        }
        // If there are already more children then the relative index it means we are adding
        // child nodes of some expanded node in the anim wrap. In this case we have to insert
        // the nodes in the right location
        else {
            Ext.fly(childNodes[relativeIndex]).insertSibling(children, 'before', true);
        }
 
        // We also have to update the node cache of the DataView
        all.insert(index, children);
 
        return children;
    },
 
    onRemove: function(ds, records, index) {
        var me = this,
            empty, i,
            oldItems;
 
        if (me.viewReady) {
            empty = me.store.getCount() === 0;
 
            // If buffered rendering is being used, call the parent class.
            if (me.bufferedRenderer) {
                return me.callParent([ds, records, index]);
            }
            
            oldItems = this.all.slice(index, index + records.length);
            
            // Nothing left, just refresh the view.
            if (empty) {
                me.refresh();
            }
            else {
                // Remove in reverse order so that indices remain correct
                for (= records.length - 1, index += i; i >= 0; --i, --index) {
                    me.doRemove(records[i], index);
                }
                
                me.refreshSizePending = true;
            }
 
            me.fireItemMutationEvent('itemremove', records, index, oldItems, me);
        }
    },
 
    doRemove: function(record, index) {
        // If we are adding records which have a parent that is currently expanding
        // lets add them to the animation wrap
        var me = this,
            all = me.all,
            animWrap = me.getAnimWrap(record),
            item = all.item(index),
            node = item ? item.dom : null;
 
        if (!node || !animWrap || !animWrap.collapsing) {
            return me.callParent([record, index]);
        }
 
        // Insert the item at the beginning of the animate el - child nodes are removed
        // in reverse order so that the index can be used.
        animWrap.targetEl.dom.insertBefore(node, animWrap.targetEl.dom.firstChild);
        all.removeElement(index);
    },
 
    onBeforeExpand: function(parent, records, index) {
        var me = this,
            animWrap;
 
        if (me.rendered && me.all.getCount() && me.animate) {
            if (me.getNode(parent)) {
                animWrap = me.getAnimWrap(parent, false);
 
                if (!animWrap) {
                    animWrap = me.animWraps[parent.internalId] = me.createAnimWrap(parent);
                    animWrap.animateEl.setHeight(0);
                }
                else if (animWrap.collapsing) {
                    // If we expand this node while it is still expanding then we
                    // have to remove the nodes from the animWrap.
                    animWrap.targetEl.select(me.itemSelector).destroy();
                }
 
                animWrap.expanding = true;
                animWrap.collapsing = false;
            }
        }
    },
 
    onExpand: function(parent) {
        var me = this,
            queue = me.animQueue,
            id = parent.getId(),
            node = me.getNode(parent),
            index = node ? me.indexOf(node) : -1,
            animWrap,
            animateEl,
            targetEl;
 
        if (me.singleExpand) {
            me.ensureSingleExpand(parent);
        }
 
        // The item is not visible yet
        if (index === -1) {
            return;
        }
 
        animWrap = me.getAnimWrap(parent, false);
 
        if (!animWrap) {
            parent.isExpandingOrCollapsing = false;
            me.fireEvent('afteritemexpand', parent, index, node);
 
            return;
        }
 
        animateEl = animWrap.animateEl;
        targetEl = animWrap.targetEl;
 
        animateEl.stopAnimation();
        queue[id] = true;
 
        // Must set element height before this event finishes because animation does not set
        // initial condition until first tick has elapsed.
        // Which is good because the upcoming layout resumption must read the content height
        // BEFORE it gets squished.
        Ext.on('idle', function() {
            animateEl.dom.style.height = '0px';
        }, null, { single: true });
 
        animateEl.animate({
            from: {
                height: 0
            },
            to: {
                height: targetEl.dom.scrollHeight
            },
            duration: me.expandDuration,
            listeners: {
                afteranimate: function() {
                    // Move all the nodes out of the anim wrap to their proper location
                    // Must do this in afteranimate because lastframe does not fire if the
                    // animation is stopped.
                    var items = targetEl.dom.childNodes,
                        activeEl = Ext.Element.getActiveElement();
 
                    if (items.length) {
                        if (!targetEl.contains(activeEl)) {
                            activeEl = null;
                        }
                        
                        animWrap.el.insertSibling(items, 'before', true);
                        
                        if (activeEl) {
                            Ext.fly(activeEl).focus();
                        }
                    }
                    
                    animWrap.el.destroy();
                    queue[id] = null;
                    
                    if (!me.destroyed) {
                        me.animWraps[animWrap.record.internalId] = null;
                    }
                }
            },
            callback: function() {
                parent.isExpandingOrCollapsing = false;
 
                if (!me.destroyed) {
                    me.refreshSize(true);
                }
 
                me.fireEvent('afteritemexpand', parent, index, node);
            }
        });
    },
 
    // Triggered by the TreeStore's beforecollapse event.
    onBeforeCollapse: function(parent, records, index, callback, scope) {
        var me = this,
            animWrap;
 
        if (me.rendered && me.all.getCount()) {
            if (me.animate) {
                // Only process if the collapsing node is in the UI.
                // A node may be collapsed as part of a recursive ancestor collapse, and if it
                // has already been removed from the UI by virtue of an ancestor being collapsed,
                // we should not do anything.
                if (parent.getTreeStore().isVisible(parent)) {
                    animWrap = me.getAnimWrap(parent);
 
                    if (!animWrap) {
                        animWrap = me.animWraps[parent.internalId] =
                            me.createAnimWrap(parent, index);
                    }
                    else if (animWrap.expanding) {
                        // If we collapse this node while it is still expanding then we
                        // have to remove the nodes from the animWrap.
                        animWrap.targetEl.select(this.itemSelector).destroy();
                    }
 
                    animWrap.expanding = false;
                    animWrap.collapsing = true;
                    animWrap.callback = callback;
                    animWrap.scope = scope;
                }
            }
            else {
                // Cache any passed callback for use in the onCollapse post collapse handler
                // non-animated codepath
                me.onCollapseCallback = callback;
                me.onCollapseScope = scope;
            }
        }
    },
 
    onCollapse: function(parent) {
        var me = this,
            queue = me.animQueue,
            id = parent.getId(),
            node = me.getNode(parent),
            index = node ? me.indexOf(node) : -1,
            animWrap = me.getAnimWrap(parent),
            animateEl;
 
        // If the collapsed node is already removed from the UI
        // by virtue of being a descendant of a collapsed node, then
        // we have nothing to do here.
        if (!me.all.getCount() || !parent.isVisible()) {
            return;
        }
 
        // Not animating, all items will have been added, so updateLayout and resume layouts
        if (!animWrap) {
            parent.isExpandingOrCollapsing = false;
            me.fireEvent('afteritemcollapse', parent, index, node);
 
            // Call any collapse callback cached in the onBeforeCollapse handler
            Ext.callback(me.onCollapseCallback, me.onCollapseScope);
            me.onCollapseCallback = me.onCollapseScope = null;
 
            return;
        }
 
        animateEl = animWrap.animateEl;
 
        queue[id] = true;
 
        animateEl.stopAnimation();
        animateEl.animate({
            to: {
                height: 0
            },
            duration: me.collapseDuration,
            listeners: {
                afteranimate: function() {
                    // In case lastframe did not fire because the animation was stopped.
                    animWrap.el.destroy();
                    queue[id] = null;
    
                    if (!me.destroyed) {
                        me.animWraps[animWrap.record.internalId] = null;
                    }
                }
            },
            callback: function() {
                parent.isExpandingOrCollapsing = false;
 
                if (!me.destroyed) {
                    me.refreshSize(true);
                }
 
                me.fireEvent('afteritemcollapse', parent, index, node);
 
                // Call any collapse callback cached in the onBeforeCollapse handler
                Ext.callback(animWrap.callback, animWrap.scope);
                animWrap.callback = animWrap.scope = null;
            }
        });
    },
 
    /**
     * Checks if a node is currently undergoing animation
     * @private
     * @param {Ext.data.Model} node The node
     * @return {Boolean} True if the node is animating
     */
    isAnimating: function(node) {
        return !!this.animQueue[node.getId()];
    },
 
    /**
     * Expands a record that is loaded in the view.
     *
     * If an animated collapse or expand of the record is in progress, this call will be ignored.
     * @param {Ext.data.Model} record The record to expand
     * @param {Boolean} [deep] True to expand nodes all the way down the tree hierarchy.
     * @param {Function} [callback] The function to run after the expand is completed
     * @param {Object} [scope] The scope of the callback function.
     */
    expand: function(record, deep, callback, scope) {
        var me = this,
            doAnimate = !!me.animate,
            result;
 
        // Block toggling if we are already animating an expand or collapse operation.
        if (!doAnimate || !record.isExpandingOrCollapsing) {
            if (!record.isLeaf()) {
                record.isExpandingOrCollapsing = doAnimate;
            }
 
            // Need to suspend layouts because the expand process makes multiple changes to the UI
            // in addition to inserting new nodes. Folder and elbow images have to change, so we
            // need to coalesce all resulting layouts.
            Ext.suspendLayouts();
            result = record.expand(deep, callback, scope);
            Ext.resumeLayouts(true);
 
            return result;
        }
    },
 
    /**
     * Collapses a record that is loaded in the view.
     *
     * If an animated collapse or expand of the record is in progress, this call will be ignored.
     * @param {Ext.data.Model} record The record to collapse
     * @param {Boolean} [deep] True to collapse nodes all the way up the tree hierarchy.
     * @param {Function} [callback] The function to run after the collapse is completed
     * @param {Object} [scope] The scope of the callback function.
     */
    collapse: function(record, deep, callback, scope) {
        var me = this,
            doAnimate = !!me.animate;
 
        // Block toggling if we are already animating an expand or collapse operation.
        if (!doAnimate || !record.isExpandingOrCollapsing) {
            if (!record.isLeaf()) {
                record.isExpandingOrCollapsing = doAnimate;
            }
 
            return record.collapse(deep, callback, scope);
        }
    },
 
    /**
     * Toggles a record between expanded and collapsed.
     *
     * If an animated collapse or expand of the record is in progress, this call will be ignored.
     * @param {Ext.data.Model} record 
     * @param {Boolean} [deep] True to collapse nodes all the way up the tree hierarchy.
     * @param {Function} [callback] The function to run after the expand/collapse is completed
     * @param {Object} [scope] The scope of the callback function.
     */
    toggle: function(record, deep, callback, scope) {
        if (record.isExpanded()) {
            this.collapse(record, deep, callback, scope);
        }
        else {
            this.expand(record, deep, callback, scope);
        }
    },
 
    onItemDblClick: function(record, item, index, e) {
        var me = this,
            editingPlugin = me.editingPlugin;
 
        me.callParent([record, item, index, e]);
        
        if (me.toggleOnDblClick && record.isExpandable() &&
            !(editingPlugin && editingPlugin.clicksToEdit === 2)) {
            me.toggle(record);
        }
    },
    
    onCellClick: function(cell, cellIndex, record, row, rowIndex, e) {
        var me = this,
            column = e.position.column;
 
        // We're only interested in clicks in the tree column
        if (column.isTreeColumn) {
            
            // Click on the checkbox and there is a defined data value; toggle it.
            if (e.getTarget(me.checkboxSelector, cell) && record.get('checked') != null) {
                me.onCheckChange(e);
 
                // Allow the stopSelection config on checkable tree columns to prevent selection
                if (column.stopSelection) {
                    e.stopSelection = true;
                }
            }
 
            // Click on the expander
            else if (e.getTarget(me.expanderSelector, cell) && record.isExpandable()) {
                // Ensure focus is on the clicked cell so that if this causes a refresh,
                // focus restoration does not scroll back to the previouslty focused position.
                // onCellClick is called *befor* cellclick is fired which is what changes
                // focus position.
                // TODO: connect directly from View's event processing to NavigationModel
                // without relying on events.
                me.getNavigationModel().setPosition(e.position);
                me.toggle(record, e.ctrlKey);
 
                // So that we know later to stop event propagation by returning false
                // from the NavigationModel
                // TODO: when NavigationModel is directly hooked up to be called *before*
                // the event sequence
                // This flag will not be necessary.
                e.nodeToggled = true;
            }
 
            return me.callParent([cell, cellIndex, record, row, rowIndex, e]);
        }
    },
    
    onCheckChange: function(e) {
        var me = this,
            record = e.record,
            wasChecked = record.get('checked'),
            checked;
    
        // 1 means semi-checked.
        // Toggle of that state checks.
        if (wasChecked === 1) {
            checked = true;
        }
        else {
            checked = !wasChecked;
        }
 
        me.setChecked(record, checked, e);
    },
 
    setChecked: function(record, meChecked, e, options) {
        var me = this,
            checkPropagation =
                me.checkPropagationFlags[me.ownerGrid.checkPropagation.toLowerCase()],
            wasChecked = record.data.checked,
            halfCheckedValue = me.ownerGrid.triStateCheckbox ? 1 : false,
            progagateCheck = (!options || options.propagateCheck !== false) &&
                             (checkPropagation & 1),
            checkParent = (!options || options.checkParent !== false) && (checkPropagation & 2),
            matchedChildCount = 0,
            parentNode, parentChecked, foundCheck, foundClear, childNodes, len, i;
 
        if (me.fireEvent('beforecheckchange', record, wasChecked, e) === false) {
            return;
        }
 
        // Propagate full ->true and ->false changes to child nodes
        // unless we're being called from a setChecked on a child node.
        if (meChecked !== 1 && progagateCheck) {
            childNodes = record.childNodes;
            len = childNodes.length;
            
            for (= 0; i < len; i++) {
 
                // We are setting child nodes, so pass the
                // checkParent flag as false to avoid reentry back into this node.
                me.setChecked(childNodes[i], meChecked, e, {
                    checkParent: false
                });
 
                if (childNodes[i].get('checked') === meChecked) {
                    matchedChildCount++;
                }
            }
 
            // If one or more of the child nodes refused
            if (matchedChildCount !== len) {
                meChecked = matchedChildCount ? halfCheckedValue : false;
            }
        }
 
        // If the new valud was not reset due to vetoing from
        // changes propagated to child nodes, then go ahead with the change.
        if (record.get('checked') !== meChecked) {
            record.set('checked', meChecked, options);
 
            // Fire checkchange now we know the valus has changed.
            me.fireEvent('checkchange', record, meChecked, e);
 
            // If there's a parent node, and the parent node has a checked data property
            // keep parent up to date with checkedness of its child nodes.
            if (checkParent && (parentNode = record.parentNode) &&
                (parentChecked = parentNode.data.checked) != null) {
                childNodes = parentNode.childNodes;
                len = childNodes.length;
 
                // If we're semi checked, the parent is semi checked.
                if (meChecked === halfCheckedValue) {
                    parentChecked = halfCheckedValue;
                }
                // If we're the sole child, the parent is our state.
                else if (len === 1) {
                    parentChecked = meChecked;
                }
                else {
                    foundCheck = foundClear = false;
                    
                    for (= 0; !(foundCheck && foundClear) & i < len; i++) {
                        if (childNodes[i].data.checked === 1) {
                            foundCheck = foundClear = true;
                        }
                        else if (!childNodes[i].data.checked) {
                            foundClear = true;
                        }
                        else {
                            foundCheck = true;
                        }
                    }
                    
                    parentChecked = foundCheck && foundClear
                        ? halfCheckedValue
                        : (foundCheck ? true : false);
                }
 
                if (parentNode.get('checked') !== parentChecked) {
                    // We are setting the parent node, so pass the
                    // progagateCheck flag as false to avoid reentry back into this node.
                    me.setChecked(parentNode, parentChecked, e, {
                        propagateCheck: false
                    });
                }
            }
        }
    },
 
    onExpanderMouseOver: function(e) {
        Ext.fly(e.getTarget(this.cellSelector, 10)).addCls(this.expanderIconOverCls);
    },
 
    onExpanderMouseOut: function(e) {
        Ext.fly(e.getTarget(this.cellSelector, 10)).removeCls(this.expanderIconOverCls);
    },
 
    getStoreListeners: function() {
        return Ext.apply(this.callParent(), {
            rootchange: this.onRootChange,
            fillcomplete: this.onFillComplete
        });
    },
 
    onBindStore: function(store, initial, propName, oldStore) {
        var oldRoot = oldStore && oldStore.getRootNode(),
            newRoot = store && store.getRootNode();
 
        this.callParent([store, initial, propName, oldStore]);
 
        // The root implicitly changes when reconfigured with a new store.
        // The store's own rootChange event when it initially sets its own rootNode
        // will not have reached us because it was not ourt store during its initialization.
        if (newRoot !== oldRoot) {
            this.onRootChange(newRoot, oldRoot);
        }
    },
 
    onRootChange: function(newRoot, oldRoot) {
        var me = this,
            grid = me.grid;
 
        if (oldRoot) {
            me.rootListeners.destroy();
            me.rootListeners = null;
        }
        
        if (newRoot) {
            me.rootListeners = newRoot.on({
                beforeexpand: me.onBeforeExpand,
                expand: me.onExpand,
                beforecollapse: me.onBeforeCollapse,
                collapse: me.onCollapse,
                destroyable: true,
                scope: me
            });
 
            grid.addRelayers(newRoot);
        }
    },
 
    ensureSingleExpand: function(node) {
        var parent = node.parentNode;
        
        if (parent) {
            parent.eachChild(function(child) {
                if (child !== node && child.isExpanded()) {
                    child.collapse();
                }
            });
        }
    },
 
    privates: {
        checkPropagationFlags: {
            none: 0,
            down: 1,
            up: 2,
            both: 3
        },
 
        deferRefreshForLoad: function(store) {
            var ret = this.callParent([store]),
                options, node;
 
            if (ret) {
                options = store.lastOptions;
                node = options && options.node;
                
                // If the root isn't loading, then proceed with the refresh, we'll
                // add the other nodes as they come in
                if (node && node !== store.getRoot()) {
                    ret = false;
                }
            }
            
            return ret;
        }
    }
});