/* eslint-disable max-len */
/**
 * This class specifies the definition for a column inside a {@link Ext.grid.Grid}. It
 * encompasses both the grid header configuration as well as displaying data within the
 * grid itself.
 *
 * In general an array of column configurations will be passed to the grid:
 *
 * ```javascript
 * @example({ framework: 'extjs' })
 * Ext.create({
 *     xtype: 'grid',
 *     title: 'Tree Grid Demo',
 *     itemConfig: {
 *         viewModel: true
 *     },
 *     store: {
 *          data: [
 *              {firstname:"Michael", lastname:"Scott", seniority:7, department:"Management", hired:"01/10/2004"},
 *              {firstname:"Dwight", lastname:"Schrute", seniority:2, department:"Sales", hired:"04/01/2004"},
 *              {firstname:"Jim", lastname:"Halpert", seniority:3, department:"Sales", hired:"02/22/2006"},
 *              {firstname:"Kevin", lastname:"Malone", seniority:4, department:"Accounting", hired:"06/10/2007"},
 *              {firstname:"Angela", lastname:"Martin", seniority:5, department:"Accounting", hired:"10/21/2008"}
 *          ]
 *     },
 *     columns: [
 *         {text: 'First Name',  dataIndex:'firstname'},
 *         {text: 'Last Name',  dataIndex:'lastname'},
 *         {text: 'Hired Month',  dataIndex:'hired'},
 *         {
 *             text: 'Department',
 *             width: 200,
 *             cell: {
 *                bind: '{record.department} ({record.seniority})'
 *             }
 *         }
 *     ],
 *     width: 500,
 *     fullscreen: true
 * });
 * ```
 * ```html
 * @example({framework: 'ext-web-components', packages:['ext-web-components'], tab: 1 })
 * <ext-container width="100%" height="100%">
 *     <ext-grid shadow="true" height="100%" onready="columnGrid.onGridReady">
 *         <ext-column text="Name" dataIndex="name" flex="1"></ext-column>
 *         <ext-column text="Email" dataIndex="email" flex="1"></ext-column>
 *         <ext-column text="Phone" dataIndex="phone" flex="1"></ext-column>
 *     </ext-grid>
 * </ext-container>
 * ```
 * ```javascript
 * @example({framework: 'ext-web-components', tab: 2, packages: ['ext-web-components']})
 * import '@sencha/ext-web-components/dist/ext-container.component';
 * import '@sencha/ext-web-components/dist/ext-grid.component';
 * import '@sencha/ext-web-components/dist/ext-column.component';
 * 
 * export default class ColumnGridComponent {
 *     constructor() {
 *        this.store = new Ext.data.Store({
 *           data: [
 *               { "name": "Lisa", "email": "[email protected]", "phone": "555-111-1224" },
 *               { "name": "Bart", "email": "[email protected]", "phone": "555-222-1234" },
 *               { "name": "Homer", "email": "[email protected]", "phone": "555-222-1244" },
 *               { "name": "Marge", "email": "[email protected]", "phone": "555-222-1254" }
 *           ]
 *        });
 *     }
 * 
 *     onGridReady(event) {
 *         this.basicGridCmp = event.detail.cmp;
 *         this.basicGridCmp.setStore(this.store);
 *     }
 * }
 * 
 * window.columnGrid = new ColumnGridComponent();
 * ```
 * ```javascript
 * @example({framework: 'ext-react', packages:['ext-react']})
 * import React, { Component } from 'react';
 * import { ExtGrid, ExtColumn, ExtPanel } from '@sencha/ext-modern';
 *
 * export default class MyExample extends Component {
 *     render() {
 *         this.store = new Ext.data.Store({
 *             data: [
 *                 { "name": "Lisa", "email": "[email protected]", "phone": "555-111-1224" },
 *                 { "name": "Bart", "email": "[email protected]", "phone": "555-222-1234" },
 *                 { "name": "Homer", "email": "[email protected]", "phone": "555-222-1244" },
 *                 { "name": "Marge", "email": "[email protected]", "phone": "555-222-1254" }
 *             ]
 *         });
 *         return (
 *            <ExtPanel width="100%" height="100%">
 *                 <ExtGrid shadow="true" height="100%" store={this.store}>
 *                     <ExtColumn text="Name" dataIndex="name" flex="1"></ExtColumn>
 *                     <ExtColumn text="Email" dataIndex="email" flex="1"></ExtColumn>
 *                     <ExtColumn text="Phone" dataIndex="phone" flex="1"></ExtColumn>
 *                 </ExtGrid>
 *             </ExtPanel>
 *         )
 *     }
 * }
 * ```
 * ```javascript
 * @example({framework: 'ext-angular', packages:['ext-angular']})
 * import { Component } from '@angular/core'
 * declare var Ext: any;
 *
 * @Component({
 *     selector: 'column-component',
 *     styles: [``],
 *     template: `
 *       <ExtPanel [width]="400" [height]="400">
 *            <ExtGrid shadow="true" [height]="400" [store]="store">
 *                <ExtColumn [text]="'Name'" dataIndex="name" flex="1"></ExtColumn>
 *                <ExtColumn [text]="'Email'" dataIndex="email" flex="1"></ExtColumn>
 *                <ExtColumn [text]="'Phone'" dataIndex="phone" flex="1"></ExtColumn>
 *            </ExtGrid>
 *        </ExtPanel>`
 *   })
 * export class ColumnComponent  {
 *    store = new Ext.data.Store({
 *    data: [
 *        { "name": "Lisa", "email": "[email protected]", "phone": "555-111-1224" },
 *        { "name": "Bart", "email": "[email protected]", "phone": "555-222-1234" },
 *        { "name": "Homer", "email": "[email protected]", "phone": "555-222-1244" },
 *        { "name": "Marge", "email": "[email protected]", "phone": "555-222-1254" }
 *    ]
 *    });
 * }
 * ```
 *
 * # Convenience Subclasses
 *
 * There are several column subclasses that provide default rendering for various data types
 *
 *  - {@link Ext.grid.column.Boolean}: Renders for boolean values
 *  - {@link Ext.grid.column.Date}: Renders for date values
 *  - {@link Ext.grid.column.Number}: Renders for numeric values
 *
 * For more information about configuring cell content, see {@link Ext.grid.Grid}.
 *
 * # Setting Sizes
 *
 * The columns can be only be given an explicit width value. If no width is specified the
 * grid will automatically the size the column to 20px.
 *
 * # Header Options
 *
 *  - {@link #text}: Sets the header text for the column
 *  - {@link #sortable}: Specifies whether the column can be sorted by clicking the header
 *    or using the column menu
 *
 * # Data Options
 *
 *  - {@link #dataIndex}: The dataIndex is the field in the underlying {@link Ext.data.Store}
 *    to use as the value for the column.
 *  - {@link #renderer}: Allows the underlying store value to be transformed before being
 *    displayed in the grid.
 */
Ext.define('Ext.grid.column.Column', {
    extend: 'Ext.grid.HeaderContainer',
    alternateClassName: 'Ext.grid.column.Template',
 
    xtype: ['gridcolumn', 'column', 'templatecolumn'],
 
    /**
     * @property {Boolean} isGridColumn
     * Set in this class to identify, at runtime, instances which are not instances of the
     * HeaderContainer base class, but are in fact, the subclass: Ext.grid.Column.
     */
    isGridColumn: true,
 
    mixins: [
        // This mixin is used to cache the padding size for cells in this column,
        // to be shared by all cells in the column.
        'Ext.mixin.StyleCacher',
        'Ext.mixin.Toolable'
    ],
 
    requires: [
        'Ext.util.TextMetrics'
    ],
 
    /**
     * @property {Boolean} isLeafHeader
     * This will be set to `true` if the column has no child columns.
     */
 
    /**
     * @property {Boolean} isHeaderGroup
     * This will be set to `true` if the column has child columns.
     */
 
    config: {
        /**
         * @cfg {String} [align='left']
         * Sets the alignment of the header and rendered columns.
         * Possible values are: `'left'`, `'center'`, and `'right'`.
         */
        align: undefined, // undefined so applier will run to determine default value
 
        /**
         * @cfg {Object} cell
         * The config object used to create {@link Ext.grid.cell.Base cells} for this column.
         * By default, cells use the {@link Ext.grid.cell.Cell gridcell} `xtype`. To create
         * a different type of cell, simply provide this config and the desired `xtype`.
         */
        cell: {
            xtype: 'gridcell'
        },
 
        /**
         * @cfg {String} dataIndex (required)
         * The name of the field in the grid's {@link Ext.data.Store}'s {@link Ext.data.Model}
         * definition from which to draw the column's value.
         */
        dataIndex: null,
 
        /**
         * @cfg {Boolean/String} locked
         * This config can be used with Locking Grid
         * Determines whether the column is locked or not.
         * Configure as `true` to lock the column to default locked region 
         * {@link Ext.grid.locked.Grid LockedGrid}
         * String values contains one of the defined locking regions - "left", "right" or "center"
        */
        locked: null,
 
        /**
         * @cfg {Number} defaultWidth
         * A width to apply if the {@link #flex} or {@link #width} configurations have not
         * been specified.
         *
         * @since 6.2.0
         */
        defaultWidth: 100,
 
        /**
         * @cfg {String[]} depends
         * Set this config to the field names that effect this column's rendering. This is
         * important for best performance when using a `renderer`, a `summaryRenderer` or
         * a `tpl` to render the cell's content. This is because such mechanisms can use
         * any field and as such must be refreshed on *any* field change. When this config
         * is provided, only changes to these fields (or the `dataIndex`) will cause a
         * refresh.
         *
         * When not using these mechanisms, only changes to the `dataIndex` will cause the
         * cell content to be refreshed.
         * @since 6.5.1
         */
        depends: null,
 
        emptyText: {
            cached: true,
            $value: '\xA0'
        },
 
        /**
         * @cfg {String} text
         * The header text to be used as innerHTML (html tags are accepted) to display in the
         * Grid.
         *
         * **Note**: to have a clickable header with no text displayed you can use the default
         * non-breaking space (`&nbsp;`).
         */
        text: '\xa0',
 
        /**
         * @cfg {Boolean} sortable
         * False to disable sorting of this column. Whether local/remote sorting is used is
         * specified in `{@link Ext.data.Store#remoteSort}`.
         */
        sortable: true,
 
        /**
         * @cfg {Boolean} groupable
         * If the grid is {@link Ext.grid.Grid#grouped grouped}, the menu for this column
         * will offer to "Group by this column" if this is set to `true`.
         *
         * If using the {@link Ext.grid.plugin.ViewOptions ViewOptions} plugin, this option
         * may be used to disable the option to group by this column.
         */
        groupable: true,
 
        /**
         * @cfg {Boolean} resizable
         * False to prevent the column from being resizable.
         * Note that this configuration only works when the
         * {@link Ext.grid.plugin.ColumnResizing ColumnResizing} plugin is enabled on the
         * {@link Ext.grid.Grid Grid}.
         */
        resizable: true,
 
        /**
         * @cfg {Boolean} hideable
         * False to prevent the user from hiding this column.
         *
         * @since 6.5.0
         */
        hideable: true,
 
        /**
         * @cfg {Function/String} renderer
         * A renderer is a method which can be used to transform data (value, appearance, etc.)
         * before it is rendered.
         *
         * For example:
         *
         *      {
         *          text: 'Some column',
         *          dataIndex: 'fieldName',
         *
         *          renderer: function(value, record) {
         *              if (value === 1) {
         *                  return '1 person';
         *              }
         *              return value + ' people';
         *          }
         *      }
         *
         * If a string is supplied, it should be the name of a renderer method from the
         * appropriate {@link Ext.app.ViewController}.
         *
         * This config is only processed if the {@link #cell} type is the default of
         * {@link Ext.grid.cell.Cell gridcell}.
         *
         * **Note** See {@link Ext.grid.Grid} documentation for other, better alternatives
         * to rendering cell content.
         *
         * @cfg {Object} renderer.value The data value for the current cell.
         * @cfg {Ext.data.Model} renderer.record The record for the current row.
         * @cfg {Number} renderer.dataIndex The dataIndex of the current column.
         * @cfg {Ext.grid.cell.Base} renderer.cell The current cell.
         * @cfg {Ext.grid.column.Column} renderer.column The current column.
         * @cfg {String} renderer.return The HTML string to be rendered. *Note*: to
         * render HTML into the cell, you will have to configure the column's {@link #cell}
         * with `encodeHtml: false`
         */
        renderer: null,
 
        /**
         * @cfg {String} formatter
         * This config accepts a format specification as would be used in a `Ext.Template`
         * formatted token. For example `'round(2)'` to round numbers to 2 decimal places
         * or `'date("Y-m-d")'` to format a Date.
         *
         * In previous releases the `renderer` config had limited abilities to use one
         * of the `Ext.util.Format` methods but `formatter` now replaces that usage and
         * can also handle formatting parameters.
         *
         * When the value begins with `"this."` (for example, `"this.foo(2)"`), the
         * implied scope on which "foo" is found is the `scope` config for the column.
         *
         * If the `scope` is not given, or implied using a prefix of `"this"`, then either the
         * {@link #method!getController ViewController} or the closest ancestor component
         * configured as {@link #defaultListenerScope} is assumed to be the object with the
         * method.
         * @since 6.2.0
         */
        formatter: null,
 
        /**
         * @cfg {Object} scope
         * The scope to use when calling the {@link #renderer} or {@link #formatter} function.
         */
        scope: null,
 
        /**
         * @cfg {Boolean} editable
         * Set this to true to make this column editable.
         * Only applicable if the grid is using an {@link Ext.grid.plugin.Editable Editable} plugin.
         */
        editable: null,
 
        /**
         * @cfg {Object/String} editor
         * The `xtype` or config object for a {@link Ext.field.Field Field} to use for
         * editing. This config is used by the {@link Ext.grid.plugin.Editable grideditable}
         * plugin.
         *
         * If this config is not set, and {@link #editable} is set to true, the
         * {@link #defaultEditor} is used.
         */
        editor: null,
 
        /**
         * @cfg {Object/Ext.field.Field} defaultEditor
         * An optional config object used to create a default editor for values in this
         * column when no {@link #editor} is specified. This config is typically defined
         * by derived column classes such as {@link Ext.grid.column.Date datecolumn} to
         * tune the default editor.
         *
         * This value is augmented by the {@link #cfg!editorDefaults editorDefaults}
         * config.
         */
        defaultEditor: {
            lazy: true,
            $value: {}
        },
 
        /**
         * @cfg {Object} editorDefaults
         * This object holds default config objects for creating the column's `editor`.
         * The keys of this object are {@link Ext.data.field.Field#cfg!type field type}
         * values (such as `'date'` or `'int'`). These keys can also be a comma-separated
         * list of such type names.
         *
         * These defaults are applied when producing an `editor` based on the field of
         * {@link #cfg!store store's} {@link Ext.data.Store#cfg!model model} identified
         * by the {@link #cfg!dataIndex dataIndex}.
         *
         * See {@link #ensureEditor ensureEditor}.
         * @since 7.0
         */
        editorDefaults: {
            cached: true,
            $value: {
                default: {
                    xtype: 'textfield',
                    autoComplete: false,
                    textAlign: undefined
                },
 
                'bool,boolean': {
                    xtype: 'checkboxfield',
                    bodyAign: undefined
                },
 
                date: {
                    xtype: 'datefield',
                    textAlign: undefined
                },
 
                'float,number': {
                    xtype: 'numberfield',
                    textAlign: undefined
                },
 
                'int,integer': {
                    xtype: 'numberfield',
                    decimals: 0,
                    textAlign: undefined
                }
            }
        },
 
        /**
         * @cfg {Boolean} ignore
         * Setting to `true` prevents this column from being used by plugins such as
         * {@link Ext.grid.plugin.ViewOptions} or {@link Ext.grid.plugin.Summary}. It is
         * intended for special columns such as the row number or checkbox selection.
         */
        ignore: false,
 
        /**
         * @cfg {Boolean} ignoreExport
         * This flag indicates that this column will be ignored when grid data is exported.
         *
         * When grid data is exported you may want to export only some columns that are
         * important and not everything. You can set this flag on any column that you want
         * to be ignored during export.
         *
         * This is used by {@link Ext.grid.plugin.Exporter exporter plugin}.
         */
        ignoreExport: false,
 
        /**
         * @cfg {Ext.exporter.file.Style/Ext.exporter.file.Style[]} exportStyle
         *
         * A style definition that is used during data export via the
         * {@link Ext.grid.plugin.Exporter exporter plugin}. This style will be applied to
         * the columns generated in the exported file.
         *
         * You could define it as a single object that will be used by all exporters:
         *
         *      {
         *          xtype: 'numbercolumn',
         *          dataIndex: 'price',
         *          exportStyle: {
         *              format: 'Currency',
         *              alignment: {
         *                  horizontal: 'Right'
         *              },
         *              font: {
         *                  italic: true
         *              }
         *          }
         *      }
         *
         * You could also define it as an array of objects, each object having a `type`
         * that specifies by which exporter will be used:
         *
         *      {
         *          xtype: 'numbercolumn',
         *          dataIndex: 'price',
         *          exportStyle: [{
         *              type: 'html', // used by the `html` exporter
         *              format: 'Currency',
         *              alignment: {
         *                  horizontal: 'Right'
         *              },
         *              font: {
         *                  italic: true
         *              }
         *          },{
         *              type: 'csv', // used by the `csv` exporter
         *              format: 'General'
         *          }]
         *      }
         *
         * Or you can define it as an array of objects that has:
         *
         * - one object with no `type` key that is considered the style to use by all exporters
         * - objects with the `type` key defined that are exceptions of the above rule
         *
         *      {
         *          xtype: 'numbercolumn',
         *          dataIndex: 'price',
         *          exportStyle: [{
         *              // no type defined means this is the default
         *              format: 'Currency',
         *              alignment: {
         *                  horizontal: 'Right'
         *              },
         *              font: {
         *                  italic: true
         *              }
         *          },{
         *              type: 'csv', // only the CSV exporter has a special style
         *              format: 'General'
         *          }]
         *      }
         *
         */
        exportStyle: null,
 
        /**
         * @cfg {Boolean/Function/String} exportRenderer
         *
         * During data export via the {@link Ext.grid.plugin.Exporter} plugin the data for
         * this column could be formatted in multiple ways:
         *
         * - using the `exportStyle.format`
         * - using the `formatter` if no `exportStyle` is defined
         * - using the `exportRenderer`
         *
         * If you want to use the `renderer` defined on this column then set `exportRenderer`
         * to `true`. Beware that this should only happen if the `renderer` deals only with
         * data on the record or value and it does NOT style the cell or returns an html
         * string.
         *
         *      {
         *          xtype: 'numbercolumn',
         *          dataIndex: 'price',
         *          text: 'Price',
         *          renderer: function(value, record, dataIndex, cell, column) {
         *              return Ext.util.Format.currency(value);
         *          },
         *          exportRenderer: true
         *      }
         *
         * If you don't want to use the `renderer` during export but you still want to format
         * the value in a special way then you can provide a function to `exportRenderer` or
         * a string (which is a function name on the ViewController).
         * The provided function has the same signature as the renderer.
         *
         *      {
         *          xtype: 'numbercolumn',
         *          dataIndex: 'price',
         *          text: 'Price',
         *          exportRenderer: function(value, record, dataIndex, cell, column) {
         *              return Ext.util.Format.currency(value);
         *          }
         *      }
         *
         *
         *      {
         *          xtype: 'numbercolumn',
         *          dataIndex: 'price',
         *          text: 'Price',
         *          exportRenderer: 'exportAsCurrency' // this is a function on the ViewController
         *      }
         *
         *
         * If `exportStyle.format`, `formatter` and `exportRenderer` are all defined on the
         * column then the `exportStyle` wins and will be used to format the data for this
         * column.
         */
        exportRenderer: false,
 
        /**
         * @cfg {String} summary
         * This config replaces the default mechanism of acquiring a summary result from
         * the summary record. When specified, this string is the name of a summary type:
         *
         *  - {@link Ext.data.summary.Average average}
         *  - {@link Ext.data.summary.Count count}
         *  - {@link Ext.data.summary.Max max}
         *  - {@link Ext.data.summary.Min min}
         *  - {@link Ext.data.summary.Sum sum}
         *
         * The summary is based on either the {@link #cfg!summaryDataIndex} or the
         * {@link #cfg!dataIndex} if there is no `summaryDataIndex`.
         *
         * This config is only valid when all data is available client-side to calculate
         * summaries.
         *
         * It is generally best to allow the summary {@link Ext.data.Model record} to
         * computer summary values (and not use this config). In some cases, however,
         * this config can be useful to isolate summary calculations to only certain grids.
         *
         * To implement a custom summary for a column, use {@link #cfg!summaryRenderer}.
         * @since 6.5.0
         */
        summary: null,
 
        /**
         * @cfg {Object} summaryCell
         * The config object used to create {@link Ext.grid.cell.Base cells} in
         * {@link Ext.grid.SummaryRow Summary Rows} for this column.
         */
        summaryCell: null,
 
        /**
         * @cfg {String} summaryDataIndex
         * For {@link Ext.grid.SummaryRow summary rows} this config overrides the normal
         * `dataIndex` to use from the summary record.
         * @since 6.5.0
         */
        summaryDataIndex: null,
 
        /**
         * @cfg {String} summaryFormatter
         * This summaryFormatter is similar to {@link #formatter} but is called before
         * displaying a value in the SummaryRow. The config is optional, if not specified
         * the default calculated value is shown. The summaryFormatter is called with:
         *
         *  - value: The calculated value.
         *
         * Note that this configuration only works when the grid has the
         * {@link Ext.grid.plugin.Summary gridsummary} plugin enabled.
         */
        summaryFormatter: null,
 
        /**
         * @cfg {Function/String} summaryRenderer
         * This summaryRenderer is called to render the value to display in a cell of a
         * summary row. If the value of this config is a String, it is the name of the
         * renderer method on the associated {@link Ext.Component#controller controller}.
         *
         * @cfg {Mixed} summaryRenderer.value The summary value to render. This value is
         * retrieved from the summary record based on the {@link #cfg!summaryDataIndex} or
         * {@link #cfg!dataIndex}, or by applying the {@link #cfg!summary} algorithm to
         * the appropriate records. While this value can be useful, it can also be ignored
         * and the renderer method can use the `context` information to determine the value
         * to render entirely on its own.
         *
         * @cfg {Object} summaryRenderer.context The summary context object.
         *
         * @cfg {String} summaryRenderer.context.dataIndex The data field. This will be
         * either the {@link #cfg!summaryDataIndex} if one is specified, or the normal
         * {@link #cfg!dataIndex} if not.
         *
         * @cfg {String} summaryRenderer.context.group The {@link Ext.data.Group group}
         * being summarized. This is `null` if the summary is for the whole `store`.
         *
         * @cfg {String} summaryRenderer.context.store The {@link Ext.data.Store store}
         * being summarized.
         *
         * If this method returns `undefined`, no update is made to the cell. Instead it
         * is assumed that the `summaryRenderer` has made all of the necessary changes.
         *
         * Note that this configuration only works when the grid has the
         * {@link Ext.grid.plugin.Summary gridsummary} plugin enabled.
         */
        summaryRenderer: null,
 
        /**
         * @cfg {String/Function} summaryType
         * This configuration specifies the type of summary. There are several built in
         * summary types. These call underlying methods on the store:
         *
         *  - {@link Ext.data.Store#count count}
         *  - {@link Ext.data.Store#sum sum}
         *  - {@link Ext.data.Store#min min}
         *  - {@link Ext.data.Store#max max}
         *  - {@link Ext.data.Store#average average}
         *
         * Any other name is assumed to be the name of a method on the associated
         * {@link Ext.app.ViewController view controller}.
         *
         * Note that this configuration only works when the grid has the
         * {@link Ext.grid.plugin.Summary gridsummary} plugin enabled.
         *
         * @deprecated 6.5 Use {@link #cfg!summary} or {@link #cfg!summaryRenderer} instead.
         */
        summaryType: null,
 
        /**
         * @cfg {Object/String[]} summaries
         * This config is used by {@link Ext.grid.plugin.Summaries} plugin.
         *
         * Define here what functions are available for your users to choose from
         * when they want to change the summary type on this column. By default only
         * `count` is supported but you can add more summary functions.
         *
         *      {
         *          xtype: 'column',
         *          summaries: {
         *              sum: true,
         *              average: true,
         *              count: false
         *          }
         *      }
         *
         *  Or like this if you want to bring new functions in:
         *
         *      {
         *          xtype: 'column',
         *          summaries: {
         *              calculateSomething: true
         *          }
         *      }
         *
         *  In such case `calculateSomething` needs to be defined as a summary function.
         *  For this you need to define a summary class like this:
         *
         *      Ext.define('Ext.data.summary.CalculateSomething', {
         *          extend: 'Ext.data.summary.Base',
         *          alias: 'data.summary.calculateSomething',
         *
         *          text: 'Calculate something',
         *
         *          calculate: function (records, property, root, begin, end) {
         *              // do your own calculation here
         *          }
         *      });
         *
         *
         */
        summaries: {
            $value: {
                count: true
            },
            lazy: true,
            merge: function(newValue, oldValue) {
                return this.mergeSets(newValue, oldValue);
            }
        },
 
        /**
         * @cfg {Boolean/Function/String} exportSummaryRenderer
         *
         * This config is similar to {@link #exportRenderer} but is applied to summary
         * records.
         */
        exportSummaryRenderer: false,
 
        minWidth: 40,
 
        /* eslint-disable max-len */
        /**
         * @cfg {String/String[]/Ext.XTemplate} tpl
         * An {@link Ext.XTemplate XTemplate}, or an XTemplate *definition string* to use
         * to process a {@link Ext.data.Model records} data to produce a cell's rendered
         * value.
         *
         *     @example
         *     Ext.create('Ext.data.Store', {
         *         storeId:'employeeStore',
         *         fields:['firstname', 'lastname', 'seniority', 'department'],
         *         groupField: 'department',
         *         data:[
         *             { firstname: "Michael", lastname: "Scott",   seniority: 7, department: "Management" },
         *             { firstname: "Dwight",  lastname: "Schrute", seniority: 2, department: "Sales" },
         *             { firstname: "Jim",     lastname: "Halpert", seniority: 3, department: "Sales" },
         *             { firstname: "Kevin",   lastname: "Malone",  seniority: 4, department: "Accounting" },
         *             { firstname: "Angela",  lastname: "Martin",  seniority: 5, department: "Accounting" }
         *         ]
         *     });
         *
         *     Ext.create('Ext.grid.Panel', {
         *         title: 'Column Template Demo',
         *         store: Ext.data.StoreManager.lookup('employeeStore'),
         *         columns: [{
         *             text: 'Full Name',
         *             tpl: '{firstname} {lastname}'
         *         }, {
         *             text: 'Department (Yrs)',
         *             tpl: '{department} ({seniority})'
         *         }],
         *         height: 200,
         *         width: 300,
         *         renderTo: Ext.getBody()
         *     });
         *
         * This config is only processed if the {@link #cell} type is the default of
         * {@link Ext.grid.cell.Cell gridcell}.
         *
         * **Note** See {@link Ext.grid.Grid} documentation for other, better alternatives
         * to rendering cell content.
         */
        tpl: null,
        /* eslint-enable max-len */
 
        /**
         * @cfg {Number} computedWidth
         * The computed width for this column, may come from either
         * {@link #width} or {@link #flex}.
         * @readonly
         */
        computedWidth: null,
 
        /**
         * A {@link Ext.grid.plugin.filterbar.filters.Base} configuration.
         *
         * This filter type is used by the {@link Ext.grid.plugin.filterbar.FilterBar} plugin.
         */
        filterType: null,
 
        /**
         * @cfg {Function/String/Object/Ext.util.Grouper} grouper
         * A grouper config object to apply when the standard grouping user interface is
         * is invoked. This option is, for example, available in the column's header
         * menu.
         *
         * Note that a grouper may also be specified as a function which accepts two
         * records to compare.
         *
         * A `{@link Ext.app.ViewController controller}` method can be used like so:
         *
         *      grouper: 'groupMethodName'
         *
         * This is different then a `sorter` in that the `grouper` method is used to
         * set the {@link Ext.util.Grouper#cfg!groupFn groupFn}. This string returned
         * by this method is used to determine group membership. To specify both the
         * `grpoupFn` and the `sorterFn`:
         *
         *      grouper: {
         *          groupFn: 'groupMethodName'
         *          sorterFn: 'sorterMethodName
         *      }
         *
         * @since 6.5.0
         */
        grouper: {
            lazy: true,
            $value: null
        },
 
        /**
         * @cfg {String/String[]/Ext.XTemplate} groupHeaderTpl
         * This config allows a column to replace the default template supplied by the
         * grid's {@link Ext.grid.RowHeader#tpl groupHeader.tpl}.
         *
         * @since 6.5.0
         */
        groupHeaderTpl: null,
 
        /**
         * @cfg {String} groupFormatter
         * This config accepts a format specification as would be used in a `Ext.Template`
         * formatted token. For example `'round(2)'` to round numbers to 2 decimal places
         * or `'date("Y-m-d")'` to format a Date.
         *
         * It is used by the {@link Ext.grid.plugin.GroupingPanel} plugin when adding groupers
         * to the store. When you drag a column from the grid to the grouping panel then
         * the `groupFormatter` will be used to create a new store grouper
         * {@link Ext.util.Grouper#formatter}.
         *
         * **Note:** if summaries are calculated on the server side then the server
         * side grouping should match the client side formatter otherwise the
         * summaries may be wrong.
         */
        groupFormatter: false,
 
        /**
         * @cfg {Function/String/Object/Ext.util.Sorter} sorter
         * A sorter config object to apply when the standard sort user interface is
         * is invoked. This is usually clicking this column header, but there are also
         * menu options to sort ascending or descending.
         *
         * Note that a sorter may also be specified as a function which accepts two
         * records to compare.
         *
         * A `{@link Ext.app.ViewController controller}` method can be used like so:
         *
         *      sorter: 'sorterMethodName'
         *
         * Or more explicitly:
         *
         *      sorter: {
         *          sorterFn: 'sorterMethodName'
         *      }
         *
         * By default sorting is based on the `dataIndex` but this can be adjusted
         * like so:
         *
         *      sorter: {
         *          property: 'otherProperty'
         *      }
         *
         * @since 6.5.0
         */
        sorter: {
            lazy: true,
            $value: true
        },
 
        /**
         * @cfg {Ext.grid.cell.Cell/Object} scratchCell
         * @since 6.5.0
         * @private
         */
        scratchCell: {
            lazy: true,
            $value: true
        },
 
        /**
         * @cfg {Ext.menu.Menu/Object} menu
         * An optional menu configuration object which is merged with the grid's
         * {@link #cfg!columnMenu} to create this column's header menu. This can be set
         * to `null` to remove the menu from this column. To dynamically change whether
         * the menu should be enabled or not use the `menuDisabled` config.
         *
         * The grid's {@link Ext.grid.Grid#cfg!columnMenu} provides the sort items, this
         * config can be used to add column-specific menu items or override aspects of
         * the common items.
         * @since 6.5.0
         */
        menu: {
            lazy: true,
            $value: {}
        },
 
        /**
         * @cfg {Boolean} [menuDisabled=false]
         * Set to `true` to disable this column's `menu` containing sort/hide options.
         * This can be useful if the menu will be dynamically available since setting
         * `menu` to `null` will eliminate the menu making dynamic changes to its
         * availability more expensive.
         * @since 6.5.0
         */
        menuDisabled: null,
 
        /**
         * @cfg {Ext.menu.CheckItem/Object} hideShowMenuItem
         * The {@link Ext.menu.CheckItem menu item} to be used by the owning grid's
         * header menu to hide or show this column.
         * @since 6.5.0
         * @private
         */
        hideShowMenuItem: {
            lazy: true,
            $value: {
                xtype: 'menucheckitem'
            }
        }
    },
 
    updateLocked: function(v) {
        var me = this,
            grid = me.getGrid(),
            region = grid && grid.region,
            lockedGrid, key, targetRegion;
 
        if (region) {
            lockedGrid = region.lockedGrid;
            key = lockedGrid.getRegionKey(v);
            targetRegion = lockedGrid.getRegion(key);
 
            if (targetRegion && targetRegion !== region) {
                lockedGrid.handleChangeRegion(targetRegion, me);
            }
        }
 
        return v;
    },
 
    toolDefaults: {
        ui: 'gridcolumn',
        zone: 'tail'
    },
 
    toolAnchorName: 'titleWrapElement',
 
    dockTools: false,
 
    scrollable: false,
 
    docked: null,
 
    sortState: null,
 
    // These are not readable descriptions; the values go in the aria-sort attribute.
    ariaSortStates: {
        ASC: 'ascending',
        DESC: 'descending'
    },
 
    inheritUi: true,
 
    classCls: Ext.baseCSSPrefix + 'gridcolumn',
    sortedCls: Ext.baseCSSPrefix + 'sorted',
    secondarySortCls: Ext.baseCSSPrefix + 'secondary-sort',
    auxSortCls: Ext.baseCSSPrefix + 'aux-sort',
    resizableCls: Ext.baseCSSPrefix + 'resizable',
    groupCls: Ext.baseCSSPrefix + 'group',
    leafCls: Ext.baseCSSPrefix + 'leaf',
    menuOpenCls: Ext.baseCSSPrefix + 'menu-open',
    alignCls: {
        left: Ext.baseCSSPrefix + 'align-left',
        center: Ext.baseCSSPrefix + 'align-center',
        right: Ext.baseCSSPrefix + 'align-right'
    },
 
    /**
     * @param {Ext.data.summary.Base} summaryType 
     *
     * This function is called by {@link Ext.grid.plugin.Summaries} plugin
     * when the summary on this column is changed.
     *
     * It is quite useful when you need to change the column summary renderer/formatter
     * depending on the chosen summary.
     */
    onSummaryChange: null,
 
    grouperIdPrefix: Ext.baseCSSPrefix + 'gridgrouper',
 
    /**
     * @event columnmenucreated
     * @member Ext.grid.Grid
     * Fired when a column first creates its column menu. This is to allow plugins
     * to access and manipulate the column menu.
     *
     * There will be the two sort items, and a column hide/show item with a child menu of
     * checkboxes. After this, developers may add custom enu items.
     *
     * Menu items may be configured with a `weight` config, and those with the lowest weight
     * gravitate to the top.
     *
     * The sort ascending, sort descending, and hide columns items have weight -3, -2, and -1
     * @param {Ext.grid.Grid} grid This Grid
     * @param {Ext.grid.Column} column The column creating the menu
     * @param {Ext.menu.Menu} menu The column's new menu
     */
 
    constructor: function(config) {
        var me = this,
            isHeaderGroup, menu;
 
        // If we are configured or prototyped as a HeaderGroup
        // TODO - move to updater (me.columns won't work in all cases)
        if (config.columns || me.columns) {
            isHeaderGroup = me.isHeaderGroup = true;
        }
        else {
            me.isLeafHeader = true;
        }
 
        me.callParent([config]);
 
        me.addCls(isHeaderGroup ? me.groupCls : me.leafCls);
 
        menu = me.getConfig('menu', /* peek= */true);
 
        if (!menu && me.getMenuDisabled() === null) {
            me.setMenuDisabled(true);
        }
    },
 
    getTemplate: function() {
        var me = this,
            beforeTitleTemplate = me.beforeTitleTemplate,
            afterTitleTemplate = me.afterTitleTemplate,
            titleTpl = [];
 
        // Hook for subclasses to insert extra elements
        if (beforeTitleTemplate) {
            titleTpl.push.apply(titleTpl, beforeTitleTemplate);
        }
 
        titleTpl.push({
            reference: 'titleElement',
            className: Ext.baseCSSPrefix + 'title-el',
            children: [{
                reference: 'textElement',
                className: Ext.baseCSSPrefix + 'text-el',
                "data-qoverflow": true
            }, {
                reference: 'sortIconElement',
                cls: Ext.baseCSSPrefix + 'sort-icon-el ' +
                Ext.baseCSSPrefix + 'font-icon'
            }]
        });
 
        // Hook for subclasses to insert extra elements
        if (afterTitleTemplate) {
            titleTpl.push.apply(titleTpl, afterTitleTemplate);
        }
 
        return [{
            reference: 'headerElement',
            cls: Ext.baseCSSPrefix + 'header-el',
            children: [{
                reference: 'titleWrapElement',
                cls: Ext.baseCSSPrefix + 'title-wrap-el',
                uiCls: 'title-wrap-el',
                children: titleTpl
            }, {
                reference: 'resizerElement',
                cls: Ext.baseCSSPrefix + 'resizer-el ' +
                     Ext.baseCSSPrefix + 'item-no-tap'
            }, {
                reference: 'triggerElement',
                cls: Ext.baseCSSPrefix + 'trigger-el ' +
                     Ext.baseCSSPrefix + 'font-icon ' +
                     Ext.baseCSSPrefix + 'item-no-tap'
            }]
        }, {
            reference: 'bodyElement',
            cls: Ext.baseCSSPrefix + 'body-el',
            uiCls: 'body-el'
        }];
    },
 
    /**
     * Return item order in its header group
     * @returns {Number} column weight
     */
    getWeight: function() {
        var me = this;
 
        if (me.rendered) {
            return me.parent.indexOf(me);
        }
 
        return me._weight;
    },
 
    initialize: function() {
        var me = this;
 
        if (me.isLeafHeader && !me.getWidth() && me.getFlex() == null) {
            me.setWidth(me.getDefaultWidth());
        }
 
        me.callParent();
 
        me.element.on({
            tap: 'onColumnTap',
            longpress: 'onColumnLongPress',
            scope: this
        });
        me.triggerElement.on({
            tap: 'onTriggerTap',
            scope: this
        });
        me.resizerElement.on({
            tap: 'onResizerTap',
            doubletap: 'onResizerDoubleTap',
            scope: this
        });
 
        if (me.isHeaderGroup) {
            me.on({
                add: 'doVisibilityCheck',
                remove: 'doVisibilityCheck',
                show: 'onColumnShow',
                hide: 'onColumnHide',
                move: 'onColumnMove',
                delegate: '> column',
                scope: me
            });
 
            me.on({
                show: 'onShow',
                scope: me
            });
        }
    },
 
    doDestroy: function() {
        var me = this;
 
        me.destroyMembers('editor', 'resizeListener', 'menu', 'hideShowMenuItem',
                          'childColumnsMenu');
 
        me.setScratchCell(null);
 
        me.mixins.toolable.doDestroy.call(me);
 
        me.callParent();
    },
 
    onAdded: function(parent, instanced) {
        this.visibleIndex = null;
        this.callParent([parent, instanced]);
    },
 
    /**
     * This method returns the {@link #cfg!editor editor} for this column. If an `editor`
     * is not explicitly configured and `editable` is `true`, then `defaultEditor` and
     * `editorDefaults` configs are used to produce an appropriate editor based on the
     * column's derived type and/or the `dataIndex` of the associated model.
     * @return {Ext.Component} 
     * @since 7.0
     */
    ensureEditor: function() {
        var me = this,
            editable = me.getEditable(),
            editor = editable !== false && me.getEditor(),
            cfg;
 
        if (!editor && editable) {
            cfg = me.getDefaultEditor();
            editor = Ext.create(cfg);
 
            me.setEditor(editor);
        }
 
        return editor;
    },
 
    /**
     * Returns the index of this column in the list of *visible* columns only if this column is a
     * base level Column. If it is a group column, it returns `false`.
     * @return {Number} 
     */
    getVisibleIndex: function() {
        // Note that the visibleIndex property is assigned by the owning HeaderContainer
        // when assembling the visible column set for the view.
        var visibleIndex = this.visibleIndex,
            rootHeaders;
 
        if (visibleIndex == null) {
            if (this.isHeaderGroup) {
                visibleIndex = false;
            }
            else {
                rootHeaders = this.getRootHeaderCt();
 
                if (rootHeaders) {
                    visibleIndex = rootHeaders.indexOfLeaf(this);
                }
            }
 
            this.visibleIndex = visibleIndex;
        }
 
        return visibleIndex;
    },
 
    _columnScopeRe: /^column\./,
    _gridScopeRe: /^grid\./,
 
    applyMenu: function(menu) {
        var me = this,
            grid = me.getGrid(),
            columnScopeRe = me._columnScopeRe,
            gridScopeRe = me._gridScopeRe,
            extraItems, gridColumnMenu, i, item, items, s;
 
        Ext.destroy(me.sortChangeListener);
 
        // Allow menu:null to rid the column of all menus... so only merge in the
        // grid's column menu if we have a non-null menu
        if (menu && !menu.isMenu) {
            if (Ext.isArray(menu)) {
                extraItems = menu;
                menu = null;
            }
            else if (!menu.items) {
                menu = {
                    items: menu
                };
            }
 
            if (!(gridColumnMenu = grid.getColumnMenu())) {
                // if menu was an array it is now null, so just make an empty {}
                menu = menu ? Ext.clone(menu) : {};
            }
            else {
                gridColumnMenu = Ext.clone(gridColumnMenu);
                menu = menu ? Ext.merge(gridColumnMenu, menu) : gridColumnMenu;
            }
 
            menu.ownerCmp = me;
 
            menu = Ext.create(menu);
 
            // This column is informed about group changes
            me.sortChangeListener = menu.on({
                groupchange: 'onColumnMenuGroupChange',
                scope: me
            });
 
            // We cannot use defaultListenerScope to map handlers in our menu to
            // ourselves because user views would then be blocked from doing so to
            // items they may have added to the same menu.
            //
            // Our trick is to encode special scopes in the handler names and see
            // if they have survived until now. It is possible the user has set
            // the handler to something else...
 
            items = menu.getItems().items;
 
            for (= items && items.length; i-- > 0; /* empty */) {
                item = items[i];
 
                if (columnScopeRe.test(= item.getHandler() || '')) {
                    item.setHandler(s.substr(7));  // remove "column."
                    item.scope = me;
                }
                else if (gridScopeRe.test(s)) {
                    item.setHandler(s.substr(5));  // remove "grid."
                    item.scope = grid;
                }
                else if (item.isMenuCheckItem) {
                    if (columnScopeRe.test(= item.getCheckHandler() || '')) {
                        item.setCheckHandler(s.substr(7));
                        item.scope = me;
                    }
                    else if (gridScopeRe.test(s)) {
                        item.setCheckHandler(s.substr(5));
                        item.scope = grid;
                    }
                }
            }
 
            if (extraItems) {
                menu.add(extraItems);
            }
 
            grid.fireEvent('columnmenucreated', grid, me, menu);
        }
 
        return menu;
    },
 
    updateMenu: function(menu, oldMenu) {
        if (oldMenu) {
            oldMenu.destroy();
        }
    },
 
    beforeShowMenu: function(menu) {
        var me = this,
            store = me.getGrid().getStore(),
            isGrouped = store && !!store.getGrouper(),
            groupByThis = menu.getComponent('groupByThis'),
            showInGroups = menu.getComponent('showInGroups'),
            sortAsc = menu.getComponent('sortAsc'),
            sortDesc = menu.getComponent('sortDesc');
 
        sortAsc.setDisabled(!store);
        sortDesc.setDisabled(!store);
 
        // We have no store yet, we can't group or ungroup
        if (!store) {
            groupByThis.setHidden(true);
            showInGroups.setHidden(true);
 
            return;
        }
 
        // Ensure the checked state of the ascending and descending menu items
        // matches the reality of the Store's sorters.
        //
        // We are syncing the menu state with the reality of the store.
        // Ensure its state change doesn't drive the store state
        // by suspending the groupchange event.
        menu.suspendEvent('groupchange');
 
        if (sortAsc) {
            me.syncMenuItemState(sortAsc);
        }
 
        if (sortDesc) {
            me.syncMenuItemState(sortDesc);
        }
 
        if (groupByThis) {
            groupByThis.setHidden(!(me.canGroup() && !store.isTreeStore));
        }
 
        menu.resumeEvent('groupchange');
 
        if (showInGroups) {
            // A TreeStore is never grouped
            showInGroups.setHidden(store.isTreeStore);
            // Disable the "Show in groups" options if we're not already shown in groups
            showInGroups.setChecked(isGrouped);
            showInGroups.setDisabled(!isGrouped);
        }
    },
 
    showMenu: function() {
        var me = this,
            menu = !me.getMenuDisabled() && me.getMenu(),
            menuOpenCls = me.menuOpenCls,
            grid;
 
        // Only try if the menu is not disabled, and there *is* a menu
        if (menu) {
            grid = me.getGrid();
 
            if (me.beforeShowMenu(menu) !== false &&
                    grid.beforeShowColumnMenu(me, menu) !== false) {
                menu.showBy(me.triggerElement);
 
                // Add menu open class to show the trigger element while the menu is open
                me.addCls(menuOpenCls);
 
                menu.on({
                    single: true,
                    hide: function() {
                        if (!(me.destroyed || me.destroying)) {
                            me.removeCls(menuOpenCls);
                        }
 
                        if (!(grid.destroyed || grid.destroying)) {
                            grid.onColumnMenuHide(me, menu);
                        }
                    }
                });
            }
        }
    },
 
    getCells: function() {
        var cells = [],
            rows = this.getGrid().items.items,
            len = rows.length,
            i, row;
 
        for (= 0; i < len; ++i) {
            row = rows[i];
 
            if (row.isGridRow) {
                cells.push(row.getCellByColumn(this));
            }
        }
 
        return cells;
    },
 
    getColumnForField: function(fieldName) {
        if (fieldName === this.getDataIndex()) {
            return this;
        }
 
        return this.callParent([ fieldName ]);
    },
 
    /**
     * Determines whether the UI should be allowed to offer an option to hide this column.
     *
     * A column may *not* be hidden if to do so would leave the grid with no visible columns.
     *
     * This is used to determine the enabled/disabled state of header hide menu items.
     */
    isHideable: function() {
        var menuOfferingColumns = [];
 
        // Collect menu offering columns so that we can assess our hideability.
        // Cannot use CQ because we need to use getConfig with peek flag to
        // check whether there's a menu without instantiating it.
        this.getRootHeaderCt().visitPreOrder('gridcolumn:not([hidden])', function(col) {
            if (!col.getMenuDisabled() && col.getConfig('menu', true)) {
                menuOfferingColumns.push(col);
            }
        });
 
        return menuOfferingColumns.length > 1 || menuOfferingColumns[0] !== this;
    },
 
    applyTpl: function(tpl) {
        return Ext.XTemplate.get(tpl);
    },
 
    applyAlign: function(align, oldAlign) {
        if (align == null) {
            align = this.isHeaderGroup ? 'center' : 'left';
        }
 
        return align;
    },
 
    updateAlign: function(align, oldAlign) {
        var me = this,
            alignCls = me.alignCls;
 
        if (oldAlign) {
            me.removeCls(alignCls[oldAlign]);
        }
 
        if (align) {
            //<debug>
            if (!alignCls[align]) {
                Ext.raise("Invalid value for align: '" + align + "'");
            }
            //</debug>
 
            me.addCls(alignCls[align]);
        }
 
        me.syncToolableAlign();
    },
 
    updateMenuDisabled: function(menuDisabled) {
        if (this.triggerElement) {
            this.triggerElement.setVisible(!menuDisabled);
        }
    },
 
    onColumnTap: function(e) {
        var me = this,
            grid = me.getGrid(),
            selModel = grid.getSelectable(),
            store = grid.getStore(),
            sorters = store && store.getSorters(true),
            sorter = store && me.pickSorter(),
            sorterIndex = sorter ? sorters.indexOf(sorter) : -1,
            isSorted = sorter && (sorterIndex !== -1 || sorter === store.getGrouper());
 
        // Tapping on the trigger or resizer must not sort the column and
        // neither should tapping on any components (e.g. tools) contained
        // in the column.
        if (Ext.Component.from(e) !== me ||
            e.getTarget('.' + Ext.baseCSSPrefix + 'item-no-tap', me)) {
            return;
        }
 
        // Column tap sorts if we are sortable, and the selection model
        // is not selecting columns
        if (store && me.isSortable() && (!selModel || !selModel.getColumns())) {
            // Special case that our sorter is the grouper
            if (sorter.isGrouper) {
                sorter.toggle();
                store.group(sorter);
            }
            // If we are already the primary sorter
            // then just toggle through the three states
            else if (sorterIndex === 0) {
                me.toggleSortState();
            }
            // We must be a secondary or auxilliary in a multi column sort grid,
            // or unsorted now.
            else {
                // We're secondary or auxilliary, bring top top of sorter stack
                if (isSorted) {
                    store.sort(sorter, 'prepend');
                }
                // Our sorter is unused, go primary, ascending
                else {
                    me.sort('ASC');
                }
            }
        }
 
        return me.fireEvent('tap', me, e);
    },
 
    onTriggerTap: function(e) {
        this.fireEvent('triggertap', this, e);
    },
 
    onResizerTap: function(e) {
        // If they tapped on the resizer without dragging, interpret that as a tap
        // on the trigger, if it's in the correct region.
        if (e.getPoint().isContainedBy(this.triggerElement.getRegion())) {
            this.fireEvent('triggertap', this, e);
        }
    },
 
    onResizerDoubleTap: function(e) {
        e.claimGesture();
        Ext.asap(this.autoSize, this);
    },
 
    onColumnLongPress: function(e) {
        this.fireEvent('longpress', this, e);
    },
 
    onGroupByThis: function() {
        var me = this,
            grid = me.getGrid(),
            grouper = me.getGrouper(),
            store = grid.getStore(),
            dataIndex;
 
        if (!grouper) {
            dataIndex = me.getDataIndex();
 
            if (dataIndex != null) {
                me.setGrouper({
                    property: dataIndex
                });
 
                grouper = me.getGrouper();
            }
        }
 
        //<debug>
        if (store && store.isVirtualStore && grouper) {
            Ext.Logger.warn('Virtual store does not suppport grouping');
 
            return;
        }
        //</debug>
 
        if (grouper) {
            store.setGrouper(grouper);
        }
    },
 
    /**
     * @private
     * Called as a groupchange handler on the header menu to either set the direction, or
     * remove the sorter.
     */
    onColumnMenuGroupChange: function(menu, groupName, value) {
        if (groupName === 'sortDir') {
            this.setSortDirection(value);
        }
    },
 
    getSortDirection: function() {
        var sorter = this.pickSorter();
 
        return sorter && sorter.getDirection();
    },
 
    setSortDirection: function(direction) {
        var me = this,
            grid = me.getGrid(),
            store = grid.getStore(),
            sorter = me.pickSorter(),
            sorters = store.getSorters(true),
            isSorted = sorter && (sorters.contains(sorter) || sorter.isGrouper);
 
        // Toggling to checked.
        if (direction) {
            if (isSorted) {
                if (sorter.getDirection() !== direction) {
                    sorter.setDirection(direction);
 
                    if (sorter.isGrouper) {
                        store.group(sorter);
                    }
                    else {
                        sorters.beginUpdate();
                        sorters.endUpdate();
                    }
                }
            }
            // Either the sorter is not applied, or it's the first time and there's no sorter.
            // Sort by direction as primary
            else {
                return me.sort(direction);
            }
        }
        // Toggled to clear.
        // If we own a sorter, and its in our direction, and it's applied to the store
        // then remove it.
        else if (sorter) {
            sorters.remove(sorter);
        }
 
        // A locally sorted store will not refresh in response to having a sorter
        // removed, so we must sync the column header arrows now.
        // AbstractStore#onSorterEndUpdate will however always fire the sort event
        // which is what Grid uses to trigger a HeaderContainer sort state sync
        if (!store.getRemoteSort()) {
            me.getRootHeaderCt().setSortState();
        }
    },
 
    syncMenuItemState: function(menuItem) {
        if (menuItem) {
            // eslint-disable-next-line vars-on-top
            var me = this,
                sortable = me.isSortable(),
                store = me.getGrid().getStore(),
                sorter = me.pickSorter(),
                isSorted = sorter && (store.getSorters().contains(sorter) || sorter.isGrouper);
 
            menuItem.setDisabled(!sortable);
            menuItem.setChecked(sortable && isSorted &&
                                sorter.getDirection() === menuItem.getValue());
        }
    },
 
    onToggleShowInGroups: function() {
        var grid = this.getGrid(),
            store = grid.getStore();
 
        store.setGrouper(null);
    },
 
    updateResizable: function() {
        var me = this,
            widthed = me.getWidth() != null,
            flexed = me.getFlex() != null;
 
        // Column only drag-resizable if it's widthed, flexed, or a leaf.
        // If it's shrinkwrapping child columns then the child columns must be resized.
        me.toggleCls(me.resizableCls, !!(me.getResizable() && (widthed || flexed ||
            me.isLeafHeader)));
    },
 
    updateText: function(text) {
        this.setHtml(text || '\xa0');
    },
 
    onResize: function() {
        if (!this.isHidden(true)) {
            // Update the resizability of this column based on *how* it's just been sized.
            // If we are shrinkwrapping, we are not drag-resizable.
            this.updateResizable(this.getResizable());
 
            // Computed with needs to be exact so that sub-pixel changes are
            // not rejected by the config system because scrollbars may
            // depend upon the *exact* width of the cells in the view.
            this.measureWidth();
 
            // Sync row height for all grid regions on resize columns  
            this.syncRowHeight();
 
        }
    },
 
    syncRowHeight: function() {
        /**
         * In case of allPartners, it gives patner grids + self grid
         * when partners are not available, we need to assign self grid
         */
        var grids = this.getGrid().allPartners || [this.getGrid()],
            i,
            len = grids.length,
            grid;
 
        for (= 0; i < len; ++i) {
            grid = grids[i];
            grid.syncRowsToHeight(true);
        }
    },
 
    getComputedWidth: function() {
        return this.isVisible(true) ? this._computedWidth : 0;
    },
 
    updateColumns: function(columns) {
        this.getItems();
        this.add(columns);
    },
 
    measureWidth: function() {
        // Computed width must be a real. exact pixel width.
        // It cannot be em or rem etc because it is used to size owned cells
        // and different styles and fonts may be applied to cells.
        var width = this.el.measure('w');
 
        this.setComputedWidth(width);
 
        return width;
    },
 
    updateComputedWidth: function(value, oldValue) {
        var me = this,
            rootHeaderCt = !me.isConfiguring && me.getRootHeaderCt();
 
        // This is how grid's resize their cells in response. Not through events.
        // Width change events arrive asynchronously through resize listeners
        // and that would cause janky grid resizes.
        //
        // By informing the grid, it can force all flexed columns to republish
        // their computed widths, and correctly update all cells in one pass.
        if (rootHeaderCt) {
            // This updates the cells.
            rootHeaderCt.onColumnComputedWidthChange(me, value);
 
            // Fire the event after cells have been resized
            me.fireEvent('columnresize', me, value, oldValue);
        }
    },
 
    updateDataIndex: function(dataIndex) {
        var sorter;
 
        if (!this.isConfiguring) {
            sorter = this.pickSorter();
 
            if (sorter) {
                this.setSorter(null);
            }
        }
    },
 
    applyGroupHeaderTpl: function(tpl) {
        return Ext.XTemplate.get(tpl);
    },
 
    updateGroupHeaderTpl: function(tpl) {
        var grouper = this.grouper;
 
        if (grouper) {
            grouper.headerTpl = tpl;
        }
    },
 
    isSortable: function() {
        var me = this;
 
        // Only leaf headers are sortable.
        // Only if we are not configured sortable: false.
        // We're not sortable if there's no Sorter configured AND we have no dataIndex.
        // HeaderContainer's sortable config must be honoured dynamically since
        // SelectionModels can change it.
        // And the grid has the final say.
        return me.isLeafHeader &&
            me.getSortable() &&
            (me.pickSorter() || me.getDataIndex()) &&
            me.getRootHeaderCt().getSortable() &&
            me.getGrid().sortableColumns !== false;
    },
 
    applyEditor: function(value) {
        if (value && !value.isInstance) {
            if (typeof(value) === 'string') {
                value = {
                    xtype: value
                };
            }
 
            if (!value.xtype) {
                value = Ext.apply({
                    xtype: value.field ? 'celleditor' : 'textfield'
                }, value);
            }
 
            return Ext.create(value);
        }
 
        return value;
    },
 
    applyEditorDefaults: function(defaults) {
        var ret = {},
            i, key, keys;
 
        if (defaults) {
            for (key in defaults) {
                keys = key.split(',');
 
                for (= 0; i < keys.length; ++i) {
                    ret[keys[i]] = defaults[key];
                }
            }
        }
 
        return ret;
    },
 
    applyDefaultEditor: function(editor) {
        var me = this,
            dataIndex = me.getDataIndex(),
            bodyAlign = 'bodyAlign',
            textAlign = 'textAlign',
            defaults, model, field, undef;
 
        if (dataIndex) {
            model = me.getGrid().store.getModel();
            field = model.getField(dataIndex);
 
            if (!editor.isInstance) {
                // We mutate the config
                editor = Ext.clone(editor);
 
                // Infer default xtype from data field type
                if (!editor.xtype) {
                    defaults = me.getEditorDefaults();
                    defaults = Ext.clone((field && defaults[field.type]) || defaults.default);
 
                    if (textAlign in defaults && defaults[textAlign] === undef) {
                        defaults[textAlign] = me.getAlign();
                    }
                    else if (bodyAlign in defaults && defaults[bodyAlign] === undef) {
                        defaults[bodyAlign] = me.getAlign();
                    }
 
                    Ext.applyIf(editor, defaults);
                }
            }
 
            editor._validationField = field;
        }
 
        return editor;
    },
 
    updateEditor: function(editor, oldEditor) {
        // If we are changing editors destroy the last one
        // but if we are changing from a field to a cell editor make sure we do not destroy
        // the field that is now a child of the cell editor
        if (oldEditor && (!editor || (editor.isCellEditor && editor.getField() !== oldEditor))) {
            oldEditor.destroy();
        }
 
        if (editor) {
            editor.$column = this;
        }
    },
 
    applyFormatter: function(format) {
        var me = this,
            fmt = format,
            parser;
 
        if (fmt) {
            parser = Ext.app.bind.Parser.fly(fmt);
            fmt = parser.compileFormat();
            parser.release();
 
            return function(v) {
                return fmt(v, me.getScope() || me.resolveListenerScope());
            };
        }
 
        return fmt;
    },
 
    applySummaryFormatter: function(format) {
        var me = this,
            fmt = format,
            parser;
 
        if (fmt) {
            parser = Ext.app.bind.Parser.fly(fmt);
            fmt = parser.compileFormat();
            parser.release();
 
            return function(v) {
                return fmt(v, me.getScope() || me.resolveListenerScope());
            };
        }
 
        return fmt;
    },
 
    applyGrouper: function(grouper) {
        var me = this,
            cfg = grouper;
 
        if (cfg && !cfg.isInstance) {
            if (typeof cfg === 'string') {
                cfg = {
                    groupFn: cfg
                };
            }
            else {
                cfg = Ext.apply({}, cfg);
            }
 
            if (typeof cfg.groupFn === 'string') {
                cfg = me.scopeReplacer(cfg, grouper, 'groupFn', 'setGroupFn');
            }
 
            if (typeof cfg.sorterFn === 'string') {
                cfg = me.scopeReplacer(cfg, grouper, 'sorterFn', 'setSorterFn');
            }
 
            grouper = new Ext.util.Grouper(cfg);
        }
 
        // The owner/headerTpl expandos on our grouper are picked up by the ItemHeader
        // as a means to override the list's groupHeaderTpl...
        if (grouper) {
            grouper.owner = me.getGrid();
            grouper.headerTpl = me.getGroupHeaderTpl();
        }
 
        // So folks can easily pick this up w/o calling getGrouper which will trigger
        // its creation.
        return grouper;
    },
 
    updateGrouper: function(grouper, oldGrouper) {
        var store = this.getGrid().getStore();
 
        if (store && oldGrouper) {
            if (oldGrouper === store.getGrouper()) {
                store.setGrouper(grouper);
            }
        }
 
        this.grouper = grouper;
    },
 
    applySorter: function(sorter) {
        var me = this,
            cfg = sorter,
            sortProperty;
 
        if (cfg && !cfg.isInstance) {
            // The default value is true to indicate use the dataIndex
            if (cfg === true) {
                sortProperty = me.getSortParam();
 
                if (!sortProperty) {
                    return null;
                }
 
                cfg = {
                    property: sortProperty,
                    direction: 'ASC'
                };
            }
            else {
                if (typeof cfg === 'string') {
                    cfg = {
                        sorterFn: cfg
                    };
                }
 
                if (typeof cfg.sorterFn === 'string') {
                    cfg = me.scopeReplacer(cfg, sorter, 'sorterFn', 'setSorterFn');
                }
            }
 
            sorter = new Ext.util.Sorter(cfg);
        }
 
        if (sorter) {
            sorter.owner = me.getGrid();
        }
 
        return sorter;
    },
 
    updateSorter: function(sorter, oldSorter) {
        var store = this.getGrid().getStore(),
            sorters = store ? store.getSorters() : null,
            at;
 
        // If our previous sorter is in the store, replace it with the new one or
        // just remove it if we don't have one.
        if (sorters) {
            if (oldSorter && (at = sorters.indexOf(oldSorter)) > -1) {
                if (sorter) {
                    sorters.splice(at, 1, sorter);
                }
                else {
                    sorters.remove(oldSorter);
                }
            }
        }
 
        // So folks can easily pick this up w/o calling getSorter which will trigger
        // its creation.
        this.sorter = sorter;
    },
 
    pickSorter: function() {
        var me = this,
            store = me.getGrid().getStore(),
            result, groupers;
 
        // Must always use the grouper if our dataIndex is the store's groupField.
        // We have to test dynamically in the getter because of possible store changes
        if (store.isGrouped()) {
            groupers = store.getGroupers(false);
 
            if (groupers) {
                result = groupers.get(me.getDataIndex());
            }
            else if (store.getGroupField() === me.getDataIndex()) {
                result = me.getGrouper() || store.getGrouper();
            }
 
            if (result) {
                // The sort state is always the direction of the grouper
                me.sortState = result.getDirection();
            }
        }
 
        if (!result) {
            result = me.getSorter();
        }
 
        return result;
    },
 
    applyHideShowMenuItem: function(config, existing) {
        return Ext.updateWidget(existing, config, this, 'createHideShowMenuItem');
    },
 
    createHideShowMenuItem: function(defaults) {
        return Ext.apply({
            text: this.getText(),
            checked: !this.getHidden(),
            column: this
        }, defaults);
    },
 
    getHideShowMenuItem: function(deep) {
        var me = this,
            result = me.callParent(),
            items = me.items.items,
            len = items.length,
            childItems = [],
            childColumnsMenu = me.childColumnsMenu,
            i;
 
        // If we're a header group, we offer our hideable child columns
        // in a submenu.
        if (me.isHeaderGroup && deep !== false) {
            if (!childColumnsMenu) {
                result.setMenu({});
                me.childColumnsMenu = childColumnsMenu = result.getMenu();
            }
 
            if (!childColumnsMenu.items.length || me.rebuildChildColumnsMenu) {
                for (= 0; i < len; i++) {
                    if (items[i].getHideable()) {
                        childItems.push(items[i].getHideShowMenuItem());
                    }
                }
 
                childColumnsMenu.removeAll(false);
                childColumnsMenu.add(childItems);
            }
        }
 
        // Ensure we're enabled/disabled correctly on first show
        result['set' + (result.getMenu() ? 'CheckChange' : '') + 'Disabled'](!me.isHideable());
 
        return result;
    },
 
    getInnerHtmlElement: function() {
        return this.textElement;
    },
 
    /**
     * Returns the parameter to sort upon when sorting this header. By default this returns
     * the dataIndex and will not need to be overridden in most cases.
     * @return {String} 
     */
    getSortParam: function() {
        return this.getDataIndex();
    },
 
    applyCell: function(cell, oldCell) {
        // Allow the cell config object to be reconfigured.
        if (oldCell) {
            cell = Ext.apply(oldCell, cell);
        }
 
        return cell;
    },
 
    createCell: function(row) {
        var me = this,
            cfg = {
                row: row,
                ownerCmp: row || me,
                column: me,
                width: me.rendered ? (me.getComputedWidth() || me.measureWidth()) : me.getWidth(),
                minWidth: me.getMinWidth()
            },
            align = me.getAlign(),
            cellCfg;
 
        if (row && row.isSummaryRow) {
            cellCfg = me.getSummaryCell();
 
            if (!cellCfg) {
                cellCfg = me.getCell();
 
                if (cellCfg.xtype === 'widgetcell') {
                    // We don't default to creating a widgetcell in a summary row, so
                    // fallback to a normal cell
                    cellCfg = Ext.apply({}, cellCfg);
                    cellCfg.xtype = 'gridcell';
 
                    delete cellCfg.widget;
                }
            }
        }
        else {
            cellCfg = me.getCell();
        }
 
        if (align) {
            // only put align on the config object if it is not null.  This prevents
            // the column's default value of null from overriding a value set on the
            // cell's class definition (e.g. widgetcell)
            cfg.align = align;
        }
 
        if (row) {
            cfg.hidden = me.isHidden(row.getGrid().getHeaderContainer());
            cfg.record = row.getRecord();
 
            if (!(cfg.ui = row.getDefaultCellUI())) {
                delete cfg.ui;
            }
        }
 
        if (typeof cellCfg === 'string') {
            cfg.xtype = cellCfg;
        }
        else {
            Ext.apply(cfg, cellCfg);
        }
 
        return cfg;
    },
 
    applyScratchCell: function(cell, oldCell) {
        var me = this;
 
        if (cell) {
            cell = Ext.create(me.createCell());
 
            if (!cell.printValue) {
                // If this cell type (widgetcell) cannot print its value, fallback to
                // default gridcell
                Ext.destroy(cell);
                cell = me.createCell();
                cell.xtype = 'gridcell';
                cell = Ext.create(cell);
            }
 
            // Add the positioned class to make this position:absolute so that it can be
            // added to the document without breaking the layout.
            cell.addCls(me.floatingCls);
        }
 
        if (oldCell) {
            oldCell.destroy();
        }
 
        return cell;
    },
 
    printValue: function(value) {
        var me = this,
            row = me.getGrid().dataItems[0],
            cell;
 
        if (row && row.isGridRow) {
            cell = row.getCellByColumn(me);
        }
 
        cell = (cell && cell.printValue) ? cell : me.getScratchCell();
 
        return cell.printValue(value);
    },
 
    /**
     * Sizes this Column to fit the max content width of records.
     * @since 7.0
     */
    autoSize: function() {
        var me = this,
            max = Math.max,
            textMatrics = new Ext.util.TextMetrics(),
            widthAdjust = 0,
            maxWidth = 0,
            innerCells = me.getCells(),
            grid = me.getGrid(),
            columnIndex, textWidth, rec, store, len, i,
            records, idx, paddedWidth, cellElement;
 
        if (!me.getResizable()) {
            return;
        }
 
        store = grid.getStore();
 
        columnIndex = me.getDataIndex();
 
        if (store && columnIndex) {
            if (store.isVirtualStore) {
                records = store.pageMap.pages;
            }
            else {
                records = store.getData() && store.getData().items;
            }
 
            // Calculate size through store records
            if (columnIndex && Ext.isArray(records)) {
                len = records.length;
 
                for (= 0; i < len; i++) {
                    textWidth = textMatrics.getWidth(records[i].get(columnIndex));
                    maxWidth = max(maxWidth, textWidth);
                }
            }
            else if (Ext.isObject(records)) {
                // Calculate size for virtual store records
                for (idx in records) {
                    rec = records[idx].records;
                    len = rec && rec.length;
 
                    for (= 0; i < len; i++) {
                        textWidth = textMatrics.getWidth(rec[i].get(columnIndex));
                        maxWidth = max(maxWidth, textWidth);
                    }
                }
            }
        }
 
        // Allow for padding round text of header
        paddedWidth = me.textElement.dom.offsetWidth + me.titleElement.getPadding('lr');
        maxWidth = max(maxWidth, paddedWidth);
 
        // Calculate size for grid cells
        if (innerCells.length) {
            // Calculate cell text size
            len = innerCells.length;
            cellElement = innerCells[0].element;
 
            for (= 0; i < len; i++) {
                maxWidth = max(maxWidth, innerCells[i].element.getTextWidth());
            }
 
            if (Ext.supports.ScrollWidthInlinePaddingBug) {
                widthAdjust += cellElement.getPadding('r');
            }
 
            if (grid.getColumnLines()) {
                widthAdjust += cellElement.getBorderWidth('lr');
            }
 
            // in some browsers, the "after" padding is not accounted for in the scrollWidth
            maxWidth += widthAdjust;
        }
 
        // check for minimum column width
        // One extra pixel added. EXACT width shrinkwrap of text causes ellipsis to appear.
        maxWidth = max(maxWidth + 1, me.getMinWidth());
 
        me.setWidth(maxWidth);
        textMatrics.destroy();
    },
 
    /**
     * Returns an array of summary functions supported on this column.
     * @return {String[]} 
     */
    getListOfSummaries: function() {
        var ret = [],
            v = this.getSummaries() || {},
            keys = Ext.Object.getAllKeys(v),
            len = keys.length,
            i, key;
 
        // we need to extract 'true' summaries from the object
        for (= 0; i < len; i++) {
            key = keys[i];
 
            if (v[key]) {
                ret.push(key);
            }
        }
 
        return ret;
    },
 
    applySummaries: function(newValue, oldValue) {
        var config = this.self.getConfigurator().configs.summaries;
 
        return config.mergeSets(newValue);
    },
 
    privates: {
        // State map for cycling our sortState property
        directionSequence: {
            "null": "ASC",
            "ASC": "DESC",
            "DESC": null
        },
 
        applySummary: function(summary) {
            if (summary) {
                summary = Ext.Factory.dataSummary(summary);
            }
 
            return summary;
        },
 
        beginRefresh: function(context) {
            // This is called by our detached cells
            var me = this,
                grid = me.getGrid();
 
            context = context || {};
 
            context.column = me;
            context.grid = grid;
            // record = null
            // row = null
            context.store = grid.store;
 
            return context;
        },
 
        canGroup: function() {
            return this.getGroupable() && (this.getDataIndex() || this.getGrouper());
        },
 
        /**
         * Sorts by this column's sorter in the passed direction.
         * @param direction
         * @param mode
         */
        sort: function(direction, mode) {
            var me = this,
                sorter = me.pickSorter(),
                grid = me.getGrid(),
                store = grid.getStore(),
                sorters = store.getSorters();
 
            if (!me.isSortable()) {
                return;
            }
 
            // This is the "group by" column - we have to set the grouper and tell it to
            // recalculate. AbstractStore#group just calls its Collection's updateGrouper
            // if passed a Grouper because *something* in the grouper might have changed,
            // but the config system would reject that as not a change.
            if (sorter.isGrouper) {
                if (sorter.getDirection() !== direction) {
                    sorter.toggle();
                    store.group(sorter);
                }
            }
            // We are moving to a sorted state
            else if (direction) {
                // We have a sorter - set its direction.
                if (sorter) {
                    // Not the primary. We will make it so.
                    // If it's already the primary, SorterCollection#addSort will toggle it
                    if (sorters.indexOf(sorter) !== 0) {
                        sorter.setDirection(direction);
                    }
                }
                // First time in, create a sorter with required direction
                else {
                    me.setSorter({
                        property: me.getSortParam(),
                        direction: 'ASC'
                    });
 
                    sorter = me.getSorter(); // not pickSorter
                }
 
                // If the grid is NOT configured with multi column sorting, then specify
                // "replace". Only if we are doing multi column sorting do we insert it as
                // one of a multi set.
                store.sort(sorter, mode || grid.getMultiColumnSort() ? 'multi' : 'replace');
            }
            // We're moving to an unsorted state
            else {
                if (sorter) {
                    sorters.remove(sorter);
 
                    // A locally sorted store will not refresh in response to having a
                    // sorter removed, so we must sync the column header arrows now.
                    // AbstractStore#onSorterEndUpdate will however always fire the sort
                    // event which is what Grid uses to trigger a HeaderContainer sort
                    // state sync
                    if (!store.getRemoteSort()) {
                        me.getRootHeaderCt().setSortState();
                    }
                }
            }
        },
 
        /**
         * Called on HeaderTap to toggle the column through three sort states.
         *
         *    Is primary sort?
         *      Yes - Cycle through ASC, DESC, None
         *      No - is sorted?
         *          Yes - Make it primary (leave direction alone)
         *          No - Make it primary ASC
         *
         *  - None -> ASC
         *  - ASC  -> DESC
         *  - DESC -> None
         */
        toggleSortState: function() {
            this.sort(this.directionSequence[this.sortState]);
        },
 
        /**
         * Sets the column sort state according to the direction of the Sorter passed.
         * @param {String/Ext.util.Sorter} sorter A Sorter, or the direction (`'ASC'` or `'DESC'`)
         * to display in the header.
         */
        setSortState: function(sorter) {
            // Set the UI state to reflect the state of any passed Sorter
            // Called by the grid's HeaderContainer on view refresh
            var me = this,
                store = me.getGrid().getStore(),
                grouper = store.isGrouped() && store.getGrouper(),
                oldDirection = me.sortState,
                direction = null,
                sortedCls = me.sortedCls,
                secondarySortCls = me.secondarySortCls,
                auxSortCls = me.auxSortCls,
                ascCls = sortedCls + '-asc',
                descCls = sortedCls + '-desc',
                ariaDom = me.ariaEl.dom,
                sortPrioClass = '',
                changed, index,
                remove = [
                    secondarySortCls,
                    auxSortCls
                ],
                add;
 
            if (sorter) {
                if (typeof sorter === 'string') {
                    direction = sorter;
                }
                else {
                    //<debug>
                    if (!sorter.isSorter) {
                        Ext.raise('Must pass a sorter instance into HeaderContainer#saveState');
                    }
                    //</debug>
 
                    // The Grouper is always primary
                    if (sorter === grouper) {
                        index = 0;
                    }
                    else {
                        index = store.getSorters().indexOf(sorter);
                    }
 
                    //<debug>
                    if (index === -1) {
                        Ext.raise("Sorter passed to HeaderContainer#saveState not in grid's store");
                    }
                    //</debug>
 
                    direction = sorter.getDirection();
                    sortPrioClass = index === 1 ? secondarySortCls : index > 1 ? auxSortCls : '';
                }
            }
 
            // Detect if we've changed state, then set our state
            changed = direction !== oldDirection;
            me.sortState = direction;
 
            switch (direction) {
                case 'DESC':
                    add = [sortedCls, descCls, sortPrioClass];
                    remove.push(ascCls);
                    break;
 
                case 'ASC':
                    add = [sortedCls, ascCls, sortPrioClass];
                    remove.push(descCls);
                    break;
 
                default:
                    remove.push(sortedCls, ascCls, descCls);
                    break;
            }
 
            me.replaceCls(remove, add);
 
            if (ariaDom) {
                if (direction) {
                    ariaDom.setAttribute('aria-sort', me.ariaSortStates[direction]);
                }
                else {
                    ariaDom.removeAttribute('aria-sort');
                }
            }
 
            // we only want to fire the event if we have actually sorted
            if (changed) {
                me.fireEvent('sort', me, direction, oldDirection);
            }
        },
 
        getVisibleCount: function() {
            var columns = this.getInnerItems(),
                len = columns.length,
                count = 0,
                i;
 
            for (= 0; i < len; ++i) {
                if (columns[i].isHeaderGroup) {
                    count += columns[i].getVisibleCount();
                }
                else {
                    count += columns[i].isHidden() ? 0 : 1;
                }
            }
 
            return count;
        },
 
        onShow: function() {
            var toShow;
 
            // No visible subcolumns, then show the first child.
            if (!this.getVisibleCount()) {
                toShow = this.getComponent(0);
 
                if (toShow) {
                    toShow.show();
                }
            }
        },
 
        doVisibilityCheck: function() {
            var me = this,
                columns = me.getInnerItems(),
                ln = columns.length,
                i, column;
 
            for (= 0; i < ln; i++) {
                column = columns[i];
 
                if (!column.isHidden()) {
                    if (me.isHidden()) {
                        if (me.initialized) {
                            me.show();
                        }
                        else {
                            me.setHidden(false);
                        }
                    }
 
                    return;
                }
            }
 
            me.hide();
 
            // Next time we show our hide/show item, we need to rebuild the submenu
            me.rebuildChildColumnsMenu = true;
 
            // Update hideable/showable state of column menu items
            me.updateMenuDisabledState();
        },
 
        onColumnShow: function() {
            var me = this,
                hideShowItem;
 
            if (me.getVisibleCount() > 0) {
                me.show();
                hideShowItem = me.getHideShowMenuItem(false);
                hideShowItem.setChecked(true);
                hideShowItem.setCheckChangeDisabled(false);
            }
 
            // Next time we show our hide/show item, we need to rebuild the submenu
            me.rebuildChildColumnsMenu = true;
 
            // Update hideable/showable state of column menu items
            me.updateMenuDisabledState();
        },
 
        onColumnHide: function(column) {
            var me = this,
                hideShowItem;
 
            if (me.getVisibleCount() === 0) {
                me.hide();
                hideShowItem = me.getHideShowMenuItem(false);
                hideShowItem.setChecked(false);
                hideShowItem.setCheckChangeDisabled(true);
            }
 
            // Next time we show our hide/show item, we need to rebuild the submenu
            me.rebuildChildColumnsMenu = true;
 
            // Update hideable/showable state of column menu items
            me.updateMenuDisabledState();
        },
 
        onColumnMove: function(column) {
            // Next time we show our hide/show item, we need to rebuild the submenu
            this.rebuildChildColumnsMenu = true;
        },
 
        scopeReplacer: function(config, original, prop, setter) {
            var me = this,
                name = config[prop];
 
            if (typeof name === 'string') {
                prop = prop || 'sorterFn';
                setter = setter || 'setSorterFn';
 
                if (original === config) {
                    config = Ext.apply({}, config);
                }
 
                // The goal of this method is to be called only on the first use
                // and then replace itself (using the setter) to direct all future
                // calls to the proper method.
                config[prop] = function() {
                    // NOTE "this" is Sorter or Grouper!
                    var scope = me.resolveListenerScope(),
                        fn = scope && scope[name],
                        ret = 0;
 
                    if (fn) {
                        this[setter](fn.bind(scope));
 
                        ret = fn.apply(scope, arguments);
                    }
                    //<debug>
                    else if (!scope) {
                        Ext.raise('Cannot resolve scope for column ' + me.id);
                    }
                    else {
                        Ext.raise('No such method "' + name + '" on ' + scope.$className);
                    }
                    //</debug>
 
                    return ret;
                };
            }
 
            return config;
        }
    } // privates
});