    /**
     * @private
     *
     * Lockable is a private mixin which injects lockable behavior into any
     * TablePanel subclass such as GridPanel or TreePanel. TablePanel will
     * automatically inject the Ext.grid.locking.Lockable mixin in when one of the
     * these conditions are met:
     *
     *  - The TablePanel has the lockable configuration set to true
     *  - One of the columns in the TablePanel has locked set to true/false
     *
     * Each TablePanel subclass must register an alias. It should have an array
     * of configurations to copy to the 2 separate tablepanel's that will be generated
     * to note what configurations should be copied. These are named normalCfgCopy and
     * lockedCfgCopy respectively.
     *
     * Columns which are locked must specify a fixed width. They do NOT support a
     * flex width.
     *
     * Configurations which are specified in this class will be available on any grid or
     * tree which is using the lockable functionality.
     */
    Ext.define('Ext.grid.locking.Lockable', {
        alternateClassName: 'Ext.grid.Lockable',

        requires: [
            'Ext.grid.locking.View',
            'Ext.grid.header.Container',
            'Ext.grid.locking.HeaderContainer',
            'Ext.view.Table'
        ],

        /**
         * @cfg {Boolean} syncRowHeight Synchronize rowHeight between the normal and
         * locked grid view. This is turned on by default. If your grid is guaranteed
         * to have rows of all the same height, you should set this to false to
         * optimize performance.
         */
        syncRowHeight: true,

        /**
         * @cfg {String} subGridXType The xtype of the subgrid to specify. If this is
         * not specified lockable will determine the subgrid xtype to create by the
         * following rule. Use the superclasses xtype if the superclass is NOT
         * tablepanel, otherwise use the xtype itself.
         */

        /**
         * @cfg {Object} lockedViewConfig A view configuration to be applied to the
         * locked side of the grid. Any conflicting configurations between lockedViewConfig
         * and viewConfig will be overwritten by the lockedViewConfig.
         */

        /**
         * @cfg {Object} normalViewConfig A view configuration to be applied to the
         * normal/unlocked side of the grid. Any conflicting configurations between normalViewConfig
         * and viewConfig will be overwritten by the normalViewConfig.
         */

        headerCounter: 0,

        /**
         * @cfg {Number} scrollDelta
         * Number of pixels to scroll when scrolling the locked section with mousewheel.
         */
        scrollDelta: 40,

        /**
         * @cfg {Object} lockedGridConfig
         * Any special configuration options for the locked part of the grid
         */

        /**
         * @cfg {Object} normalGridConfig
         * Any special configuration options for the normal part of the grid
         */

        lockedGridCls: Ext.baseCSSPrefix + 'grid-inner-locked',

        // i8n text
        //<locale>
        unlockText: 'Unlock',
        //</locale>
        //<locale>
        lockText: 'Lock',
        //</locale>

        // Required for the Lockable Mixin. These are the configurations which will be copied to the
        // normal and locked sub tablepanels
        bothCfgCopy: [
            'invalidateScrollerOnRefresh',
            'hideHeaders',
            'enableColumnHide',
            'enableColumnMove',
            'enableColumnResize',
            'sortableColumns',
            'columnLines',
            'rowLines'
        ],
        normalCfgCopy: [
            'verticalScroller',
            'verticalScrollDock',
            'verticalScrollerType',
            'scroll'
        ],
        lockedCfgCopy: [],

        determineXTypeToCreate: function(lockedSide) {
            var me = this,
                typeToCreate,
                xtypes, xtypesLn, xtype, superxtype;

            if (me.subGridXType) {
                typeToCreate = me.subGridXType;
            } else {
                // Treeness only moves down into the locked side.
                // The normal side is always just a grid
                if (!lockedSide) {
                    return 'gridpanel';
                }
                xtypes     = this.getXTypes().split('/');
                xtypesLn   = xtypes.length;
                xtype      = xtypes[xtypesLn - 1];
                superxtype = xtypes[xtypesLn - 2];

                if (superxtype !== 'tablepanel') {
                    typeToCreate = superxtype;
                } else {
                    typeToCreate = xtype;
                }
            }

            return typeToCreate;
        },

        // injectLockable will be invoked before initComponent's parent class implementation
        // is called, so throughout this method this. are configurations
        injectLockable: function() {
            // ensure lockable is set to true in the TablePanel
            this.lockable = true;
            // Instruct the TablePanel it already has a view and not to create one.
            // We are going to aggregate 2 copies of whatever TablePanel we are using
            this.hasView = true;

            var me = this,
                scrollbarHeight = Ext.getScrollbarSize().height,
                store = me.store = Ext.StoreManager.lookup(me.store),

                // share the selection model
                selModel = me.getSelectionModel(),

                // Hash of {lockedFeatures:[],normalFeatures:[]}
                allFeatures,

                // Hash of {topPlugins:[],lockedPlugins:[],normalPlugins:[]}
                allPlugins,

                lockedGrid,
                normalGrid,
                i,
                columns,
                lockedHeaderCt,
                normalHeaderCt,
                lockedView,
                normalView,
                listeners,
                loadMask = me.viewConfig && me.viewConfig.loadMask,
                bufferedRenderer = me.findPlugin('bufferedrenderer');

            allFeatures = me.constructLockableFeatures();

            // This is just a "shell" Panel which acts as a Container for the two grids and must not use the features
            if (me.features) {
                me.features = null;
            }

            // Distribute plugins to whichever Component needs them
            allPlugins = me.constructLockablePlugins();
            me.plugins = allPlugins.topPlugins;

            lockedGrid = Ext.apply({
                id: me.id + '-locked',
                isLocked: true,
                ownerLockable: me,
                xtype: me.determineXTypeToCreate(true),
                store: store,
                scrollerOwner: false,
                // Lockable does NOT support animations for Tree
                // Because the right side is just a grid, and the grid view doen't animate bulk insertions/removals
                animate: false,
                // If the OS does not show a space-taking scrollbar, the locked view can be overflow-y:auto
                scroll: scrollbarHeight ? false : 'vertical',
                selModel: selModel,
                border: false,
                cls: me.lockedGridCls,
                isLayoutRoot: function() {
                    return false;
                },
                features: allFeatures.lockedFeatures,
                plugins: allPlugins.lockedPlugins
            }, me.lockedGridConfig);

            normalGrid = Ext.apply({
                id: me.id + '-normal',
                isLocked: false,
                ownerLockable: me,
                xtype: me.determineXTypeToCreate(),
                store: store,
                scrollerOwner: false,
                selModel: selModel,
                border: false,
                isLayoutRoot: function() {
                    return false;
                },
                features: allFeatures.normalFeatures,
                plugins: allPlugins.normalPlugins
            }, me.normalGridConfig);

            me.addCls(Ext.baseCSSPrefix + 'grid-locked');

            // Copy appropriate configurations to the respective aggregated tablepanel instances.
            // Pass 4th param true to NOT exclude those settings on our prototype.
            // Delete them from the master tablepanel.
            Ext.copyTo(normalGrid, me, me.bothCfgCopy, true);
            Ext.copyTo(lockedGrid, me, me.bothCfgCopy, true);
            Ext.copyTo(normalGrid, me, me.normalCfgCopy, true);
            Ext.copyTo(lockedGrid, me, me.lockedCfgCopy, true);
            for (i = 0; i < me.normalCfgCopy.length; i++) {
                delete me[me.normalCfgCopy[i]];
            }
            for (i = 0; i < me.lockedCfgCopy.length; i++) {
                delete me[me.lockedCfgCopy[i]];
            }

            me.addEvents(
                /**
                 * @event processcolumns
                 * Fires when the configured (or **reconfigured**) column set is split into two depending on the {@link Ext.grid.column.Column#locked locked} flag.
                 * @param {Ext.grid.column.Column[]} lockedColumns The locked columns.
                 * @param {Ext.grid.column.Column[]} normalColumns The normal columns.
                 */
                'processcolumns',

                /**
                 * @event lockcolumn
                 * Fires when a column is locked.
                 * @param {Ext.grid.Panel} this The gridpanel.
                 * @param {Ext.grid.column.Column} column The column being locked.
                 */
                'lockcolumn',

                /**
                 * @event unlockcolumn
                 * Fires when a column is unlocked.
                 * @param {Ext.grid.Panel} this The gridpanel.
                 * @param {Ext.grid.column.Column} column The column being unlocked.
                 */
                'unlockcolumn'
            );

            me.addStateEvents(['lockcolumn', 'unlockcolumn']);

            columns = me.processColumns(me.columns);

            // Locked grid has a right border. Locked grid must shrinkwrap columns with no horiontal scroll, so
            // we must increment by the border width: 1px. TODO: Use shrinkWrapDock on the locked grid when it works.
            lockedGrid.width = columns.lockedWidth + Ext.num(selModel.headerWidth, 0) + (columns.locked.items.length ? 1 : 0);
            lockedGrid.columns = columns.locked;
            normalGrid.columns = columns.normal;

            // normal grid should flex the rest of the width
            normalGrid.flex = 1;
            lockedGrid.viewConfig = me.lockedViewConfig || {};
            lockedGrid.viewConfig.loadingUseMsg = false;
            lockedGrid.viewConfig.loadMask = false;

            // Padding below locked grid body only if there are scrollbars on this system
            // This takes the place of any spacer element.
            // Important: The height has to be postprocessed after render to override
            // the inline border styles injected by automatic component border settings.
            if (scrollbarHeight) {
                lockedGrid.viewConfig.style = 'border-bottom:' + scrollbarHeight +
                    'px solid #f6f6f6;' + (lockedGrid.viewConfig.style || '');
            }

            normalGrid.viewConfig = me.normalViewConfig || {};
            normalGrid.viewConfig.loadMask = false;

            //<debug>
            if (me.viewConfig && me.viewConfig.id) {
                Ext.log.warn('id specified on Lockable viewConfig, it will be shared between both views: "' + me.viewConfig.id + '"');
            }
            //</debug>

            Ext.applyIf(lockedGrid.viewConfig, me.viewConfig);
            Ext.applyIf(normalGrid.viewConfig, me.viewConfig);

            me.lockedGrid = Ext.ComponentManager.create(lockedGrid);

            if (me.isTree) {
                // Tree must not animate because the partner grid is unable to animate
                me.lockedGrid.getView().animate = false;

                // When this is a locked tree, the normal side is just a gridpanel, so needs the flat NodeStore
                normalGrid.store = me.lockedGrid.view.store;

                // To stay in sync with the tree side; trees don't use deferInitialRefresh by default. Overrideable by user config.
                normalGrid.deferRowRender = false;

                // Match configs between sides
                normalGrid.viewConfig.stripeRows = me.lockedGrid.view.stripeRows;
                normalGrid.rowLines = me.lockedGrid.rowLines;
            }

            // Set up a bidirectional relationship between the two sides of the locked view
            lockedView = me.lockedGrid.getView();
            normalGrid.viewConfig.lockingPartner = lockedView;
            me.normalGrid = Ext.ComponentManager.create(normalGrid);
            lockedView.lockingPartner = normalView = me.normalGrid.getView();

            me.view = new Ext.grid.locking.View({
                loadingText: normalView.loadingText,
                loadingCls: normalView.loadingCls,
                loadingUseMsg: normalView.loadingUseMsg,
                // viewConfig.loadMask === false takes priority over me.loadMask
                loadMask: loadMask === undefined ? me.loadMask : loadMask,
                locked: me.lockedGrid,
                normal: me.normalGrid,
                panel: me
            });

            // Set up listeners for the locked view. If its SelModel ever scrolls it, the normal view must sync
            listeners = bufferedRenderer ? {} : {
                scroll: {
                    fn: me.onLockedViewScroll,
                    element: 'el',
                    scope: me
                }
            };

            // If there are system scrollbars, we have to monitor the mousewheel and fake a scroll
            // Also we need to postprocess the border width because of inline border setting styles.
            // The locked grid needs a bottom border to match with any scrollbar present in the normal grid .
            // Keep locked section's bottom border width synched
            if (scrollbarHeight) {
                me.lockedGrid.on({
                    afterlayout: me.afterLockedViewLayout,
                    scope: me
                });

                // Ensure the overflow flags have been calculated from the various overflow configs
                lockedView.getOverflowStyle();

                // If the locked view is configured to scoll vertically, force the columns to fit initially.
                if (lockedView.scrollFlags.y) {
                    me.lockedGrid.headerCt.forceFit = true;
                }
                // If the locked view is not configured to scoll vertically, we must listen for mousewheel events to scroll it.
                else {
                    listeners.mousewheel = {
                        fn: me.onLockedViewMouseWheel,
                        element: 'el',
                        scope: me
                    };
                }
            }
            lockedView.on(listeners);

            // Set up listeners for the normal view
            listeners = bufferedRenderer ? {} : {
                scroll: {
                    fn: me.onNormalViewScroll,
                    element: 'el',
                    scope: me
                },
                scope: me
            };
            normalView.on(listeners);

            lockedHeaderCt = me.lockedGrid.headerCt;
            normalHeaderCt = me.normalGrid.headerCt;

            // The top grid, and the LockingView both need to have a headerCt which is usable.
            // It is part of their private API that framework code uses when dealing with a grid or grid view
            me.headerCt = me.view.headerCt = new Ext.grid.locking.HeaderContainer(me);

            lockedHeaderCt.lockedCt = true;
            lockedHeaderCt.lockableInjected = true;
            normalHeaderCt.lockableInjected = true;

            lockedHeaderCt.on({
                // buffer to wait until multiple adds have fired
                add: {
                    buffer: 1,
                    scope: me,
                    fn: me.onLockedHeaderAdd
                },
                columnshow: me.onLockedHeaderShow,
                columnhide: me.onLockedHeaderHide,
                sortchange: me.onLockedHeaderSortChange,
                columnresize: me.onLockedHeaderResize,
                scope: me
            });

            normalHeaderCt.on({
                sortchange: me.onNormalHeaderSortChange,
                scope: me
            });

            me.modifyHeaderCt();
            me.items = [me.lockedGrid, me.normalGrid];

            me.relayHeaderCtEvents(lockedHeaderCt);
            me.relayHeaderCtEvents(normalHeaderCt);

            // The top level Lockable container does not get bound to the store, so we need to programatically add the relayer so that
            // The filterchange state event is fired.
            me.storeRelayers = me.relayEvents(store, [
                /**
                 * @event filterchange
                 * @inheritdoc Ext.data.Store#filterchange
                 */
                'filterchange',
                /**
                 * @event groupchange
                 * @inheritdoc Ext.data.Store#groupchange
                 */
                'groupchange'
            ]);

            me.layout = {
                type: 'hbox',
                align: 'stretch'
            };
        },

        getLockingViewConfig: function(){
            return {
                xclass: 'Ext.grid.locking.View',
                locked: this.lockedGrid,
                normal: this.normalGrid,
                panel: this
            };
        },

        processColumns: function(columns) {
            // split apart normal and locked
            var i,
                len,
                column,
                cp = new Ext.grid.header.Container(),
                lockedHeaders = [],
                normalHeaders = [],
                lockedHeaderCt = {
                    itemId: 'lockedHeaderCt',
                    stretchMaxPartner: '^^>>#normalHeaderCt',
                    items: lockedHeaders
                },
                normalHeaderCt = {
                    itemId: 'normalHeaderCt',
                    stretchMaxPartner: '^^>>#lockedHeaderCt',
                    items: normalHeaders
                },
                result = {
                    lockedWidth: 0,
                    locked: lockedHeaderCt,
                    normal: normalHeaderCt
                };

            // In case they specified a config object with items...
            if (Ext.isObject(columns)) {
                Ext.applyIf(lockedHeaderCt, columns);
                Ext.applyIf(normalHeaderCt, columns);
                Ext.apply(cp, columns);
                columns = columns.items;
            }

            for (i = 0, len = columns.length; i < len; ++i) {
                column = columns[i];

                // Use the HeaderContainer object to correctly configure and create the column.
                // MUST instantiate now because the locked or autoLock config which we read here might be in the prototype.
                // MUST use a Container instance so that defaults from an object columns config get applied.
                if (!column.isComponent) {
                    column = cp.lookupComponent(cp.applyDefaults(column));
                }

                // mark the column as processed so that the locked attribute does not
                // trigger the locked subgrid to try to become a split lockable grid itself.
                column.processed = true;
                if (column.locked || column.autoLock) {
                    if (!column.hidden) {
                        result.lockedWidth += this.getColumnWidth(column) || cp.defaultWidth;
                    }
                    lockedHeaders.push(column);
                } else {
                    normalHeaders.push(column);
                }
                if (!column.headerId) {
                    column.headerId = (column.initialConfig || column).id || ('h' + (++this.headerCounter));
                }
            }
            this.fireEvent('processcolumns', this, lockedHeaders, normalHeaders);
            cp.destroy();
            return result;
        },

        // Used when calculating total locked column width in processColumns
        // Use shrinkwrapping of child columns if no top level width.
        getColumnWidth: function(column) {
            var result = column.width || 0,
                subcols, len, i;

            // <debug>
            if (column.flex) {
                Ext.Error.raise("Columns which are locked do NOT support a flex width. You must set a width on the " + column.text + "column.");
            }
            // </debug>
            if (!result && column.isGroupHeader) {
                subcols = column.items.items;
                len = subcols.length;
                for (i = 0; i < len; i++) {
                    result += this.getColumnWidth(subcols[i]);
                }
            }
            return result;
        },

        // Due to automatic component border setting using inline style, to create the scrollbar-replacing
        // bottom border, we have to postprocess the locked view *after* render.
        // If there are visible normal columns, we do need the fat bottom border.
        afterLockedViewLayout: function() {
            var me = this,
                lockedView = me.lockedGrid.getView(),
                lockedViewEl = lockedView.el.dom,
                spacerHeight = (me.normalGrid.headerCt.tooNarrow ? Ext.getScrollbarSize().height : 0);

            // If locked view is configured to scroll horizontally, and it overflows horizontally, then we do not need the spacer
            if (lockedView.scrollFlags.x && lockedViewEl.scrollWidth > lockedViewEl.clientWidth) {
                spacerHeight = 0;
            }

            lockedView.el.dom.style.borderBottomWidth = spacerHeight + 'px';

            // Legacy IE browsers which cannot be forced to use the sensible border box model.
            // We have to account in the element's style height for flip-flopping border width
            if (!Ext.isBorderBox) {
                lockedView.el.setHeight(lockedView.lastBox.height);
            }
        },

        /**
         * @private
         * Listen for mousewheel events on the locked section which does not scroll.
         * Scroll it in response, and the other section will automatically sync.
         */
        onLockedViewMouseWheel: function(e) {
            var me = this,
                scrollDelta = -me.scrollDelta,
                deltaY = scrollDelta * e.getWheelDeltas().y,
                vertScrollerEl = me.lockedGrid.getView().el.dom,
                verticalCanScrollDown, verticalCanScrollUp;

            if (!me.ignoreMousewheel) {
                if (vertScrollerEl) {
                    verticalCanScrollDown = vertScrollerEl.scrollTop !== vertScrollerEl.scrollHeight - vertScrollerEl.clientHeight;
                    verticalCanScrollUp   = vertScrollerEl.scrollTop !== 0;
                }

                if ((deltaY < 0 && verticalCanScrollUp) || (deltaY > 0 && verticalCanScrollDown)) {
                    e.stopEvent();

                    // Inhibit processing of any scroll events we *may* cause here.
                    // Some OSs do not fire a scroll event when we set the scrollTop of an overflow:hidden element,
                    // so we invoke the scroll handler programatically below.
                    vertScrollerEl.scrollTop += deltaY;
                    me.normalGrid.getView().el.dom.scrollTop = vertScrollerEl.scrollTop;

                    // Invoke the scroll event handler programatically to sync everything.
                    me.onNormalViewScroll();
                }
            }
        },

        onLockedViewScroll: function() {
            var me = this,
                lockedView = me.lockedGrid.getView(),
                normalView = me.normalGrid.getView(),
                normalDom = normalView.el.dom,
                lockedDom = lockedView.el.dom,
                normalTable,
                lockedTable;

            // See onNormalViewScroll
            if (normalDom.scrollTop !== lockedDom.scrollTop) {
                normalDom.scrollTop = lockedDom.scrollTop;

                // For buffered views, the absolute position is important as well as scrollTop
                if (me.store.buffered) {
                    lockedTable = lockedView.el.child('table', true);
                    normalTable = normalView.el.child('table', true);
                    normalTable.style.position = 'absolute';
                    normalTable.style.top = lockedTable.style.top;
                }
            }
        },

        onNormalViewScroll: function() {
            var me = this,
                lockedView = me.lockedGrid.getView(),
                normalView = me.normalGrid.getView(),
                normalDom = normalView.el.dom,
                lockedDom = lockedView.el.dom,
                normalTable,
                lockedTable;

            // When we set the position, it will cause a scroll event on the other view, so we need to
            // check the top here. We can't just set a flag, like:
            // if (!me.scrolling) {
            //     me.scrolling = true;
            //     other.scrollTop = this.scrollTop;
            //     me.scrolling = false;
            // }
            // The browser waits until the "thread" finishes before setting the
            // top, so by the time we set scrolling = false it hasn't hit the other
            // scroll event yet
            if (normalDom.scrollTop !== lockedDom.scrollTop) {
                lockedDom.scrollTop = normalDom.scrollTop;

                // For buffered views, the absolute position is important as well as scrollTop
                if (me.store.buffered) {
                    lockedTable = lockedView.el.child('table', true);
                    normalTable = normalView.el.child('table', true);
                    lockedTable.style.position = 'absolute';
                    lockedTable.style.top = normalTable.style.top;
                }
            }
        },

        /**
         * Synchronizes the row heights between the locked and non locked portion of the grid for each
         * row. If one row is smaller than the other, the height will be increased to match the larger one.
         */
        syncRowHeights: function() {
            var me = this,
                i,
                lockedView = me.lockedGrid.getView(),
                normalView = me.normalGrid.getView(),
                lockedRowEls = lockedView.all.slice(),
                normalRowEls = normalView.all.slice(),
                ln = lockedRowEls.length,
                scrollTop;

            // Ensure there are an equal number of locked and normal rows before synchronization
            if (normalRowEls.length === ln) {

                // Loop thru all rows and ask the TableView class to sync the sides.
                for (i = 0; i < ln; i++) {
                    normalView.syncRowHeights(lockedRowEls[i], normalRowEls[i]);
                }

                // Synchronize the scrollTop positions of the two views
                scrollTop = normalView.el.dom.scrollTop;
                normalView.el.dom.scrollTop = scrollTop;
                lockedView.el.dom.scrollTop = scrollTop;
            }
        },

        // inject Lock and Unlock text
        // Hide/show Lock/Unlock options
        modifyHeaderCt: function() {
            var me = this;
            me.lockedGrid.headerCt.getMenuItems = me.getMenuItems(me.lockedGrid.headerCt.getMenuItems, true);
            me.normalGrid.headerCt.getMenuItems = me.getMenuItems(me.normalGrid.headerCt.getMenuItems, false);
            me.lockedGrid.headerCt.showMenuBy = Ext.Function.createInterceptor(me.lockedGrid.headerCt.showMenuBy, me.showMenuBy);
            me.normalGrid.headerCt.showMenuBy = Ext.Function.createInterceptor(me.normalGrid.headerCt.showMenuBy, me.showMenuBy);
        },

        onUnlockMenuClick: function() {
            this.unlock();
        },

        onLockMenuClick: function() {
            this.lock();
        },

        showMenuBy: function(t, header) {
            var menu = this.getMenu(),
                unlockItem  = menu.down('#unlockItem'),
                lockItem = menu.down('#lockItem'),
                sep = unlockItem.prev();

            if (header.lockable === false) {
                sep.hide();
                unlockItem.hide();
                lockItem.hide();
            } else {
                sep.show();
                unlockItem.show();
                lockItem.show();
                if (!unlockItem.initialConfig.disabled) {
                    unlockItem.setDisabled(header.lockable === false);
                }
                if (!lockItem.initialConfig.disabled) {
                    lockItem.setDisabled(!header.isLockable());
                }
            }
        },

        getMenuItems: function(getMenuItems, locked) {
            var me            = this,
                unlockText    = me.unlockText,
                lockText      = me.lockText,
                unlockCls     = Ext.baseCSSPrefix + 'hmenu-unlock',
                lockCls       = Ext.baseCSSPrefix + 'hmenu-lock',
                unlockHandler = Ext.Function.bind(me.onUnlockMenuClick, me),
                lockHandler   = Ext.Function.bind(me.onLockMenuClick, me);

            // runs in the scope of headerCt
            return function() {

                // We cannot use the method from HeaderContainer's prototype here
                // because other plugins or features may already have injected an implementation
                var o = getMenuItems.call(this);
                o.push('-', {
                    itemId: 'unlockItem',
                    iconCls: unlockCls,
                    text: unlockText,
                    handler: unlockHandler,
                    disabled: !locked
                });
                o.push({
                    itemId: 'lockItem',
                    iconCls: lockCls,
                    text: lockText,
                    handler: lockHandler,
                    disabled: locked
                });
                return o;
            };
        },

        /**
         * @private
         * Updates the overall view after columns have been resized, or moved from
         * the locked to unlocked side or vice-versa.
         *
         * If all columns are removed from either side, that side must be hidden, and the
         * sole remaining column owning grid then becomes *the* grid. It must flex to occupy the
         * whole of the locking view. And it must also allow scrolling.
         *
         * If columns are shared between the two sides, the *locked* grid shrinkwraps the
         * width of the visible locked columns while the normal grid flexes in what space remains.
         *
         * @return {Boolean} `true` if there are visible locked columns which need refreshing.
         *
         */
        syncLockedWidth: function() {
            var me = this,
                locked = me.lockedGrid,
                lockedView = locked.view,
                lockedViewEl = lockedView.el.dom,
                normal = me.normalGrid,
                lockedColCount = locked.headerCt.getVisibleGridColumns().length,
                normalColCount = normal.headerCt.getVisibleGridColumns().length;

            Ext.suspendLayouts();

            // If there are still visible normal columns, then the normal grid will flex
            // while we effectively shrinkwrap the width of the locked columns
            if (normalColCount) {
                normal.show();
                if (lockedColCount) {

                    // The locked grid shrinkwraps the total column width while the normal grid flexes in what remains
                    // UNLESS it has been set to forceFit
                    if (!locked.headerCt.forceFit) {
                        delete locked.flex;
                        // Don't pass the purge flag here
                        locked.setWidth(locked.headerCt.getFullWidth());
                    }
                    locked.addCls(me.lockedGridCls);
                    locked.show();
                } else {
                    // No visible locked columns: hide the locked grid
                    // We also need to trigger a refresh to clear out any
                    // old dom nodes
                    locked.getView().refresh();
                    locked.hide();
                }

                // Fix it so that the locked view scrolls the way it was initially configured to do.
                lockedView.el.setStyle(lockedView.getOverflowStyle());

                // Ignore mousewheel events if the view is configured to scroll vertically
                me.ignoreMousewheel = lockedView.scrollFlags.y;
            }

            // There are no normal grid columns. The "locked" grid has to be *the*
            // grid, and cannot have a shrinkwrapped width, but must flex the entire width.
            else {
                normal.hide();

                // When the normal grid is hidden, we no longer need the bottom border "scrollbar replacement"
                lockedViewEl.style.borderBottomWidth = '0';

                // The locked now becomes *the* grid and has to flex to occupy the full view width
                locked.flex = 1;
                delete locked.width;
                locked.removeCls(me.lockedGridCls);
                locked.show();

                // Fix it so that the "locked" has the same scroll settings as the normal view.
                // Because it is the only grid view visible now.
                lockedView.el.setStyle(normal.view.getOverflowStyle());
                me.ignoreMousewheel = true;
            }
            Ext.resumeLayouts(true);
            return [lockedColCount, normalColCount];
        },

        onLockedHeaderAdd: function() {
            // Columns can be added to the locked grid whwen reconfiguring or during a lock
            // operation when syncLockedWidth will be called anyway, so allow adding to be ignored
            if (!this.ignoreAddLockedColumn) {
                this.syncLockedWidth();
            }
        },

        onLockedHeaderResize: function() {
            this.syncLockedWidth();
        },

        onLockedHeaderHide: function() {
            this.syncLockedWidth();
        },

        onLockedHeaderShow: function() {
            this.syncLockedWidth();
        },

        onLockedHeaderSortChange: function(headerCt, header, sortState) {
            if (sortState) {
                // no real header, and silence the event so we dont get into an
                // infinite loop
                this.normalGrid.headerCt.clearOtherSortStates(null, true);
            }
        },

        onNormalHeaderSortChange: function(headerCt, header, sortState) {
            if (sortState) {
                // no real header, and silence the event so we dont get into an
                // infinite loop
                this.lockedGrid.headerCt.clearOtherSortStates(null, true);
            }
        },

        // going from unlocked section to locked
        /**
         * Locks the activeHeader as determined by which menu is open OR a header
         * as specified.
         * @param {Ext.grid.column.Column} [header] Header to unlock from the locked section.
         * Defaults to the header which has the menu open currently.
         * @param {Number} [toIdx] The index to move the unlocked header to.
         * Defaults to appending as the last item.
         * @private
         */
        lock: function(activeHd, toIdx, toCt) {
            var me         = this,
                normalGrid = me.normalGrid,
                lockedGrid = me.lockedGrid,
                normalHCt  = normalGrid.headerCt,
                refreshFlags,
                ownerCt;

            activeHd = activeHd || normalHCt.getMenu().activeHeader;
            toCt = toCt || lockedGrid.headerCt,
            ownerCt = activeHd.ownerCt;

            // isLockable will test for making the locked side too wide
            if (!activeHd.isLockable()) {
                return;
            }

            // if column was previously flexed, get/set current width
            // and remove the flex
            if (activeHd.flex) {
                activeHd.width = activeHd.getWidth();
                activeHd.flex = null;
            }

            Ext.suspendLayouts();
            ownerCt.remove(activeHd, false);
            activeHd.locked = true;

            // Flag to the locked column add listener to do nothing
            me.ignoreAddLockedColumn = true;
            if (Ext.isDefined(toIdx)) {
                toCt.insert(toIdx, activeHd);
            } else {
                toCt.add(activeHd);
            }
            me.ignoreAddLockedColumn = false;

            refreshFlags = me.syncLockedWidth();
            if (refreshFlags[0]) {
                lockedGrid.getView().refresh();
            }
            if (refreshFlags[1]) {
                normalGrid.getView().refresh();
            }
            Ext.resumeLayouts(true);

            me.fireEvent('lockcolumn', me, activeHd);
        },

        // going from locked section to unlocked
        /**
         * Unlocks the activeHeader as determined by which menu is open OR a header
         * as specified.
         * @param {Ext.grid.column.Column} [header] Header to unlock from the locked section.
         * Defaults to the header which has the menu open currently.
         * @param {Number} [toIdx=0] The index to move the unlocked header to.
         * @private
         */
        unlock: function(activeHd, toIdx, toCt) {
            var me         = this,
                normalGrid = me.normalGrid,
                lockedGrid = me.lockedGrid,
                lockedHCt  = lockedGrid.headerCt,
                refreshFlags;

            // Unlocking; user expectation is that the unlocked column is inserted at the beginning.
            if (!Ext.isDefined(toIdx)) {
                toIdx = 0;
            }
            activeHd = activeHd || lockedHCt.getMenu().activeHeader;
            toCt = toCt || normalGrid.headerCt;

            Ext.suspendLayouts();
            activeHd.ownerCt.remove(activeHd, false);
            activeHd.locked = false;
            toCt.insert(toIdx, activeHd);

            // syncLockedWidth returns visible column counts for both grids.
            // only refresh what needs refreshing
            refreshFlags = me.syncLockedWidth();

            if (refreshFlags[0]) {
                lockedGrid.getView().refresh();
            }
            if (refreshFlags[1]) {
                normalGrid.getView().refresh();
            }
            Ext.resumeLayouts(true);

            me.fireEvent('unlockcolumn', me, activeHd);
        },

        // we want to totally override the reconfigure behaviour here, since we're creating 2 sub-grids
        reconfigureLockable: function(store, columns) {
            var me = this,
                oldStore = me.store,
                lockedGrid = me.lockedGrid,
                normalGrid = me.normalGrid;

            Ext.suspendLayouts();
            if (columns) {
                lockedGrid.headerCt.removeAll();
                normalGrid.headerCt.removeAll();

                columns = me.processColumns(columns);

                // Flag to the locked column add listener to do nothing
                me.ignoreAddLockedColumn = true;
                lockedGrid.headerCt.add(columns.locked.items);
                me.ignoreAddLockedColumn = false;
                normalGrid.headerCt.add(columns.normal.items);

                // Ensure locked grid is set up correctly with correct width and bottom border,
                // and that both grids' visibility and scrollability status is correct
                me.syncLockedWidth();
            }

            if (store && store !== oldStore) {
                store = Ext.data.StoreManager.lookup(store);
                me.store = store;
                lockedGrid.bindStore(store);
                normalGrid.bindStore(store);
            } else {
                lockedGrid.getView().refresh();
                normalGrid.getView().refresh();
            }
            Ext.resumeLayouts(true);
        },

        constructLockableFeatures: function() {
            var features = this.features,
                feature,
                featureClone,
                lockedFeatures,
                normalFeatures,
                i = 0, len;

            if (features) {
                if (!Ext.isArray(features)) {
                    features = [ features ];
                }
                lockedFeatures = [];
                normalFeatures = [];
                len = features.length;
                for (; i < len; i++) {
                    feature = features[i];
                    if (!feature.isFeature) {
                        feature = Ext.create('feature.' + feature.ftype, feature);
                    }
                    switch (feature.lockableScope) {
                        case 'locked':
                            lockedFeatures.push(feature);
                            break;
                        case 'normal':
                            normalFeatures.push(feature);
                            break;
                        default:
                            feature.lockableScope = 'both';
                            lockedFeatures.push(feature);
                            normalFeatures.push(featureClone = feature.clone());

                            // When cloned to either side, each gets a "lockingPartner" reference to the other
                            featureClone.lockingPartner = feature;
                            feature.lockingPartner = featureClone;
                    }
                }
            }
            return {
                normalFeatures: normalFeatures,
                lockedFeatures: lockedFeatures
            };
        },

        constructLockablePlugins: function() {
            var plugins = this.plugins,
                plugin,
                normalPlugin,
                lockedPlugin,
                topPlugins,
                lockedPlugins,
                normalPlugins,
                i = 0, len,
                lockableScope,
                pluginCls;

            if (plugins) {
                if (!Ext.isArray(plugins)) {
                    plugins = [ plugins ];
                }
                topPlugins = [];
                lockedPlugins = [];
                normalPlugins = [];
                len = plugins.length;
                for (; i < len; i++) {

                    plugin = plugins[i];

                    // Plugin will most likely already have been instantiated by the AbstractComponent constructor
                    if (plugin.init) {
                        lockableScope = plugin.lockableScope;
                    }
                    // If not, it's because of late addition through a subclass's initComponent implementation, so we
                    // must ascertain the lockableScope directly from the class.
                    else {
                        pluginCls = plugin.ptype ? Ext.ClassManager.getByAlias(('plugin.' + plugin.ptype)) : Ext.ClassManager.get(plugin.xclass);
                        lockableScope = pluginCls.prototype.lockableScope;
                    }

                    switch (lockableScope) {
                        case 'both':
                            lockedPlugins.push(lockedPlugin = plugin.clonePlugin());
                            normalPlugins.push(normalPlugin = plugin.clonePlugin());

                            // When cloned to both sides, each gets a "lockingPartner" reference to the other
                            lockedPlugin.lockingPartner = normalPlugin;
                            normalPlugin.lockingPartner = lockedPlugin;

                            // If the plugin has to be propagated down to both, a new plugin config object must be given to that side
                            // and this plugin must be destroyed.
                            Ext.destroy(plugin);
                            break;
                        case 'locked':
                            lockedPlugins.push(plugin);
                            break;
                        case 'normal':
                            normalPlugins.push(plugin);
                            break;
                        default:
                            topPlugins.push(plugin);
                    }
                }
            }
            return {
                topPlugins:    topPlugins,
                normalPlugins: normalPlugins,
                lockedPlugins: lockedPlugins
            };
        },

        destroyLockable: function(){
            // The locking view isn't a "real" view, so we need to destroy it manually
            Ext.destroy(this.view, this.headerCt);
        }
    }, function() {
        this.borrow(Ext.AbstractComponent, ['constructPlugin']);
    });