com.sencha.gxt.widget.core.client.grid.GroupingView.java Source code

Java tutorial

Introduction

Here is the source code for com.sencha.gxt.widget.core.client.grid.GroupingView.java

Source

/**
 * Sencha GXT 4.0.0 - Sencha for GWT
 * Copyright (c) 2006-2015, Sencha Inc.
 *
 * licensing@sencha.com
 * http://www.sencha.com/products/gxt/license/
 *
 * ================================================================================
 * Open Source License
 * ================================================================================
 * This version of Sencha GXT is licensed under the terms of the Open Source GPL v3
 * license. You may use this license only if you are prepared to distribute and
 * share the source code of your application under the GPL v3 license:
 * http://www.gnu.org/licenses/gpl.html
 *
 * If you are NOT prepared to distribute and share the source code of your
 * application under the GPL v3 license, other commercial and oem licenses
 * are available for an alternate download of Sencha GXT.
 *
 * Please see the Sencha GXT Licensing page at:
 * http://www.sencha.com/products/gxt/license/
 *
 * For clarification or additional options, please contact:
 * licensing@sencha.com
 * ================================================================================
 *
 *
 * ================================================================================
 * Disclaimer
 * ================================================================================
 * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND
 * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
 * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY,
 * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND
 * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING.
 * ================================================================================
 */
package com.sencha.gxt.widget.core.client.grid;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.safecss.shared.SafeStyles;
import com.google.gwt.safecss.shared.SafeStylesUtils;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.Widget;
import com.sencha.gxt.core.client.GXT;
import com.sencha.gxt.core.client.ValueProvider;
import com.sencha.gxt.core.client.dom.XElement;
import com.sencha.gxt.core.client.util.Point;
import com.sencha.gxt.core.client.util.Util;
import com.sencha.gxt.data.shared.SortDir;
import com.sencha.gxt.data.shared.SortInfo;
import com.sencha.gxt.data.shared.SortInfoBean;
import com.sencha.gxt.data.shared.Store.StoreSortInfo;
import com.sencha.gxt.messages.client.DefaultMessages;
import com.sencha.gxt.widget.core.client.event.CheckChangeEvent;
import com.sencha.gxt.widget.core.client.event.CheckChangeEvent.CheckChangeHandler;
import com.sencha.gxt.widget.core.client.event.CollapseItemEvent;
import com.sencha.gxt.widget.core.client.event.CollapseItemEvent.CollapseItemHandler;
import com.sencha.gxt.widget.core.client.event.CollapseItemEvent.HasCollapseItemHandlers;
import com.sencha.gxt.widget.core.client.event.ExpandItemEvent;
import com.sencha.gxt.widget.core.client.event.ExpandItemEvent.ExpandItemHandler;
import com.sencha.gxt.widget.core.client.event.ExpandItemEvent.HasExpandItemHandlers;
import com.sencha.gxt.widget.core.client.menu.CheckMenuItem;
import com.sencha.gxt.widget.core.client.menu.Item;
import com.sencha.gxt.widget.core.client.menu.Menu;
import com.sencha.gxt.widget.core.client.menu.MenuItem;
import com.sencha.gxt.widget.core.client.menu.SeparatorMenuItem;

/**
 * <code>GridView</code> that groups data based on a given grouping column.
 * 
 * @param <M> the model type
 */
public class GroupingView<M> extends GridView<M>
        implements HasCollapseItemHandlers<List<M>>, HasExpandItemHandlers<List<M>> {

    /**
     * Wrapper describing a given group, with the items in the group, and the value they hold in common. These are not
     * presently persisted, but only analyzed when changes occur to the grid's data.
     *
     * @param <M> the type of row in the grid
     */
    public static class GroupingData<M> {
        private final Object value;
        private final int startRow;
        private final List<M> items = new ArrayList<M>();
        private boolean collapsed;

        public GroupingData(Object value, int startRow) {
            this.value = value;
            this.startRow = startRow;
        }

        @Override
        public boolean equals(Object other) {
            if (other instanceof GroupingData) {
                return Util.equalWithNull(((GroupingData<?>) other).value, value);
            }
            return false;
        }

        public List<M> getItems() {
            return items;
        }

        public int getStartRow() {
            return startRow;
        }

        public Object getValue() {
            return value;
        }

        @Override
        public int hashCode() {
            return value == null ? 0 : value.hashCode();
        }

        public boolean isCollapsed() {
            return collapsed;
        }

        public void setCollapsed(boolean collapsed) {
            this.collapsed = collapsed;
        }

    }

    public interface GroupingViewAppearance {

        XElement findHead(XElement element);

        XElement getGroup(XElement head);

        ImageResource groupByIcon();

        boolean isCollapsed(XElement group);

        void onGroupExpand(XElement group, boolean expanded);

        SafeHtml renderGroupHeader(GroupingData<?> groupInfo);

        GroupingViewStyle style();
    }

    public interface GroupingViewStyle extends CssResource {

        String bodyCollapsed();

        String group();

        String groupCollapsed();

        String groupHead();
    }

    public interface GroupSummaryTemplate<M> {
        SafeHtml renderGroupSummary(GroupingData<M> groupInfo);
    }

    protected ColumnConfig<M, ?> lastGroupField;
    protected ColumnConfig<M, ?> groupingColumn;

    protected boolean enableGrouping;

    @SuppressWarnings("unused")
    private GroupSummaryTemplate<M> groupSummaryTemplate;
    private final GroupingViewAppearance groupAppearance;

    private boolean enableGroupingMenu = true;
    private boolean enableNoGroups = true;
    private boolean showGroupedColumn = true;
    private boolean startCollapsed = false;
    protected final Map<Object, Boolean> state = new HashMap<Object, Boolean>();

    private boolean isUpdating = false;
    private StoreSortInfo<M> lastStoreSort;
    private SortInfo lastSort;

    /**
     * Creates a new grouping view instance.
     */
    public GroupingView() {
        this(GWT.<GridAppearance>create(GridAppearance.class),
                GWT.<GroupingViewAppearance>create(GroupingViewAppearance.class));
    }

    /**
     * Creates a new grouping view instance.
     *
     * @param appearance the grid appearance
     * @param groupingAppearance the grouping appearance
     */
    public GroupingView(GridAppearance appearance, GroupingViewAppearance groupingAppearance) {
        super(appearance);
        this.groupAppearance = groupingAppearance;
    }

    /**
     * Creates a new grouping view instance.
     *
     * @param groupAppearance the group appearance
     */
    public GroupingView(GroupingViewAppearance groupAppearance) {
        this(GWT.<GridAppearance>create(GridAppearance.class), groupAppearance);
    }

    @Override
    public HandlerRegistration addCollapseHandler(CollapseItemHandler<List<M>> handler) {
        return addHandler(CollapseItemEvent.getType(), handler);
    }

    @Override
    public HandlerRegistration addExpandHandler(ExpandItemHandler<List<M>> handler) {
        return addHandler(ExpandItemEvent.getType(), handler);
    }

    /**
     * Collapses all groups.
     */
    public void collapseAllGroups() {
        toggleAllGroups(false);
    }

    /**
     * Expands all groups.
     */
    public void expandAllGroups() {
        toggleAllGroups(true);
    }

    /**
     * Returns the grouping view appearance.
     *
     * @return the grouping appearance
     */
    public GroupingViewAppearance getGroupingAppearance() {
        return groupAppearance;
    }

    @Override
    public StoreSortInfo<M> getSortState() {
        if (groupingColumn != null) {
            if (ds.getSortInfo().size() > 1) {
                return ds.getSortInfo().get(1);
            }
        }
        return super.getSortState();
    }

    public void groupBy(ColumnConfig<M, ?> column) {
        if (grid == null) {
            // if still being configured, save the grouping column for later
            groupingColumn = column;
        }

        if (column != groupingColumn) {
            // remove the existing group, if any
            if (groupingColumn != null) {
                if (grid.getLoader() == null || !grid.getLoader().isRemoteSort()) {
                    assert lastStoreSort != null && ds.getSortInfo().contains(lastStoreSort);
                    // remove the lastStoreSort from the listStore
                    ds.getSortInfo().remove(lastStoreSort);
                } else {
                    assert lastSort != null;
                    grid.getLoader().removeSortInfo(lastSort);
                }
            } else {// groupingColumn == null;
                assert lastStoreSort == null && lastSort == null;
            }

            // set the new one
            groupingColumn = column;
            if (column != null) {
                if (grid.getLoader() == null || !grid.getLoader().isRemoteSort()) {
                    lastStoreSort = createStoreSortInfo(column, SortDir.ASC);
                    ds.addSortInfo(0, lastStoreSort);// this triggers the sort
                } else {
                    lastSort = new SortInfoBean(column.getValueProvider(), SortDir.ASC);
                    grid.getLoader().addSortInfo(0, lastSort);
                    grid.getLoader().load();
                }
            } else {// new column == null
                lastStoreSort = null;
                lastSort = null;
                // full redraw without groups
                refresh(false);
            }
        }

        if (column == null) {
            doLastSort();
        }
    }

    /**
     * Returns true if the grouping menu is enabled.
     *
     * @return the enable grouping state
     */
    public boolean isEnableGroupingMenu() {
        return enableGroupingMenu;
    }

    /**
     * True to enable the the grouping menu items in the header context menu (defaults to true).
     *
     * @param enableGroupingMenu true to enable the grouping menu items
     */
    public void setEnableGroupingMenu(boolean enableGroupingMenu) {
        this.enableGroupingMenu = enableGroupingMenu;
    }

    /**
     * Returns true if the user can turn off grouping.
     *
     * @return the enable no groups state
     */
    public boolean isEnabledNoGroups() {
        return enableNoGroups;
    }

    /**
     * Returns true if the group is expanded.
     *
     * @param group the group
     * @return true if expanded
     */
    public boolean isExpanded(Element group) {
        return !groupAppearance.isCollapsed(group.<XElement>cast());
    }

    /**
     * Returns true if the grouped column is visible.
     *
     * @return the show grouped column
     */
    public boolean isShowGroupedColumn() {
        return showGroupedColumn;
    }

    /**
     * Sets whether the grouped column is visible (defaults to true).
     *
     * @param showGroupedColumn true to show the grouped column
     */
    public void setShowGroupedColumn(boolean showGroupedColumn) {
        this.showGroupedColumn = showGroupedColumn;
    }

    /**
     * Returns true if start collapsed is enabled.
     *
     * @return the start collapsed state
     */
    public boolean isStartCollapsed() {
        return startCollapsed;
    }

    /**
     * Sets whether the groups should start collapsed (defaults to false).
     *
     * @param startCollapsed true to start collapsed
     */
    public void setStartCollapsed(boolean startCollapsed) {
        this.startCollapsed = startCollapsed;
    }

    /**
     * True to enable the no groups menu item in the header context menu (defaults to true).
     *
     * @param enableNoGroups true to enable no groups menu item
     */
    public void setEnableNoGroups(boolean enableNoGroups) {
        this.enableNoGroups = enableNoGroups;
    }

    /**
     * Toggles all groups.
     *
     * @param expanded true to expand
     */
    public void toggleAllGroups(boolean expanded) {
        NodeList<Element> groups = getGroups();
        List<GroupingData<M>> groupData = createGroupingData();
        for (int i = 0; i < groups.getLength(); i++) {
            toggleGroup(groups.getItem(i), expanded);
            GroupingData<M> groupingData = groupData.get(i);
            if (expanded) {
                fireEvent(new ExpandItemEvent<List<M>>(groupingData.getItems()));
            } else {
                fireEvent(new CollapseItemEvent<List<M>>(groupingData.getItems()));
            }
        }
    }

    @Override
    protected void afterRender() {
        ColumnConfig<M, ?> column = groupingColumn;

        // set groupingColumn to null to force regrouping only if grouping
        // hasn't already occurred
        if (lastStoreSort == null && lastSort == null && column != null) {
            groupingColumn = null;
        }
        groupBy(column);
        super.afterRender();
    }

    @Override
    protected Menu createContextMenu(final int colIndex) {
        Menu menu = super.createContextMenu(colIndex);

        if (menu != null && enableGroupingMenu) {
            if (cm.isGroupable(colIndex)) {
                MenuItem groupBy = new MenuItem(DefaultMessages.getMessages().groupingView_groupByText());
                groupBy.setIcon(groupAppearance.groupByIcon());
                groupBy.addSelectionHandler(new SelectionHandler<Item>() {
                    @Override
                    public void onSelection(SelectionEvent<Item> event) {
                        groupBy(cm.getColumn(colIndex));
                    }
                });
                menu.add(new SeparatorMenuItem());
                menu.add(groupBy);

                groupBy.setEnabled(cm.getColumnCount(true) > 1);

                initMenuColumnShowHideHandling(menu, groupBy);
            }
            if (enableGrouping && enableNoGroups) {
                final CheckMenuItem showInGroups = new CheckMenuItem(
                        DefaultMessages.getMessages().groupingView_showGroupsText());
                showInGroups.setChecked(true);
                showInGroups.addSelectionHandler(new SelectionHandler<Item>() {
                    @Override
                    public void onSelection(SelectionEvent<Item> event) {
                        if (showInGroups.isChecked()) {
                            groupBy(cm.getColumn(colIndex));
                        } else {
                            groupBy(null);
                        }
                    }
                });
                menu.add(showInGroups);
            }
        }

        return menu;
    }

    /**
     * @see #createGroupingData(java.util.List, int)
     * @return the group data for the current store
     */
    protected List<GroupingData<M>> createGroupingData() {
        List<GroupingData<M>> groups = new ArrayList<GroupingData<M>>();

        GroupingData<M> curGroup = null;
        for (int i = 0, len = ds.size(); i < len; i++) {
            M m = ds.get(i);
            final Object gvalue;
            if (ds.hasRecord(m)) {
                gvalue = ds.getRecord(m).getValue(groupingColumn.getValueProvider());
            } else {
                gvalue = groupingColumn.getValueProvider().getValue(m);
            }
            if (curGroup == null || !valueBelongsInGroup(curGroup, gvalue)) {
                curGroup = makeGroupForRow(i, m, gvalue);
                verifyNewGroup(groups, curGroup);
                groups.add(curGroup);
            } else {
                curGroup.getItems().add(m);
            }
        }

        return groups;
    }

    /**
     * @see #createGroupingData()
     * @param rows set of rows in the store to build into groups
     * @param startGroupIndex the index to use for the first group in the returned list
     * @return a list of groups
     */
    protected List<GroupingData<M>> createGroupingData(List<M> rows, int startGroupIndex) {
        List<GroupingData<M>> groups = new ArrayList<GroupingData<M>>();

        // iterate through each item, creating a new group as needed. Assumes the
        // list is sorted
        GroupingData<M> curGroup = null;
        for (int j = 0; j < rows.size(); j++) {
            M model = rows.get(j);

            int rowIndex = (j + startGroupIndex);

            // the value for the group field
            final Object gvalue;
            if (ds.hasRecord(model)) {
                gvalue = ds.getRecord(model).getValue(groupingColumn.getValueProvider());
            } else {
                gvalue = groupingColumn.getValueProvider().getValue(model);
            }

            if (curGroup == null || !valueBelongsInGroup(curGroup, gvalue)) {
                curGroup = makeGroupForRow(rowIndex, model, gvalue);
                verifyNewGroup(groups, curGroup);
                groups.add(curGroup);

            } else {
                curGroup.getItems().add(model);
            }
        }
        return groups;
    }

    protected void doLastSort() {
        StoreSortInfo<M> info = getSortState();
        if (info == null) {
            return;
        }

        ValueProvider<? super M, ?> vp = info.getValueProvider();
        if (vp == null) {
            return;
        }

        String p = vp.getPath();
        if (p == null) {
            return;
        }

        ColumnConfig<M, ?> config = cm.findColumnConfig(p);
        if (config == null) {
            return;
        }

        int index = cm.indexOf(config);
        doSort(index, info.getDirection());
    }

    @Override
    protected SafeHtml doRender(List<ColumnData> cs, List<M> rows, int startRow) {
        enableGrouping = groupingColumn != null;

        if (!enableGrouping || isUpdating) {
            return super.doRender(cs, rows, startRow);
        }

        List<GroupingData<M>> groups = createGroupingData(rows, startRow);

        SafeHtmlBuilder buf = new SafeHtmlBuilder();

        String styles = "width:" + getTotalWidth() + "px;";
        SafeStyles tableStyles = SafeStylesUtils.fromTrustedString(styles);

        for (int i = 0, len = groups.size(); i < len; i++) {
            GroupingData<M> g = groups.get(i);
            SafeHtml renderedRows = tpls.table(getAppearance().styles().dataTable(), tableStyles,
                    super.doRender(cs, g.getItems(), g.getStartRow()), renderHiddenHeaders(getColumnWidths()));
            renderGroup(buf, g, renderedRows);
        }

        return buf.toSafeHtml();
    }

    @Override
    protected void doSort(int colIndex, SortDir sortDir) {
        ColumnConfig<M, ?> column = cm.getColumn(colIndex);
        if (groupingColumn != null) {
            if (grid.getLoader() == null || !grid.getLoader().isRemoteSort()) {
                // first sort is lastStoreSort
                assert lastStoreSort != null;
                ds.getSortInfo().clear();

                StoreSortInfo<M> sortInfo = createStoreSortInfo(column, sortDir);

                if (sortDir == null && storeSortInfo != null
                        && storeSortInfo.getValueProvider().getPath().equals(column.getValueProvider().getPath())) {
                    sortInfo.setDirection(storeSortInfo.getDirection() == SortDir.ASC ? SortDir.DESC : SortDir.ASC);
                } else if (sortDir == null) {
                    sortInfo.setDirection(SortDir.ASC);
                }

                ds.getSortInfo().add(0, lastStoreSort);
                ds.getSortInfo().add(1, sortInfo);

                if (GWT.isProdMode()) {
                    ds.applySort(false);
                } else {
                    try {
                        // applySort will apply its sort when called, which might trigger an
                        // exception if the column passed in's data isn't Comparable
                        ds.applySort(false);
                    } catch (ClassCastException ex) {
                        GWT.log("Column can't be sorted " + column.getValueProvider().getPath()
                                + " is not Comparable. ", ex);
                        throw ex;
                    }
                }
            } else {
                assert lastSort != null;
                ValueProvider<? super M, ?> vp = column.getValueProvider();
                grid.getLoader().clearSortInfo();
                grid.getLoader().addSortInfo(0, lastSort);
                grid.getLoader().addSortInfo(1, new SortInfoBean(vp, sortDir));
                grid.getLoader().load();
            }

        } else {
            super.doSort(colIndex, sortDir);
        }
    }

    protected int getGroupIndex(XElement group) {
        return group.getParentElement().<XElement>cast().indexOf(group) / 2;
    }

    protected NodeList<Element> getGroups() {
        if (!enableGrouping) {
            return JsArray.createArray().cast();
        }
        return dataTable.<XElement>cast().select("." + groupAppearance.style().group());
    }

    @Override
    protected NodeList<Element> getRows() {
        if (!enableGrouping || !hasRows()) {
            return super.getRows();
        }
        return dataTable.<XElement>cast().select("." + styles.row());
    }

    /**
     * @param groupIndex the index of this group
     * @param firstModel the first model to add to the group
     * @param value the value that this group is based on
     * @return the newly created group
     */
    protected GroupingData<M> makeGroupForRow(int groupIndex, M firstModel, Object value) {
        GroupingData<M> curGroup;
        curGroup = new GroupingData<M>(value, groupIndex);
        curGroup.setCollapsed(state.containsKey(value) ? state.get(value) : isStartCollapsed());
        curGroup.getItems().add(firstModel);
        return curGroup;
    }

    @Override
    protected void onAdd(List<M> models, int index) {
        if (enableGrouping) {
            Point ss = getScrollState();
            refresh(false);
            restoreScroll(ss);
        } else {
            super.onAdd(models, index);
        }
    }

    @Override
    protected void onMouseDown(Event ge) {
        super.onMouseDown(ge);
        XElement head = ge.getEventTarget().cast();
        head = groupAppearance.findHead(head);
        if (head != null) {
            ge.stopPropagation();
            XElement group = groupAppearance.getGroup(head);
            int index = getGroupIndex(group);
            toggleGroup(index, groupAppearance.isCollapsed(group));
        }
    }

    @Override
    protected void onRemove(M m, int index, boolean isUpdate) {
        Element parentToRemove = null;
        if (enableGrouping) {
            Element row = getRow(index).cast();

            // TODO appearance this
            Element groupContainer = row.getParentElement().cast();
            if (groupContainer.getChildCount() == 1) {
                parentToRemove = groupContainer.getParentElement().cast();
            }
        }
        super.onRemove(m, index, isUpdate);
        if (parentToRemove != null) {
            parentToRemove.removeFromParent();
        }
    }

    @Override
    protected void refreshRow(int row) {
        isUpdating = true;
        super.refreshRow(row);
        isUpdating = false;
    }

    protected void renderGroup(SafeHtmlBuilder buf, GroupingData<M> g, SafeHtml renderedRows) {
        String groupClass = groupAppearance.style().group();
        String bodyClass = "";
        if (g.isCollapsed()) {
            groupClass += " " + groupAppearance.style().groupCollapsed();
            bodyClass = groupAppearance.style().bodyCollapsed();
        }
        String headClass = groupAppearance.style().groupHead();
        final SafeHtml groupHtml;
        String cellClasses = headClass + " " + styles.cell() + " " + states.cell();
        if (selectable) {
            groupHtml = (tpls.tr(groupClass, tpls.tdWrap(cm.getColumnCount(), cellClasses,
                    styles.cellInner() + " " + states.cellInner(), renderGroupHeader(g))));
        } else {
            String innerCellClasses = styles.cellInner() + " " + states.cellInner() + " " + styles.noPadding();
            if (GXT.isIE()) {
                groupHtml = (tpls.tr(groupClass, tpls.tdWrapUnselectable(cm.getColumnCount(), cellClasses,
                        innerCellClasses, renderGroupHeader(g))));
            } else {
                groupHtml = (tpls.tr(groupClass,
                        tpls.tdWrap(cm.getColumnCount(), cellClasses, innerCellClasses, renderGroupHeader(g))));
            }

        }
        buf.append(groupHtml);

        buf.append(tpls.tr(bodyClass, tpls.tdWrap(cm.getColumnCount(), "", "", renderedRows)));
    }

    protected SafeHtml renderGroupHeader(GroupingData<M> groupInfo) {
        return groupAppearance.renderGroupHeader(groupInfo);
    }

    @Override
    protected SafeHtml renderRows(int startRow, int endRow) {
        boolean eg = groupingColumn != null;
        if (!showGroupedColumn) {
            int colIndex = cm.indexOf(groupingColumn);
            if (!eg && lastGroupField != null) {
                dataTableBody.removeChildren();
                cm.setHidden(cm.indexOf(lastGroupField), false);
                cm.getColumn(cm.indexOf(lastGroupField)).setHideable(true);
                lastGroupField = groupingColumn;
            } else if (eg && (lastGroupField == null || lastGroupField == groupingColumn)) {
                lastGroupField = groupingColumn;
                cm.setHidden(colIndex, true);
                cm.getColumn(colIndex).setHideable(false);
            } else if (eg && lastGroupField != null && !groupingColumn.equals(lastGroupField)) {
                dataTableBody.removeChildren();
                int oldIndex = cm.indexOf(lastGroupField);
                cm.setHidden(oldIndex, false);
                cm.getColumn(oldIndex).setHideable(true);
                lastGroupField = groupingColumn;
                cm.setHidden(colIndex, true);
                cm.getColumn(colIndex).setHideable(false);
            }
        }
        return super.renderRows(startRow, endRow);
    }

    /**
     * Toggles the given group index, dealing with all logical and view details.
     */
    protected void toggleGroup(int i, boolean expanded) {
        GroupingData<M> groupingData = createGroupingData().get(i);
        Object key = groupingData.getValue();
        state.put(key, !expanded);
        toggleGroup(getGroups().getItem(i), expanded);
        if (expanded) {
            fireEvent(new ExpandItemEvent<List<M>>(groupingData.getItems()));
        } else {
            fireEvent(new CollapseItemEvent<List<M>>(groupingData.getItems()));
        }
    }

    /**
     * Toggles the visibility of the group elements, but does not handle the internal state details or events.
     */
    protected void toggleGroup(Element group, boolean expanded) {
        assert group != null;
        groupAppearance.onGroupExpand(group.<XElement>cast(), expanded);
        calculateVBar(false);
    }

    /**
     * @param group a group that the value might match
     * @param value an object which may match the data in group.getValue()
     * @return true if a row with the given value belongs in the group
     */
    protected boolean valueBelongsInGroup(GroupingData<M> group, Object value) {
        return Util.equalWithNull(group.getValue(), value);
    }

    /**
     * Ensures that items which belong in the same group are not fragmented across multiple groups. It is technically
     * possible to override this to do nothing, but doing so is unsupported.
     * This method should be an assertion - use {@code assert} or {@link Class#desiredAssertionStatus()} to ensure that
     * this won't be in the compiled app if assertions are disabled.
     *
     * @param groups all groups created before the current one
     * @param nextGroup the next group to add to the list, assuming it shouldn't already be listed there
     */
    protected void verifyNewGroup(List<GroupingData<M>> groups, GroupingData<M> nextGroup) {
        assert !groups.contains(
                nextGroup) : "List didn't appear to be correctly sorted, group exists in more than one place in the list";
    }

    private void initMenuColumnShowHideHandling(Menu menu, final MenuItem groupBy) {
        // Loop through the menu and find the columns
        for (int i = 0; i < menu.getWidgetCount(); i++) {
            Widget subMenuWidget = menu.getWidget(i);
            // Only work with the columns MenuItem instances
            if (subMenuWidget instanceof MenuItem) {
                MenuItem columnsMenu = (MenuItem) subMenuWidget;
                String hasColumns = columnsMenu.getData("gxt-columns");
                // Find the columns and add handlers onto the CheckMenuItems columns
                if (hasColumns != null && hasColumns.equals("true")) {
                    Menu colMenu = columnsMenu.getSubMenu();
                    for (int b = 0; b < colMenu.getWidgetCount(); b++) {
                        CheckMenuItem colItem = (CheckMenuItem) colMenu.getWidget(b);
                        // Observe column events 'showing' and 'hiding' events
                        colItem.addCheckChangeHandler(new CheckChangeHandler<CheckMenuItem>() {
                            @Override
                            public void onCheckChange(CheckChangeEvent<CheckMenuItem> event) {
                                Scheduler.get().scheduleDeferred(new ScheduledCommand() {
                                    @Override
                                    public void execute() {
                                        // Disable the group by option when only *one* column is displayed
                                        groupBy.setEnabled(cm.getColumnCount(true) > 1);
                                    }
                                });
                            }
                        });
                    }
                }
            }
        }
    }

}