    /**
     * @class Ext.grid.PagingScroller
     * @extends Ext.grid.Scroller
     */
    Ext.define('Ext.grid.PagingScroller', {
        extend: 'Ext.grid.Scroller',
        alias: 'widget.paginggridscroller',
        //renderTpl: null,
        //tpl: [
        //    '<tpl for="pages">',
        //        '<div class="' + Ext.baseCSSPrefix + 'stretcher" style="width: {width}px;height: {height}px;"></div>',
        //    '</tpl>'
        //],
        /**
         * @cfg {Number} percentageFromEdge This is a number above 0 and less than 1 which specifies
         * at what percentage to begin fetching the next page. For example if the pageSize is 100
         * and the percentageFromEdge is the default of 0.35, the paging scroller will prefetch pages
         * when scrolling up between records 0 and 34 and when scrolling down between records 65 and 99.
         */
        percentageFromEdge: 0.35,

        /**
         * @cfg {Number} scrollToLoadBuffer This is the time in milliseconds to buffer load requests
         * when scrolling the PagingScrollbar.
         */
        scrollToLoadBuffer: 200,

        activePrefetch: true,

        chunkSize: 50,
        snapIncrement: 25,

        syncScroll: true,

        initComponent: function() {
            var me = this,
                ds = me.store;

            ds.on('guaranteedrange', me.onGuaranteedRange, me);
            me.callParent(arguments);
        },

        onGuaranteedRange: function(range, start, end) {
            var me = this,
                ds = me.store,
                rs;
            // this should never happen
            if (range.length && me.visibleStart < range[0].index) {
                return;
            }

            ds.loadRecords(range);

            if (!me.firstLoad) {
                if (me.rendered) {
                    me.invalidate();
                } else {
                    me.on('afterrender', me.invalidate, me, {single: true});
                }
                me.firstLoad = true;
            } else {
                // adjust to visible
                // only sync if there is a paging scrollbar element and it has a scroll height (meaning it's currently in the DOM)
                if (me.scrollEl && me.scrollEl.dom && me.scrollEl.dom.scrollHeight) {
                    me.syncTo();
                }
            }
        },

        syncTo: function() {
            var me            = this,
                pnl           = me.getPanel(),
                store         = pnl.store,
                scrollerElDom = this.scrollEl.dom,
                rowOffset     = me.visibleStart - store.guaranteedStart,
                scrollBy      = rowOffset * me.rowHeight,
                scrollHeight  = scrollerElDom.scrollHeight,
                clientHeight  = scrollerElDom.clientHeight,
                scrollTop     = scrollerElDom.scrollTop,
                useMaximum;

            // BrowserBug: clientHeight reports 0 in IE9 StrictMode
            // Instead we are using offsetHeight and hardcoding borders
            if (Ext.isIE9 && Ext.isStrict) {
                clientHeight = scrollerElDom.offsetHeight + 2;
            }

            // This should always be zero or greater than zero but staying
            // safe and less than 0 we'll scroll to the bottom.
            useMaximum = (scrollHeight - clientHeight - scrollTop <= 0);
            this.setViewScrollTop(scrollBy, useMaximum);
        },

        getPageData : function(){
            var panel = this.getPanel(),
                store = panel.store,
                totalCount = store.getTotalCount();

            return {
                total : totalCount,
                currentPage : store.currentPage,
                pageCount: Math.ceil(totalCount / store.pageSize),
                fromRecord: ((store.currentPage - 1) * store.pageSize) + 1,
                toRecord: Math.min(store.currentPage * store.pageSize, totalCount)
            };
        },

        onElScroll: function(e, t) {
            var me = this,
                panel = me.getPanel(),
                store = panel.store,
                pageSize = store.pageSize,
                guaranteedStart = store.guaranteedStart,
                guaranteedEnd = store.guaranteedEnd,
                totalCount = store.getTotalCount(),
                numFromEdge = Math.ceil(me.percentageFromEdge * pageSize),
                position = t.scrollTop,
                visibleStart = Math.floor(position / me.rowHeight),
                view = panel.down('tableview'),
                viewEl = view.el,
                visibleHeight = viewEl.getHeight(),
                visibleAhead = Math.ceil(visibleHeight / me.rowHeight),
                visibleEnd = visibleStart + visibleAhead,
                prevPage = Math.floor(visibleStart / pageSize),
                nextPage = Math.floor(visibleEnd / pageSize) + 2,
                lastPage = Math.ceil(totalCount / pageSize),
                snap = me.snapIncrement,
                requestStart = Math.floor(visibleStart / snap) * snap,
                requestEnd = requestStart + pageSize - 1,
                activePrefetch = me.activePrefetch;

            me.visibleStart = visibleStart;
            me.visibleEnd = visibleEnd;

            me.syncScroll = true;
            if (totalCount >= pageSize) {
                // end of request was past what the total is, grab from the end back a pageSize
                if (requestEnd > totalCount - 1) {
                    me.cancelLoad();
                    if (store.rangeSatisfied(totalCount - pageSize, totalCount - 1)) {
                        me.syncScroll = true;
                    }
                    store.guaranteeRange(totalCount - pageSize, totalCount - 1);
                // Out of range, need to reset the current data set
                } else if (visibleStart <= guaranteedStart || visibleEnd > guaranteedEnd) {
                    if (visibleStart <= guaranteedStart) {
                        // need to scroll up
                        requestStart -= snap;
                        requestEnd -= snap;

                        if (requestStart < 0) {
                            requestStart = 0;
                            requestEnd = pageSize;
                        }
                    }
                    if (store.rangeSatisfied(requestStart, requestEnd)) {
                        me.cancelLoad();
                        store.guaranteeRange(requestStart, requestEnd);
                    } else {
                        store.mask();
                        me.attemptLoad(requestStart, requestEnd);
                    }
                    // dont sync the scroll view immediately, sync after the range has been guaranteed
                    me.syncScroll = false;
                } else if (activePrefetch && visibleStart < (guaranteedStart + numFromEdge) && prevPage > 0) {
                    me.syncScroll = true;
                    store.prefetchPage(prevPage);
                } else if (activePrefetch && visibleEnd > (guaranteedEnd - numFromEdge) && nextPage < lastPage) {
                    me.syncScroll = true;
                    store.prefetchPage(nextPage);
                }
            }

            if (me.syncScroll) {
                me.syncTo();
            }
        },

        getSizeCalculation: function() {
            // Use the direct ownerCt here rather than the scrollerOwner
            // because we are calculating widths/heights.
            var me     = this,
                owner  = me.ownerGrid,
                view   = owner.getView(),
                store  = me.store,
                dock   = me.dock,
                elDom  = me.el.dom,
                width  = 1,
                height = 1;

            if (!me.rowHeight) {
                me.rowHeight = view.el.down(view.getItemSelector()).getHeight(false, true);
            }

            // If the Store is *locally* filtered, use the filtered count from getCount.
            height = store[(!store.remoteFilter && store.isFiltered()) ? 'getCount' : 'getTotalCount']() * me.rowHeight;

            if (isNaN(width)) {
                width = 1;
            }
            if (isNaN(height)) {
                height = 1;
            }
            return {
                width: width,
                height: height
            };
        },

        attemptLoad: function(start, end) {
            var me = this;
            if (!me.loadTask) {
                me.loadTask = Ext.create('Ext.util.DelayedTask', me.doAttemptLoad, me, []);
            }
            me.loadTask.delay(me.scrollToLoadBuffer, me.doAttemptLoad, me, [start, end]);
        },

        cancelLoad: function() {
            if (this.loadTask) {
                this.loadTask.cancel();
            }
        },

        doAttemptLoad:  function(start, end) {
            var store = this.getPanel().store;
            store.guaranteeRange(start, end);
        },

        setViewScrollTop: function(scrollTop, useMax) {
            var me = this,
                owner = me.getPanel(),
                items = owner.query('tableview'),
                i = 0,
                len = items.length,
                center,
                centerEl,
                calcScrollTop,
                maxScrollTop,
                scrollerElDom = me.el.dom;

            owner.virtualScrollTop = scrollTop;

            center = items[1] || items[0];
            centerEl = center.el.dom;

            maxScrollTop = ((owner.store.pageSize * me.rowHeight) - centerEl.clientHeight);
            calcScrollTop = (scrollTop % ((owner.store.pageSize * me.rowHeight) + 1));
            if (useMax) {
                calcScrollTop = maxScrollTop;
            }
            if (calcScrollTop > maxScrollTop) {
                //Ext.Error.raise("Calculated scrollTop was larger than maxScrollTop");
                return;
                // calcScrollTop = maxScrollTop;
            }
            for (; i < len; i++) {
                items[i].el.dom.scrollTop = calcScrollTop;
            }
        }
    });