    /**
     * This class encapsulates the user interface for a tabular data set.
     * It acts as a centralized manager for controlling the various interface
     * elements of the view. This includes handling events, such as row and cell
     * level based DOM events. It also reacts to events from the underlying {@link Ext.selection.Model}
     * to provide visual feedback to the user.
     *
     * This class does not provide ways to manipulate the underlying data of the configured
     * {@link Ext.data.Store}.
     *
     * This is the base class for both {@link Ext.grid.View} and {@link Ext.tree.View} and is not
     * to be used directly.
     */
    Ext.define('Ext.view.Table', {
        extend: 'Ext.view.View',
        alias: 'widget.tableview',
        uses: [
            'Ext.view.TableChunker',
            'Ext.util.DelayedTask',
            'Ext.util.MixedCollection'
        ],

        baseCls: Ext.baseCSSPrefix + 'grid-view',

        // row
        itemSelector: '.' + Ext.baseCSSPrefix + 'grid-row',
        // cell
        cellSelector: '.' + Ext.baseCSSPrefix + 'grid-cell',

        selectedItemCls: Ext.baseCSSPrefix + 'grid-row-selected',
        selectedCellCls: Ext.baseCSSPrefix + 'grid-cell-selected',
        focusedItemCls: Ext.baseCSSPrefix + 'grid-row-focused',
        overItemCls: Ext.baseCSSPrefix + 'grid-row-over',
        altRowCls:   Ext.baseCSSPrefix + 'grid-row-alt',
        rowClsRe: /(?:^|\s*)grid-row-(first|last|alt)(?:\s+|$)/g,
        cellRe: new RegExp('x-grid-cell-([^\\s]+) ', ''),

        // cfg docs inherited
        trackOver: true,

        /**
         * Override this function to apply custom CSS classes to rows during rendering. This function should return the
         * CSS class name (or empty string '' for none) that will be added to the row's wrapping div. To apply multiple
         * class names, simply return them space-delimited within the string (e.g. 'my-class another-class').
         * Example usage:
         *
         *     viewConfig: {
         *         getRowClass: function(record, rowIndex, rowParams, store){
         *             return record.get("valid") ? "row-valid" : "row-error";
         *         }
         *     }
         *
         * @param {Ext.data.Model} record The record corresponding to the current row.
         * @param {Number} index The row index.
         * @param {Object} rowParams **DEPRECATED.** For row body use the
         * {@link Ext.grid.feature.RowBody#getAdditionalData getAdditionalData} method of the rowbody feature.
         * @param {Ext.data.Store} store The store this grid is bound to
         * @return {String} a CSS class name to add to the row.
         * @method
         */
        getRowClass: null,

        initComponent: function() {
            var me = this;

            me.scrollState = {};
            me.selModel.view = me;
            me.headerCt.view = me;
            me.initFeatures();
            me.tpl = '<div></div>';
            me.callParent();
            me.mon(me.store, {
                load: me.onStoreLoad,
                scope: me
            });

            // this.addEvents(
            //     /**
            //      * @event rowfocus
            //      * @param {Ext.data.Model} record
            //      * @param {HTMLElement} row
            //      * @param {Number} rowIdx
            //      */
            //     'rowfocus'
            // );
        },

        // scroll to top of the grid when store loads
        onStoreLoad: function(){
            var me = this;

            if (me.invalidateScrollerOnRefresh) {
                if (Ext.isGecko) {
                    if (!me.scrollToTopTask) {
                        me.scrollToTopTask = Ext.create('Ext.util.DelayedTask', me.scrollToTop, me);
                    }
                    me.scrollToTopTask.delay(1);
                } else {
                    me    .scrollToTop();
                }
            }
        },

        // scroll the view to the top
        scrollToTop: Ext.emptyFn,

        /**
         * Add a listener to the main view element. It will be destroyed with the view.
         * @private
         */
        addElListener: function(eventName, fn, scope){
            this.mon(this, eventName, fn, scope, {
                element: 'el'
            });
        },

        /**
         * Get the columns used for generating a template via TableChunker.
         * See {@link Ext.grid.header.Container#getGridColumns}.
         * @private
         */
        getGridColumns: function() {
            return this.headerCt.getGridColumns();
        },

        /**
         * Get a leaf level header by index regardless of what the nesting
         * structure is.
         * @private
         * @param {Number} index The index
         */
        getHeaderAtIndex: function(index) {
            return this.headerCt.getHeaderAtIndex(index);
        },

        /**
         * Get the cell (td) for a particular record and column.
         * @param {Ext.data.Model} record
         * @param {Ext.grid.column.Column} column
         * @private
         */
        getCell: function(record, column) {
            var row = this.getNode(record);
            return Ext.fly(row).down(column.getCellSelector());
        },

        /**
         * Get a reference to a feature
         * @param {String} id The id of the feature
         * @return {Ext.grid.feature.Feature} The feature. Undefined if not found
         */
        getFeature: function(id) {
            var features = this.featuresMC;
            if (features) {
                return features.get(id);
            }
        },

        /**
         * Initializes each feature and bind it to this view.
         * @private
         */
        initFeatures: function() {
            var me = this,
                i = 0,
                features,
                len;

            me.features = me.features || [];
            features = me.features;
            len = features.length;

            me.featuresMC = Ext.create('Ext.util.MixedCollection');
            for (; i < len; i++) {
                // ensure feature hasnt already been instantiated
                if (!features[i].isFeature) {
                    features[i] = Ext.create('feature.' + features[i].ftype, features[i]);
                }
                // inject a reference to view
                features[i].view = me;
                me.featuresMC.add(features[i]);
            }
        },

        /**
         * Gives features an injection point to attach events to the markup that
         * has been created for this view.
         * @private
         */
        attachEventsForFeatures: function() {
            var features = this.features,
                ln       = features.length,
                i        = 0;

            for (; i < ln; i++) {
                if (features[i].isFeature) {
                    features[i].attachEvents();
                }
            }
        },

        afterRender: function() {
            var me = this;

            me.callParent();
            me.mon(me.el, {
                scroll: me.fireBodyScroll,
                scope: me
            });
            me.el.unselectable();
            me.attachEventsForFeatures();
        },

        fireBodyScroll: function(e, t) {
            this.fireEvent('bodyscroll', e, t);
        },

        // TODO: Refactor headerCt dependency here to colModel
        /**
         * Uses the headerCt to transform data from dataIndex keys in a record to
         * headerId keys in each header and then run them through each feature to
         * get additional data for variables they have injected into the view template.
         * @private
         */
        prepareData: function(data, idx, record) {
            var me       = this,
                orig     = me.headerCt.prepareData(data, idx, record, me, me.ownerCt),
                features = me.features,
                ln       = features.length,
                i        = 0,
                node, feature;

            for (; i < ln; i++) {
                feature = features[i];
                if (feature.isFeature) {
                    Ext.apply(orig, feature.getAdditionalData(data, idx, record, orig, me));
                }
            }

            return orig;
        },

        // TODO: Refactor headerCt dependency here to colModel
        collectData: function(records, startIndex) {
            var preppedRecords = this.callParent(arguments),
                headerCt  = this.headerCt,
                fullWidth = headerCt.getFullWidth(),
                features  = this.features,
                ln = features.length,
                o = {
                    rows: preppedRecords,
                    fullWidth: fullWidth
                },
                i  = 0,
                feature,
                j = 0,
                jln,
                rowParams;

            jln = preppedRecords.length;
            // process row classes, rowParams has been deprecated and has been moved
            // to the individual features that implement the behavior.
            if (this.getRowClass) {
                for (; j < jln; j++) {
                    rowParams = {};
                    preppedRecords[j]['rowCls'] = this.getRowClass(records[j], j, rowParams, this.store);
                    //<debug>
                    if (rowParams.alt) {
                        Ext.Error.raise("The getRowClass alt property is no longer supported.");
                    }
                    if (rowParams.tstyle) {
                        Ext.Error.raise("The getRowClass tstyle property is no longer supported.");
                    }
                    if (rowParams.cells) {
                        Ext.Error.raise("The getRowClass cells property is no longer supported.");
                    }
                    if (rowParams.body) {
                        Ext.Error.raise("The getRowClass body property is no longer supported. Use the getAdditionalData method of the rowbody feature.");
                    }
                    if (rowParams.bodyStyle) {
                        Ext.Error.raise("The getRowClass bodyStyle property is no longer supported.");
                    }
                    if (rowParams.cols) {
                        Ext.Error.raise("The getRowClass cols property is no longer supported.");
                    }
                    //</debug>
                }
            }
            // currently only one feature may implement collectData. This is to modify
            // what's returned to the view before its rendered
            for (; i < ln; i++) {
                feature = features[i];
                if (feature.isFeature && feature.collectData && !feature.disabled) {
                    o = feature.collectData(records, preppedRecords, startIndex, fullWidth, o);
                    break;
                }
            }
            return o;
        },

        // TODO: Refactor header resizing to column resizing
        /**
         * When a header is resized, setWidth on the individual columns resizer class,
         * the top level table, save/restore scroll state, generate a new template and
         * restore focus to the grid view's element so that keyboard navigation
         * continues to work.
         * @private
         */
        onHeaderResize: function(header, w, suppressFocus) {
            var me = this,
                el = me.el;

            if (el) {
                me.saveScrollState();
                // Grab the col and set the width, css
                // class is generated in TableChunker.
                // Select composites because there may be several chunks.

                // IE6 and IE7 bug.
                // Setting the width of the first TD does not work - ends up with a 1 pixel discrepancy.
                // We need to increment the passed with in this case.
                if (Ext.isIE6 || Ext.isIE7) {
                    if (header.el.hasCls(Ext.baseCSSPrefix + 'column-header-first')) {
                        w += 1;
                    }
                }
                el.select('.' + Ext.baseCSSPrefix + 'grid-col-resizer-'+header.id).setWidth(w);
                el.select('.' + Ext.baseCSSPrefix + 'grid-table-resizer').setWidth(me.headerCt.getFullWidth());
                me.restoreScrollState();
                if (!me.ignoreTemplate) {
                    me.setNewTemplate();
                }
                if (!suppressFocus) {
                    me.el.focus();
                }
            }
        },

        /**
         * When a header is shown restore its oldWidth if it was previously hidden.
         * @private
         */
        onHeaderShow: function(headerCt, header, suppressFocus) {
            var me = this;
            me.ignoreTemplate = true;
            // restore headers that were dynamically hidden
            if (header.oldWidth) {
                me.onHeaderResize(header, header.oldWidth, suppressFocus);
                delete header.oldWidth;
            // flexed headers will have a calculated size set
            // this additional check has to do with the fact that
            // defaults: {width: 100} will fight with a flex value
            } else if (header.width && !header.flex) {
                me.onHeaderResize(header, header.width, suppressFocus);
            }
            delete me.ignoreTemplate;
            me.setNewTemplate();
        },

        /**
         * When the header hides treat it as a resize to 0.
         * @private
         */
        onHeaderHide: function(headerCt, header, suppressFocus) {
            this.onHeaderResize(header, 0, suppressFocus);
        },

        /**
         * Set a new template based on the current columns displayed in the
         * grid.
         * @private
         */
        setNewTemplate: function() {
            var me = this,
                columns = me.headerCt.getColumnsForTpl(true);

            me.tpl = me.getTableChunker().getTableTpl({
                columns: columns,
                features: me.features
            });
        },

        /**
         * Returns the configured chunker or default of Ext.view.TableChunker
         */
        getTableChunker: function() {
            return this.chunker || Ext.view.TableChunker;
        },

        /**
         * Adds a CSS Class to a specific row.
         * @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement, index or instance of a model
         * representing this row
         * @param {String} cls
         */
        addRowCls: function(rowInfo, cls) {
            var row = this.getNode(rowInfo);
            if (row) {
                Ext.fly(row).addCls(cls);
            }
        },

        /**
         * Removes a CSS Class from a specific row.
         * @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement, index or instance of a model
         * representing this row
         * @param {String} cls
         */
        removeRowCls: function(rowInfo, cls) {
            var row = this.getNode(rowInfo);
            if (row) {
                Ext.fly(row).removeCls(cls);
            }
        },

        // GridSelectionModel invokes onRowSelect as selection changes
        onRowSelect : function(rowIdx) {
            this.addRowCls(rowIdx, this.selectedItemCls);
        },

        // GridSelectionModel invokes onRowDeselect as selection changes
        onRowDeselect : function(rowIdx) {
            var me = this;

            me.removeRowCls(rowIdx, me.selectedItemCls);
            me.removeRowCls(rowIdx, me.focusedItemCls);
        },

        onCellSelect: function(position) {
            var cell = this.getCellByPosition(position);
            if (cell) {
                cell.addCls(this.selectedCellCls);
            }
        },

        onCellDeselect: function(position) {
            var cell = this.getCellByPosition(position);
            if (cell) {
                cell.removeCls(this.selectedCellCls);
            }

        },

        onCellFocus: function(position) {
            //var cell = this.getCellByPosition(position);
            this.focusCell(position);
        },

        getCellByPosition: function(position) {
            var row    = position.row,
                column = position.column,
                store  = this.store,
                node   = this.getNode(row),
                header = this.headerCt.getHeaderAtIndex(column),
                cellSelector,
                cell = false;

            if (header && node) {
                cellSelector = header.getCellSelector();
                cell = Ext.fly(node).down(cellSelector);
            }
            return cell;
        },

        // GridSelectionModel invokes onRowFocus to 'highlight'
        // the last row focused
        onRowFocus: function(rowIdx, highlight, supressFocus) {
            var me = this,
                row = me.getNode(rowIdx);

            if (highlight) {
                me.addRowCls(rowIdx, me.focusedItemCls);
                if (!supressFocus) {
                    me.focusRow(rowIdx);
                }
                //this.el.dom.setAttribute('aria-activedescendant', row.id);
            } else {
                me.removeRowCls(rowIdx, me.focusedItemCls);
            }
        },

        /**
         * Focuses a particular row and brings it into view. Will fire the rowfocus event.
         * @param {HTMLElement/String/Number/Ext.data.Model} rowIdx
         * An HTMLElement template node, index of a template node, the id of a template node or the
         * record associated with the node.
         */
        focusRow: function(rowIdx) {
            var me         = this,
                row        = me.getNode(rowIdx),
                el         = me.el,
                adjustment = 0,
                panel      = me.ownerCt,
                rowRegion,
                elRegion,
                record;

            if (row && el) {
                elRegion  = el.getRegion();
                rowRegion = Ext.fly(row).getRegion();
                // row is above
                if (rowRegion.top < elRegion.top) {
                    adjustment = rowRegion.top - elRegion.top;
                // row is below
                } else if (rowRegion.bottom > elRegion.bottom) {
                    adjustment = rowRegion.bottom - elRegion.bottom;
                }
                record = me.getRecord(row);
                rowIdx = me.store.indexOf(record);

                if (adjustment) {
                    // scroll the grid itself, so that all gridview's update.
                    panel.scrollByDeltaY(adjustment);
                }
                me.fireEvent('rowfocus', record, row, rowIdx);
            }
        },

        focusCell: function(position) {
            var me          = this,
                cell        = me.getCellByPosition(position),
                el          = me.el,
                adjustmentY = 0,
                adjustmentX = 0,
                elRegion    = el.getRegion(),
                panel       = me.ownerCt,
                cellRegion,
                record;

            if (cell) {
                cellRegion = cell.getRegion();
                // cell is above
                if (cellRegion.top < elRegion.top) {
                    adjustmentY = cellRegion.top - elRegion.top;
                // cell is below
                } else if (cellRegion.bottom > elRegion.bottom) {
                    adjustmentY = cellRegion.bottom - elRegion.bottom;
                }

                // cell is left
                if (cellRegion.left < elRegion.left) {
                    adjustmentX = cellRegion.left - elRegion.left;
                // cell is right
                } else if (cellRegion.right > elRegion.right) {
                    adjustmentX = cellRegion.right - elRegion.right;
                }

                if (adjustmentY) {
                    // scroll the grid itself, so that all gridview's update.
                    panel.scrollByDeltaY(adjustmentY);
                }
                if (adjustmentX) {
                    panel.scrollByDeltaX(adjustmentX);
                }
                el.focus();
                me.fireEvent('cellfocus', record, cell, position);
            }
        },

        /**
         * Scrolls by delta. This affects this individual view ONLY and does not
         * synchronize across views or scrollers.
         * @param {Number} delta
         * @param {String} dir (optional) Valid values are scrollTop and scrollLeft. Defaults to scrollTop.
         * @private
         */
        scrollByDelta: function(delta, dir) {
            dir = dir || 'scrollTop';
            var elDom = this.el.dom;
            elDom[dir] = (elDom[dir] += delta);
        },

        onUpdate: function(ds, index) {
            this.callParent(arguments);
        },

        /**
         * Saves the scrollState in a private variable. Must be used in conjunction with restoreScrollState
         */
        saveScrollState: function() {
            if (this.rendered) {
                var dom = this.el.dom,
                    state = this.scrollState;

                state.left = dom.scrollLeft;
                state.top = dom.scrollTop;
            }
        },

        /**
         * Restores the scrollState.
         * Must be used in conjunction with saveScrollState
         * @private
         */
        restoreScrollState: function() {
            if (this.rendered) {
                var dom = this.el.dom,
                    state = this.scrollState,
                    headerEl = this.headerCt.el.dom;

                headerEl.scrollLeft = dom.scrollLeft = state.left;
                dom.scrollTop = state.top;
            }
        },

        /**
         * Refreshes the grid view. Saves and restores the scroll state, generates a new template, stripes rows and
         * invalidates the scrollers.
         */
        refresh: function() {
            this.setNewTemplate();
            this.callParent(arguments);
        },

        processItemEvent: function(record, row, rowIndex, e) {
            var me = this,
                cell = e.getTarget(me.cellSelector, row),
                cellIndex = cell ? cell.cellIndex : -1,
                map = me.statics().EventMap,
                selModel = me.getSelectionModel(),
                type = e.type,
                result;

            if (type == 'keydown' && !cell && selModel.getCurrentPosition) {
                // CellModel, otherwise we can't tell which cell to invoke
                cell = me.getCellByPosition(selModel.getCurrentPosition());
                if (cell) {
                    cell = cell.dom;
                    cellIndex = cell.cellIndex;
                }
            }

            result = me.fireEvent('uievent', type, me, cell, rowIndex, cellIndex, e);

            if (result === false || me.callParent(arguments) === false) {
                return false;
            }

            // Don't handle cellmouseenter and cellmouseleave events for now
            if (type == 'mouseover' || type == 'mouseout') {
                return true;
            }

            return !(
                // We are adding cell and feature events
                (me['onBeforeCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) ||
                (me.fireEvent('beforecell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false) ||
                (me['onCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) ||
                (me.fireEvent('cell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false)
            );
        },

        processSpecialEvent: function(e) {
            var me = this,
                map = me.statics().EventMap,
                features = me.features,
                ln = features.length,
                type = e.type,
                i, feature, prefix, featureTarget,
                beforeArgs, args,
                panel = me.ownerCt;

            me.callParent(arguments);

            if (type == 'mouseover' || type == 'mouseout') {
                return;
            }

            for (i = 0; i < ln; i++) {
                feature = features[i];
                if (feature.hasFeatureEvent) {
                    featureTarget = e.getTarget(feature.eventSelector, me.getTargetEl());
                    if (featureTarget) {
                        prefix = feature.eventPrefix;
                        // allows features to implement getFireEventArgs to change the
                        // fireEvent signature
                        beforeArgs = feature.getFireEventArgs('before' + prefix + type, me, featureTarget, e);
                        args = feature.getFireEventArgs(prefix + type, me, featureTarget, e);

                        if (
                            // before view event
                            (me.fireEvent.apply(me, beforeArgs) === false) ||
                            // panel grid event
                            (panel.fireEvent.apply(panel, beforeArgs) === false) ||
                            // view event
                            (me.fireEvent.apply(me, args) === false) ||
                            // panel event
                            (panel.fireEvent.apply(panel, args) === false)
                        ) {
                            return false;
                        }
                    }
                }
            }
            return true;
        },

        onCellMouseDown: Ext.emptyFn,
        onCellMouseUp: Ext.emptyFn,
        onCellClick: Ext.emptyFn,
        onCellDblClick: Ext.emptyFn,
        onCellContextMenu: Ext.emptyFn,
        onCellKeyDown: Ext.emptyFn,
        onBeforeCellMouseDown: Ext.emptyFn,
        onBeforeCellMouseUp: Ext.emptyFn,
        onBeforeCellClick: Ext.emptyFn,
        onBeforeCellDblClick: Ext.emptyFn,
        onBeforeCellContextMenu: Ext.emptyFn,
        onBeforeCellKeyDown: Ext.emptyFn,

        /**
         * Expands a particular header to fit the max content width.
         * This will ONLY expand, not contract.
         * @private
         */
        expandToFit: function(header) {
            if (header) {
                var maxWidth = this.getMaxContentWidth(header);
                delete header.flex;
                header.setWidth(maxWidth);
            }
        },

        /**
         * Returns the max contentWidth of the header's text and all cells
         * in the grid under this header.
         * @private
         */
        getMaxContentWidth: function(header) {
            var cellSelector = header.getCellInnerSelector(),
                cells        = this.el.query(cellSelector),
                i = 0,
                ln = cells.length,
                maxWidth = header.el.dom.scrollWidth,
                scrollWidth;

            for (; i < ln; i++) {
                scrollWidth = cells[i].scrollWidth;
                if (scrollWidth > maxWidth) {
                    maxWidth = scrollWidth;
                }
            }
            return maxWidth;
        },

        getPositionByEvent: function(e) {
            var me       = this,
                cellNode = e.getTarget(me.cellSelector),
                rowNode  = e.getTarget(me.itemSelector),
                record   = me.getRecord(rowNode),
                header   = me.getHeaderByCell(cellNode);

            return me.getPosition(record, header);
        },

        getHeaderByCell: function(cell) {
            if (cell) {
                var m = cell.className.match(this.cellRe);
                if (m && m[1]) {
                    return Ext.getCmp(m[1]);
                }
            }
            return false;
        },

        /**
         * @param {Object} position The current row and column: an object containing the following properties:
         *
         * - row - The row index
         * - column - The column index
         *
         * @param {String} direction 'up', 'down', 'right' and 'left'
         * @param {Ext.EventObject} e event
         * @param {Boolean} preventWrap Set to true to prevent wrap around to the next or previous row.
         * @param {Function} verifierFn A function to verify the validity of the calculated position.
         * When using this function, you must return true to allow the newPosition to be returned.
         * @param {Object} scope Scope to run the verifierFn in
         * @returns {Object} newPosition An object containing the following properties:
         *
         * - row - The row index
         * - column - The column index
         *
         * @private
         */
        walkCells: function(pos, direction, e, preventWrap, verifierFn, scope) {
            var me       = this,
                row      = pos.row,
                column   = pos.column,
                rowCount = me.store.getCount(),
                firstCol = me.getFirstVisibleColumnIndex(),
                lastCol  = me.getLastVisibleColumnIndex(),
                newPos   = {row: row, column: column},
                activeHeader = me.headerCt.getHeaderAtIndex(column);

            // no active header or its currently hidden
            if (!activeHeader || activeHeader.hidden) {
                return false;
            }

            e = e || {};
            direction = direction.toLowerCase();
            switch (direction) {
                case 'right':
                    // has the potential to wrap if its last
                    if (column === lastCol) {
                        // if bottom row and last column, deny right
                        if (preventWrap || row === rowCount - 1) {
                            return false;
                        }
                        if (!e.ctrlKey) {
                            // otherwise wrap to nextRow and firstCol
                            newPos.row = row + 1;
                            newPos.column = firstCol;
                        }
                    // go right
                    } else {
                        if (!e.ctrlKey) {
                            newPos.column = column + me.getRightGap(activeHeader);
                        } else {
                            newPos.column = lastCol;
                        }
                    }
                    break;

                case 'left':
                    // has the potential to wrap
                    if (column === firstCol) {
                        // if top row and first column, deny left
                        if (preventWrap || row === 0) {
                            return false;
                        }
                        if (!e.ctrlKey) {
                            // otherwise wrap to prevRow and lastCol
                            newPos.row = row - 1;
                            newPos.column = lastCol;
                        }
                    // go left
                    } else {
                        if (!e.ctrlKey) {
                            newPos.column = column + me.getLeftGap(activeHeader);
                        } else {
                            newPos.column = firstCol;
                        }
                    }
                    break;

                case 'up':
                    // if top row, deny up
                    if (row === 0) {
                        return false;
                    // go up
                    } else {
                        if (!e.ctrlKey) {
                            newPos.row = row - 1;
                        } else {
                            newPos.row = 0;
                        }
                    }
                    break;

                case 'down':
                    // if bottom row, deny down
                    if (row === rowCount - 1) {
                        return false;
                    // go down
                    } else {
                        if (!e.ctrlKey) {
                            newPos.row = row + 1;
                        } else {
                            newPos.row = rowCount - 1;
                        }
                    }
                    break;
            }

            if (verifierFn && verifierFn.call(scope || window, newPos) !== true) {
                return false;
            } else {
                return newPos;
            }
        },
        getFirstVisibleColumnIndex: function() {
            var headerCt   = this.getHeaderCt(),
                allColumns = headerCt.getGridColumns(),
                visHeaders = Ext.ComponentQuery.query(':not([hidden])', allColumns),
                firstHeader = visHeaders[0];

            return headerCt.getHeaderIndex(firstHeader);
        },

        getLastVisibleColumnIndex: function() {
            var headerCt   = this.getHeaderCt(),
                allColumns = headerCt.getGridColumns(),
                visHeaders = Ext.ComponentQuery.query(':not([hidden])', allColumns),
                lastHeader = visHeaders[visHeaders.length - 1];

            return headerCt.getHeaderIndex(lastHeader);
        },

        getHeaderCt: function() {
            return this.headerCt;
        },

        getPosition: function(record, header) {
            var me = this,
                store = me.store,
                gridCols = me.headerCt.getGridColumns();

            return {
                row: store.indexOf(record),
                column: Ext.Array.indexOf(gridCols, header)
            };
        },

        /**
         * Determines the 'gap' between the closest adjacent header to the right
         * that is not hidden.
         * @private
         */
        getRightGap: function(activeHeader) {
            var headerCt        = this.getHeaderCt(),
                headers         = headerCt.getGridColumns(),
                activeHeaderIdx = Ext.Array.indexOf(headers, activeHeader),
                i               = activeHeaderIdx + 1,
                nextIdx;

            for (; i <= headers.length; i++) {
                if (!headers[i].hidden) {
                    nextIdx = i;
                    break;
                }
            }

            return nextIdx - activeHeaderIdx;
        },

        beforeDestroy: function() {
            if (this.rendered) {
                this.el.removeAllListeners();
            }
            this.callParent(arguments);
        },

        /**
         * Determines the 'gap' between the closest adjacent header to the left
         * that is not hidden.
         * @private
         */
        getLeftGap: function(activeHeader) {
            var headerCt        = this.getHeaderCt(),
                headers         = headerCt.getGridColumns(),
                activeHeaderIdx = Ext.Array.indexOf(headers, activeHeader),
                i               = activeHeaderIdx - 1,
                prevIdx;

            for (; i >= 0; i--) {
                if (!headers[i].hidden) {
                    prevIdx = i;
                    break;
                }
            }

            return prevIdx - activeHeaderIdx;
        }
    });