The source code

    /*!
     * Ext JS Library 3.3.1
     * Copyright(c) 2006-2010 Sencha Inc.
     * licensing@sencha.com
     * http://www.sencha.com/license
     */
    Ext.ns('Ext.ux.grid');

    Ext.ux.grid.ColumnHeaderGroup = Ext.extend(Ext.util.Observable, {

        constructor: function(config){
            this.config = config;
        },

        init: function(grid){
            Ext.applyIf(grid.colModel, this.config);
            Ext.apply(grid.getView(), this.viewConfig);
        },

        viewConfig: {
            initTemplates: function(){
                this.constructor.prototype.initTemplates.apply(this, arguments);
                var ts = this.templates || {};
                if(!ts.gcell){
                    ts.gcell = new Ext.XTemplate('', '', this.grid.enableHdMenu ? '' : '', '{value}');
                }
                this.templates = ts;
                this.hrowRe = new RegExp("ux-grid-hd-group-row-(\\d+)", "");
            },

            renderHeaders: function(){
                var ts = this.templates, headers = [], cm = this.cm, rows = cm.rows, tstyle = 'width:' + this.getTotalWidth() + ';';

                for(var row = 0, rlen = rows.length; row < rlen; row++){
                    var r = rows[row], cells = [];
                    for(var i = 0, gcol = 0, len = r.length; i < len; i++){
                        var group = r[i];
                        group.colspan = group.colspan || 1;
                        var id = this.getColumnId(group.dataIndex ? cm.findColumnIndex(group.dataIndex) : gcol), gs = Ext.ux.grid.ColumnHeaderGroup.prototype.getGroupStyle.call(this, group, gcol);
                        cells[i] = ts.gcell.apply({
                            cls: 'ux-grid-hd-group-cell',
                            id: id,
                            row: row,
                            style: 'width:' + gs.width + ';' + (gs.hidden ? 'display:none;' : '') + (group.align ? 'text-align:' + group.align + ';' : ''),
                            tooltip: group.tooltip ? (Ext.QuickTips.isEnabled() ? 'ext:qtip' : 'title') + '="' + group.tooltip + '"' : '',
                            istyle: group.align == 'right' ? 'padding-right:16px' : '',
                            btn: this.grid.enableHdMenu && group.header,
                            value: group.header || ' '
                        });
                        gcol += group.colspan;
                    }
                    headers[row] = ts.header.apply({
                        tstyle: tstyle,
                        cells: cells.join('')
                    });
                }
                headers.push(this.constructor.prototype.renderHeaders.apply(this, arguments));
                return headers.join('');
            },

            onColumnWidthUpdated: function(){
                this.constructor.prototype.onColumnWidthUpdated.apply(this, arguments);
                Ext.ux.grid.ColumnHeaderGroup.prototype.updateGroupStyles.call(this);
            },

            onAllColumnWidthsUpdated: function(){
                this.constructor.prototype.onAllColumnWidthsUpdated.apply(this, arguments);
                Ext.ux.grid.ColumnHeaderGroup.prototype.updateGroupStyles.call(this);
            },

            onColumnHiddenUpdated: function(){
                this.constructor.prototype.onColumnHiddenUpdated.apply(this, arguments);
                Ext.ux.grid.ColumnHeaderGroup.prototype.updateGroupStyles.call(this);
            },

            getHeaderCell: function(index){
                return this.mainHd.query(this.cellSelector)[index];
            },

            findHeaderCell: function(el){
                return el ? this.fly(el).findParent('td.x-grid3-hd', this.cellSelectorDepth) : false;
            },

            findHeaderIndex: function(el){
                var cell = this.findHeaderCell(el);
                return cell ? this.getCellIndex(cell) : false;
            },

            updateSortIcon: function(col, dir){
                var sc = this.sortClasses, hds = this.mainHd.select(this.cellSelector).removeClass(sc);
                hds.item(col).addClass(sc[dir == "DESC" ? 1 : 0]);
            },

            handleHdDown: function(e, t){
                var el = Ext.get(t);
                if(el.hasClass('x-grid3-hd-btn')){
                    e.stopEvent();
                    var hd = this.findHeaderCell(t);
                    Ext.fly(hd).addClass('x-grid3-hd-menu-open');
                    var index = this.getCellIndex(hd);
                    this.hdCtxIndex = index;
                    var ms = this.hmenu.items, cm = this.cm;
                    ms.get('asc').setDisabled(!cm.isSortable(index));
                    ms.get('desc').setDisabled(!cm.isSortable(index));
                    this.hmenu.on('hide', function(){
                        Ext.fly(hd).removeClass('x-grid3-hd-menu-open');
                    }, this, {
                        single: true
                    });
                    this.hmenu.show(t, 'tl-bl?');
                }else if(el.hasClass('ux-grid-hd-group-cell') || Ext.fly(t).up('.ux-grid-hd-group-cell')){
                    e.stopEvent();
                }
            },

            handleHdMove: function(e, t){
                var hd = this.findHeaderCell(this.activeHdRef);
                if(hd && !this.headersDisabled && !Ext.fly(hd).hasClass('ux-grid-hd-group-cell')){
                    var hw = this.splitHandleWidth || 5, r = this.activeHdRegion, x = e.getPageX(), ss = hd.style, cur = '';
                    if(this.grid.enableColumnResize !== false){
                        if(x - r.left <= hw && this.cm.isResizable(this.activeHdIndex - 1)){
                            cur = Ext.isAir ? 'move' : Ext.isWebKit ? 'e-resize' : 'col-resize'; // col-resize
                                                                                                    // not
                                                                                                    // always
                                                                                                    // supported
                        }else if(r.right - x <= (!this.activeHdBtn ? hw : 2) && this.cm.isResizable(this.activeHdIndex)){
                            cur = Ext.isAir ? 'move' : Ext.isWebKit ? 'w-resize' : 'col-resize';
                        }
                    }
                    ss.cursor = cur;
                }
            },

            handleHdOver: function(e, t){
                var hd = this.findHeaderCell(t);
                if(hd && !this.headersDisabled){
                    this.activeHdRef = t;
                    this.activeHdIndex = this.getCellIndex(hd);
                    var fly = this.fly(hd);
                    this.activeHdRegion = fly.getRegion();
                    if(!(this.cm.isMenuDisabled(this.activeHdIndex) || fly.hasClass('ux-grid-hd-group-cell'))){
                        fly.addClass('x-grid3-hd-over');
                        this.activeHdBtn = fly.child('.x-grid3-hd-btn');
                        if(this.activeHdBtn){
                            this.activeHdBtn.dom.style.height = (hd.firstChild.offsetHeight - 1) + 'px';
                        }
                    }
                }
            },

            handleHdOut: function(e, t){
                var hd = this.findHeaderCell(t);
                if(hd && (!Ext.isIE || !e.within(hd, true))){
                    this.activeHdRef = null;
                    this.fly(hd).removeClass('x-grid3-hd-over');
                    hd.style.cursor = '';
                }
            },

            handleHdMenuClick: function(item){
                var index = this.hdCtxIndex, cm = this.cm, ds = this.ds, id = item.getItemId();
                switch(id){
                    case 'asc':
                        ds.sort(cm.getDataIndex(index), 'ASC');
                        break;
                    case 'desc':
                        ds.sort(cm.getDataIndex(index), 'DESC');
                        break;
                    default:
                        if(id.substr(0, 6) == 'group-'){
                            var i = id.split('-'), row = parseInt(i[1], 10), col = parseInt(i[2], 10), r = this.cm.rows[row], group, gcol = 0;
                            for(var i = 0, len = r.length; i < len; i++){
                                group = r[i];
                                if(col >= gcol && col < gcol + group.colspan){
                                    break;
                                }
                                gcol += group.colspan;
                            }
                            if(item.checked){
                                var max = cm.getColumnsBy(this.isHideableColumn, this).length;
                                for(var i = gcol, len = gcol + group.colspan; i < len; i++){
                                    if(!cm.isHidden(i)){
                                        max--;
                                    }
                                }
                                if(max < 1){
                                    this.onDenyColumnHide();
                                    return false;
                                }
                            }
                            for(var i = gcol, len = gcol + group.colspan; i < len; i++){
                                if(cm.config[i].fixed !== true && cm.config[i].hideable !== false){
                                    cm.setHidden(i, item.checked);
                                }
                            }
                        }else if(id.substr(0, 4) == 'col-'){
                            index = cm.getIndexById(id.substr(4));
                            if(index != -1){
                                if(item.checked && cm.getColumnsBy(this.isHideableColumn, this).length <= 1){
                                    this.onDenyColumnHide();
                                    return false;
                                }
                                cm.setHidden(index, item.checked);
                            }
                        }
                        if(id.substr(0, 6) == 'group-' || id.substr(0, 4) == 'col-'){
                            item.checked = !item.checked;
                            if(item.menu){
                                var updateChildren = function(menu){
                                    menu.items.each(function(childItem){
                                        if(!childItem.disabled){
                                            childItem.setChecked(item.checked, false);
                                            if(childItem.menu){
                                                updateChildren(childItem.menu);
                                            }
                                        }
                                    });
                                }
                                updateChildren(item.menu);
                            }
                            var parentMenu = item, parentItem;
                            while(parentMenu = parentMenu.parentMenu){
                                if(!parentMenu.parentMenu || !(parentItem = parentMenu.parentMenu.items.get(parentMenu.getItemId())) || !parentItem.setChecked){
                                    break;
                                }
                                var checked = parentMenu.items.findIndexBy(function(m){
                                    return m.checked;
                                }) >= 0;
                                parentItem.setChecked(checked, true);
                            }
                            item.checked = !item.checked;
                        }
                }
                return true;
            },

            beforeColMenuShow: function(){
                var cm = this.cm, rows = this.cm.rows;
                this.colMenu.removeAll();
                for(var col = 0, clen = cm.getColumnCount(); col < clen; col++){
                    var menu = this.colMenu, title = cm.getColumnHeader(col), text = [];
                    if(cm.config[col].fixed !== true && cm.config[col].hideable !== false){
                        for(var row = 0, rlen = rows.length; row < rlen; row++){
                            var r = rows[row], group, gcol = 0;
                            for(var i = 0, len = r.length; i < len; i++){
                                group = r[i];
                                if(col >= gcol && col < gcol + group.colspan){
                                    break;
                                }
                                gcol += group.colspan;
                            }
                            if(group && group.header){
                                if(cm.hierarchicalColMenu){
                                    var gid = 'group-' + row + '-' + gcol,
                                        item = menu.items ? menu.getComponent(gid) : null,
                                        submenu = item ? item.menu : null;
                                    if(!submenu){
                                        submenu = new Ext.menu.Menu({
                                            itemId: gid
                                        });
                                        submenu.on("itemclick", this.handleHdMenuClick, this);
                                        var checked = false, disabled = true;
                                        for(var c = gcol, lc = gcol + group.colspan; c < lc; c++){
                                            if(!cm.isHidden(c)){
                                                checked = true;
                                            }
                                            if(cm.config[c].hideable !== false){
                                                disabled = false;
                                            }
                                        }
                                        menu.add({
                                            itemId: gid,
                                            text: group.header,
                                            menu: submenu,
                                            hideOnClick: false,
                                            checked: checked,
                                            disabled: disabled
                                        });
                                    }
                                    menu = submenu;
                                }else{
                                    text.push(group.header);
                                }
                            }
                        }
                        text.push(title);
                        menu.add(new Ext.menu.CheckItem({
                            itemId: "col-" + cm.getColumnId(col),
                            text: text.join(' '),
                            checked: !cm.isHidden(col),
                            hideOnClick: false,
                            disabled: cm.config[col].hideable === false
                        }));
                    }
                }
            },

            afterRenderUI: function(){
                this.constructor.prototype.afterRenderUI.apply(this, arguments);
                Ext.apply(this.columnDrop, Ext.ux.grid.ColumnHeaderGroup.prototype.columnDropConfig);
                Ext.apply(this.splitZone, Ext.ux.grid.ColumnHeaderGroup.prototype.splitZoneConfig);
            }
        },

        splitZoneConfig: {
            allowHeaderDrag: function(e){
                return !e.getTarget(null, null, true).hasClass('ux-grid-hd-group-cell');
            }
        },

        columnDropConfig: {
            getTargetFromEvent: function(e){
                var t = Ext.lib.Event.getTarget(e);
                return this.view.findHeaderCell(t);
            },

            positionIndicator: function(h, n, e){
                var data = Ext.ux.grid.ColumnHeaderGroup.prototype.getDragDropData.call(this, h, n, e);
                if(data === false){
                    return false;
                }
                var px = data.px + this.proxyOffsets[0];
                this.proxyTop.setLeftTop(px, data.r.top + this.proxyOffsets[1]);
                this.proxyTop.show();
                this.proxyBottom.setLeftTop(px, data.r.bottom);
                this.proxyBottom.show();
                return data.pt;
            },

            onNodeDrop: function(n, dd, e, data){
                var h = data.header;
                if(h != n){
                    var d = Ext.ux.grid.ColumnHeaderGroup.prototype.getDragDropData.call(this, h, n, e);
                    if(d === false){
                        return false;
                    }
                    var cm = this.grid.colModel, right = d.oldIndex < d.newIndex, rows = cm.rows;
                    for(var row = d.row, rlen = rows.length; row < rlen; row++){
                        var r = rows[row], len = r.length, fromIx = 0, span = 1, toIx = len;
                        for(var i = 0, gcol = 0; i < len; i++){
                            var group = r[i];
                            if(d.oldIndex >= gcol && d.oldIndex < gcol + group.colspan){
                                fromIx = i;
                            }
                            if(d.oldIndex + d.colspan - 1 >= gcol && d.oldIndex + d.colspan - 1 < gcol + group.colspan){
                                span = i - fromIx + 1;
                            }
                            if(d.newIndex >= gcol && d.newIndex < gcol + group.colspan){
                                toIx = i;
                            }
                            gcol += group.colspan;
                        }
                        var groups = r.splice(fromIx, span);
                        rows[row] = r.splice(0, toIx - (right ? span : 0)).concat(groups).concat(r);
                    }
                    for(var c = 0; c < d.colspan; c++){
                        var oldIx = d.oldIndex + (right ? 0 : c), newIx = d.newIndex + (right ? -1 : c);
                        cm.moveColumn(oldIx, newIx);
                        this.grid.fireEvent("columnmove", oldIx, newIx);
                    }
                    return true;
                }
                return false;
            }
        },

        getGroupStyle: function(group, gcol){
            var width = 0, hidden = true;
            for(var i = gcol, len = gcol + group.colspan; i < len; i++){
                if(!this.cm.isHidden(i)){
                    var cw = this.cm.getColumnWidth(i);
                    if(typeof cw == 'number'){
                        width += cw;
                    }
                    hidden = false;
                }
            }
            return {
                width: (Ext.isBorderBox || (Ext.isWebKit && !Ext.isSafari2) ? width : Math.max(width - this.borderWidth, 0)) + 'px',
                hidden: hidden
            };
        },

        updateGroupStyles: function(col){
            var tables = this.mainHd.query('.x-grid3-header-offset > table'), tw = this.getTotalWidth(), rows = this.cm.rows;
            for(var row = 0; row < tables.length; row++){
                tables[row].style.width = tw;
                if(row < rows.length){
                    var cells = tables[row].firstChild.firstChild.childNodes;
                    for(var i = 0, gcol = 0; i < cells.length; i++){
                        var group = rows[row][i];
                        if((typeof col != 'number') || (col >= gcol && col < gcol + group.colspan)){
                            var gs = Ext.ux.grid.ColumnHeaderGroup.prototype.getGroupStyle.call(this, group, gcol);
                            cells[i].style.width = gs.width;
                            cells[i].style.display = gs.hidden ? 'none' : '';
                        }
                        gcol += group.colspan;
                    }
                }
            }
        },

        getGroupRowIndex: function(el){
            if(el){
                var m = el.className.match(this.hrowRe);
                if(m && m[1]){
                    return parseInt(m[1], 10);
                }
            }
            return this.cm.rows.length;
        },

        getGroupSpan: function(row, col){
            if(row < 0){
                return {
                    col: 0,
                    colspan: this.cm.getColumnCount()
                };
            }
            var r = this.cm.rows[row];
            if(r){
                for(var i = 0, gcol = 0, len = r.length; i < len; i++){
                    var group = r[i];
                    if(col >= gcol && col < gcol + group.colspan){
                        return {
                            col: gcol,
                            colspan: group.colspan
                        };
                    }
                    gcol += group.colspan;
                }
                return {
                    col: gcol,
                    colspan: 0
                };
            }
            return {
                col: col,
                colspan: 1
            };
        },

        getDragDropData: function(h, n, e){
            if(h.parentNode != n.parentNode){
                return false;
            }
            var cm = this.grid.colModel, x = Ext.lib.Event.getPageX(e), r = Ext.lib.Dom.getRegion(n.firstChild), px, pt;
            if((r.right - x) <= (r.right - r.left) / 2){
                px = r.right + this.view.borderWidth;
                pt = "after";
            }else{
                px = r.left;
                pt = "before";
            }
            var oldIndex = this.view.getCellIndex(h), newIndex = this.view.getCellIndex(n);
            if(cm.isFixed(newIndex)){
                return false;
            }
            var row = Ext.ux.grid.ColumnHeaderGroup.prototype.getGroupRowIndex.call(this.view, h),
                oldGroup = Ext.ux.grid.ColumnHeaderGroup.prototype.getGroupSpan.call(this.view, row, oldIndex),
                newGroup = Ext.ux.grid.ColumnHeaderGroup.prototype.getGroupSpan.call(this.view, row, newIndex),
                oldIndex = oldGroup.col;
                newIndex = newGroup.col + (pt == "after" ? newGroup.colspan : 0);
            if(newIndex >= oldGroup.col && newIndex <= oldGroup.col + oldGroup.colspan){
                return false;
            }
            var parentGroup = Ext.ux.grid.ColumnHeaderGroup.prototype.getGroupSpan.call(this.view, row - 1, oldIndex);
            if(newIndex < parentGroup.col || newIndex > parentGroup.col + parentGroup.colspan){
                return false;
            }
            return {
                r: r,
                px: px,
                pt: pt,
                row: row,
                oldIndex: oldIndex,
                newIndex: newIndex,
                colspan: oldGroup.colspan
            };
        }
    });