com.extjs.gxt.ui.client.widget.grid.GridView.java Source code

Java tutorial

Introduction

Here is the source code for com.extjs.gxt.ui.client.widget.grid.GridView.java

Source

/*
 * Sencha GXT 2.3.1 - Sencha for GWT
 * Copyright(c) 2007-2013, Sencha, Inc.
 * licensing@sencha.com
 * 
 * http://www.sencha.com/products/gxt/license/
 */
package com.extjs.gxt.ui.client.widget.grid;

import com.extjs.gxt.ui.client.GXT;
import com.extjs.gxt.ui.client.Style;
import com.extjs.gxt.ui.client.Style.HorizontalAlignment;
import com.extjs.gxt.ui.client.Style.SortDir;
import com.extjs.gxt.ui.client.core.DomHelper;
import com.extjs.gxt.ui.client.core.El;
import com.extjs.gxt.ui.client.core.XDOM;
import com.extjs.gxt.ui.client.data.BaseModel;
import com.extjs.gxt.ui.client.data.ModelData;
import com.extjs.gxt.ui.client.data.PagingLoadConfig;
import com.extjs.gxt.ui.client.data.SortInfo;
import com.extjs.gxt.ui.client.event.*;
import com.extjs.gxt.ui.client.js.JsArray;
import com.extjs.gxt.ui.client.store.ListStore;
import com.extjs.gxt.ui.client.store.Record;
import com.extjs.gxt.ui.client.store.StoreEvent;
import com.extjs.gxt.ui.client.store.StoreListener;
import com.extjs.gxt.ui.client.util.*;
import com.extjs.gxt.ui.client.widget.Component;
import com.extjs.gxt.ui.client.widget.ComponentHelper;
import com.extjs.gxt.ui.client.widget.menu.CheckMenuItem;
import com.extjs.gxt.ui.client.widget.menu.Menu;
import com.extjs.gxt.ui.client.widget.menu.MenuItem;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.*;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.AbstractImagePrototype;
import com.google.gwt.user.client.ui.Widget;

import java.util.*;

/**
 * This class encapsulates the user interface of an {@link Grid}. Methods of
 * this class may be used to access user interface elements to enable special
 * display effects. Do not change the DOM structure of the user interface. </p>
 * <p />
 * This class does not provide ways to manipulate the underlying data. The data
 * model of a Grid is held in an {@link ListStore}.
 */
public class GridView extends BaseObservable {

    /**
     * Icons used by Grid which can be overridden as needed. s
     */
    public class GridViewImages {
        private AbstractImagePrototype columns = GXT.IMAGES.grid_columns();
        private AbstractImagePrototype sortAsc = GXT.IMAGES.grid_sortAsc();
        private AbstractImagePrototype sortDesc = GXT.IMAGES.grid_sortDesc();

        public AbstractImagePrototype getColumns() {
            return columns;
        }

        public AbstractImagePrototype getSortAsc() {
            return sortAsc;
        }

        public AbstractImagePrototype getSortDesc() {
            return sortDesc;
        }

        public void setColumns(AbstractImagePrototype columnsIcon) {
            this.columns = columnsIcon;
        }

        public void setSortAsc(AbstractImagePrototype sortAscIcon) {
            this.sortAsc = sortAscIcon;
        }

        public void setSortDesc(AbstractImagePrototype sortDescIcon) {
            this.sortDesc = sortDescIcon;
        }
    }

    private static JavaScriptObject colRe;
    protected int activeHdIndex;
    protected boolean autoFill;
    protected int borderWidth = 2;

    protected ColumnModel cm;
    protected Listener<ColumnModelEvent> columnListener;
    protected boolean deferEmptyText;
    protected ListStore<ModelData> ds;
    // elements
    protected El el, mainWrap, mainHd, innerHd, scroller, mainBody, focusEl;
    protected SafeHtml emptyText = SafeGxt.NO_BREAK_SPACE;
    protected boolean enableHdMenu = true;
    // config
    protected boolean enableRowBody;
    protected boolean focusEnabled = true;
    protected ColumnFooter footer;
    protected boolean forceFit;

    protected Grid<ModelData> grid;
    protected ColumnHeader header;
    protected int headerColumnIndex;
    protected boolean headerDisabled;
    protected GridViewImages images;

    protected int lastViewWidth;
    protected StoreListener<ModelData> listener;

    protected Element overRow;
    protected boolean preventScrollToTopOnRefresh;
    protected int scrollOffset = XDOM.getScrollBarWidth();
    protected boolean selectable = false;
    protected SortInfo sortState;
    protected int splitterWidth = 5;
    protected GridTemplates templates;
    protected boolean userResized;
    // we first render grid with a vbar, and remove as needed
    protected boolean vbar = true;

    protected GridViewConfig viewConfig;
    protected List<List<Widget>> widgetList = new ArrayList<List<Widget>>();
    private DelayedTask addTask = new DelayedTask(new Listener<BaseEvent>() {
        public void handleEvent(BaseEvent be) {
            calculateVBar(false);
            refreshFooterData();
        }
    });
    private boolean adjustForHScroll = true;
    private String cellSelector = "td.x-grid3-cell";
    private int cellSelectorDepth = 4;

    private DelayedTask removeTask = new DelayedTask(new Listener<BaseEvent>() {
        public void handleEvent(BaseEvent be) {
            calculateVBar(false);
            applyEmptyText();
            refreshFooterData();
            processRows(0, false);
        }
    });
    private String rowSelector = "div.x-grid3-row";
    private int rowSelectorDepth = 10;
    private boolean showDirtyCells = true;

    private boolean showInvalidCells;

    /**
     * Ensured the current row and column is visible.
     * 
     * @param row the row index
     * @param col the column index
     * @param hscroll true to scroll horizontally if needed
     * @return the calculated point
     */
    public Point ensureVisible(int row, int col, boolean hscroll) {
        if (grid == null || !grid.isViewReady() || row < 0 || row > ds.getCount()) {
            return null;
        }

        if (col == -1) {
            col = 0;
        }

        Element rowEl = getRow(row);
        Element cellEl = null;
        if (!(!hscroll && col == 0)) {
            while (cm.isHidden(col)) {
                col++;
            }
            cellEl = getCell(row, col);

        }

        if (rowEl == null) {
            return null;
        }

        Element c = scroller.dom;

        int ctop = 0;
        Element p = rowEl, stope = el.dom;
        while (p != null && p != stope) {
            ctop += p.getOffsetTop();
            p = p.getOffsetParent().cast();
        }
        ctop -= mainHd.dom.getOffsetHeight();

        int cbot = ctop + rowEl.getOffsetHeight();

        int ch = c.getOffsetHeight();
        int stop = c.getScrollTop();
        int sbot = stop + ch;

        if (ctop < stop) {
            c.setScrollTop(ctop);
        } else if (cbot > sbot) {
            if (hscroll && (cm.getTotalWidth() > scroller.getWidth() - scrollOffset)) {
                cbot += scrollOffset;
            }
            c.setScrollTop(cbot -= ch);
        }

        if (hscroll && cellEl != null) {
            int cleft = cellEl.getOffsetLeft();
            int cright = cleft + cellEl.getOffsetWidth();
            int sleft = c.getScrollLeft();
            int sright = sleft + c.getOffsetWidth();
            if (cleft < sleft) {
                c.setScrollLeft(cleft);
            } else if (cright > sright) {
                c.setScrollLeft(cright - scroller.getStyleWidth());
            }
        }

        return cellEl != null ? fly(cellEl).getXY() : new Point(c.getScrollLeft(), fly(rowEl).getY());
    }

    /**
     * Returns the cell.
     * 
     * @param elem the cell element or a child element
     * @return the cell element
     */
    public Element findCell(Element elem) {
        if (elem == null) {
            return null;
        }
        return fly(elem).findParentElement(cellSelector, cellSelectorDepth);
    }

    /**
     * Returns the cell index.
     * 
     * @param elem the cell or child element
     * @param requiredStyle an optional required style name
     * @return the cell index or -1 if not found
     */
    public int findCellIndex(Element elem, String requiredStyle) {
        Element cell = findCell(elem);
        if (cell != null && (requiredStyle == null || fly(cell).hasStyleName(requiredStyle))) {
            return getCellIndex(cell);
        }
        return -1;
    }

    /**
     * Returns the row element.
     * 
     * @param el the row element or any child element
     * @return the matching row element
     */
    public Element findRow(Element el) {
        if (el == null) {
            return null;
        }
        return fly(el).findParentElement(rowSelector, rowSelectorDepth);
    }

    /**
     * Returns the row index.
     * 
     * @param elem the row or child of the row element
     * @return the index
     */
    public int findRowIndex(Element elem) {
        Element r = findRow(elem);
        return r != null ? r.getPropertyInt("rowIndex") : -1;
    }

    /**
     * Focus the cell and scrolls into view.
     * 
     * @param rowIndex the row index
     * @param colIndex the column index
     * @param hscroll true to scroll horizontally
     */
    public void focusCell(int rowIndex, int colIndex, boolean hscroll) {
        Point xy = ensureVisible(rowIndex, colIndex, hscroll);
        if (xy != null) {
            focusEl.setXY(xy);
            if (focusEnabled) {
                focusGrid();
            }
        }
    }

    /**
     * Focus the row and scrolls into view.
     * 
     * @param rowIndex the row index
     */
    public void focusRow(int rowIndex) {
        focusCell(rowIndex, 0, true);
    }

    /**
     * Returns the grid's body element.
     * 
     * @return the body element
     */
    public El getBody() {
        return scroller;
    }

    /**
     * Returns the grid's &lt;TD> HtmlElement at the specified coordinates.
     * 
     * @param row the row index in which to find the cell
     * @param col the column index of the cell
     * @return the &lt;TD> at the specified coordinates
     */
    public Element getCell(int row, int col) {
        // ROW DIV TABLE TR TD
        Element rowEl = getRow(row);
        return (Element) ((rowEl != null && rowEl.hasChildNodes())
                ? rowEl.getFirstChild().getFirstChild().getFirstChild().getChildNodes().getItem(col)
                : null);
    }

    /**
     * Returns the cell selector depth.
     * 
     * @return the cell selector depth
     */
    public int getCellSelectorDepth() {
        return cellSelectorDepth;
    }

    /**
     * Returns the editor parent element.
     * 
     * @return the editor element
     */
    public Element getEditorParent() {
        return scroller.dom;
    }

    /**
     * Returns the empty text.
     * 
     * @return the empty text
     */
    public SafeHtml getEmptyText() {
        return emptyText;
    }

    /**
     * Returns the grid's column header.
     * 
     * @return the header
     */
    public ColumnHeader getHeader() {
        return header;
    }

    /**
     * Returns the &lt;TD> HtmlElement which represents the Grid's header cell for
     * the specified column index.
     * 
     * @param index the column index
     * @return the &lt;TD> element.
     */
    public Element getHeaderCell(int index) {
        return mainHd.dom.getElementsByTagName("td").getItem(index);
    }

    /**
     * Returns the images used by grid.
     * 
     * @return the images
     */
    public GridViewImages getImages() {
        if (images == null) {
            images = new GridViewImages();
        }
        return images;
    }

    /**
     * Return the &lt;TR> HtmlElement which represents a Grid row for the
     * specified index.
     * 
     * @param row the row index
     * @return the &lt;TR> element
     */
    public Element getRow(int row) {
        if (row < 0) {
            return null;
        }
        return getRows().getItem(row);
    }

    /**
     * Return the &lt;TR> HtmlElement which represents a Grid row for the
     * specified model.
     * 
     * @param m the model
     * @return the &lt;TR> element
     */
    public Element getRow(ModelData m) {
        return getRow(ds.indexOf(m));
    }

    /**
     * Returns the row selector depth.
     * 
     * @return the row selector depth
     */
    public int getRowSelectorDepth() {
        return rowSelectorDepth;
    }

    /**
     * Returns the scroll element.
     * 
     * @return the scroll element
     */
    public El getScroller() {
        return scroller;
    }

    /**
     * Returns the current scroll state.
     * 
     * @return the scroll state
     */
    public Point getScrollState() {
        return new Point(scroller.getScrollLeft(), scroller.getScrollTop());
    }

    /**
     * Returns the view config.
     * 
     * @return the view config
     */
    public GridViewConfig getViewConfig() {
        return viewConfig;
    }

    /**
     * Returns the widget at the current location.
     * 
     * @param rowIndex the row index
     * @param colIndex the column index
     * @return the widget or null
     */
    public Widget getWidget(int rowIndex, int colIndex) {
        List<Widget> map = rowIndex < widgetList.size() ? widgetList.get(rowIndex) : null;
        return map != null && colIndex < map.size() ? map.get(colIndex) : null;
    }

    /**
     * Returns true if the grid width will be adjusted based on visibility of
     * horizontal scroll bar.
     * 
     * @return true if adjusting
     */
    public boolean isAdjustForHScroll() {
        return adjustForHScroll;
    }

    /**
     * Returns true if auto fill is enabled.
     * 
     * @return true for auto fill
     */
    public boolean isAutoFill() {
        return autoFill;
    }

    /**
     * Returns true if force fit is enabled.
     * 
     * @return true for force fit
     */
    public boolean isForceFit() {
        return forceFit;
    }

    /**
     * Returns true if dirty cell markers are enabled.
     * 
     * @return true of dirty cell markers
     */
    public boolean isShowDirtyCells() {
        return showDirtyCells;
    }

    /**
     * Returns true if invalid cell markers are enabled.
     * 
     * @return true if enabled
     */
    public boolean isShowInvalidCells() {
        return showInvalidCells;
    }

    /**
     * Returns true if sorting is enabled.
     * 
     * @return true for sorting
     */
    public boolean isSortingEnabled() {
        return !headerDisabled;
    }

    public void layout() {
        layout(false);
    }

    /**
     * Rebuilds the grid using its current configuration and data.
     * 
     * @param headerToo true to refresh the header
     */
    public void refresh(boolean headerToo) {
        if (grid != null && grid.isViewReady()) {
            stopEditing();

            detachWidgets(0, -1, true);

            if (!preventScrollToTopOnRefresh) {
                scrollToTop();
            }
            mainBody.setInnerHtml(renderRows(0, -1));
            if (headerToo) {
                sortState = null;
                header.release();

                newColumnHeader();
                renderHeader();
                if (grid.isAttached()) {
                    ComponentHelper.doAttach(header);
                }
                header.setEnableColumnResizing(grid.isColumnResize());
                header.setEnableColumnReorder(grid.isColumnReordering());
            }
            processRows(0, true);
            renderWidgets(0, -1);

            if (footer != null) {
                ComponentHelper.doDetach(footer);
                footer.el().removeFromParent();
            }
            if (cm.getAggregationRows().size() > 0) {
                footer = new ColumnFooter(grid, cm);
                renderFooter();
                if (grid.isAttached()) {
                    ComponentHelper.doAttach(footer);
                }
            }

            calculateVBar(true);

            updateHeaderSortState();

            applyEmptyText();
            constrainFocusElement();

            fireEvent(Events.Refresh);
        }
    }

    /**
     * Scrolls the grid to the top.
     */
    public void scrollToTop() {
        scroller.setScrollTop(0);
        scroller.setScrollLeft(0);
    }

    /**
     * True to adjust the grid width when the horizontal scrollbar is hidden and
     * visible (defaults to true).
     * 
     * @param adjustForHScroll true to adjust for horizontal scroll bar
     */
    public void setAdjustForHScroll(boolean adjustForHScroll) {
        this.adjustForHScroll = adjustForHScroll;
    }

    /**
     * True to auto expand the columns to fit the grid <b>when the grid is
     * created</b>.
     * 
     * @param autoFill true to expand
     */
    public void setAutoFill(boolean autoFill) {
        this.autoFill = autoFill;
    }

    /**
     * The number of levels to search for cells in event delegation (defaults to
     * 4).
     * 
     * @param cellSelectorDepth the cell selector depth
     */
    public void setCellSelectorDepth(int cellSelectorDepth) {
        this.cellSelectorDepth = cellSelectorDepth;
    }

    /**
     * Default text to display in the grid body when no rows are available
     * (defaults to '').
     * 
     * @param emptyText the empty text
     */
    public void setEmptyText(SafeHtml emptyText) {
        this.emptyText = emptyText;
    }

    public void setEmptyText(String emptyText) {
        setEmptyText(SafeGxt.fromNullableString(emptyText));
    }

    /**
     * True to auto expand/contract the size of the columns to fit the grid width
     * and prevent horizontal scrolling.
     * 
     * @param forceFit true to force fit
     */
    public void setForceFit(boolean forceFit) {
        this.forceFit = forceFit;
    }

    /**
     * The number of levels to search for rows in event delegation (defaults to
     * 10).
     * 
     * @param rowSelectorDepth the row selector depth
     */
    public void setRowSelectorDepth(int rowSelectorDepth) {
        this.rowSelectorDepth = rowSelectorDepth;
    }

    /**
     * True to display a red triangle in the upper left corner of any cells which
     * are "dirty" as defined by any existing records in the data store (defaults
     * to true).
     * 
     * @param showDirtyCells true to display the dirty flag
     */
    public void setShowDirtyCells(boolean showDirtyCells) {
        this.showDirtyCells = showDirtyCells;
    }

    /**
     * True to enabled invalid cell markers (defaults to false).
     * 
     * @param showInvalidCells true to enable
     */
    public void setShowInvalidCells(boolean showInvalidCells) {
        this.showInvalidCells = showInvalidCells;
    }

    /**
     * True to allow column sorting when the user clicks a column (defaults to
     * true).
     * 
     * @param sortable true for sortable columns
     */
    public void setSortingEnabled(boolean sortable) {
        this.headerDisabled = !sortable;
    }

    /**
     * Sets the view config.
     * 
     * @param viewConfig the view config
     */
    public void setViewConfig(GridViewConfig viewConfig) {
        this.viewConfig = viewConfig;
    }

    protected void addRowStyle(Element elem, String style) {
        if (elem != null) {
            fly(elem).addStyleName(style);
        }
    }

    protected void afterRender() {
        mainBody.setInnerHtml(renderRows(0, -1));
        renderWidgets(0, -1);
        processRows(0, true);

        if (footer != null && grid.getLazyRowRender() > 0) {
            footer.refresh();
        }

        int sh = scroller.getHeight();
        int dh = mainBody.getHeight();
        boolean vbar = dh < sh;
        if (vbar) {
            this.vbar = !vbar;
            lastViewWidth = -1;
            layout();
        }

        applyEmptyText();

    }

    protected void applyEmptyText() {
        if (emptyText == null) {
            emptyText = SafeGxt.NO_BREAK_SPACE;
        }
        if (!hasRows()) {
            mainBody.setInnerHtml(SafeHtmlUtils
                    .fromTrustedString("<div class='x-grid-empty'>" + emptyText.asString() + "</div>"));
        }
        syncHScroll();
    }

    protected void autoExpand(boolean preventUpdate) {
        if (!userResized && grid.getAutoExpandColumn() != null) {
            int tw = cm.getTotalWidth(false);
            int aw = grid.getWidth(true) - getScrollAdjust();
            if (tw != aw) {
                int ci = cm.getIndexById(grid.getAutoExpandColumn());
                assert ci != Style.DEFAULT : "auto expand column not found";
                if (cm.isHidden(ci)) {
                    return;
                }
                int currentWidth = cm.getColumnWidth(ci);
                int cw = Math.min(Math.max(((aw - tw) + currentWidth), grid.getAutoExpandMin()),
                        grid.getAutoExpandMax());
                if (cw != currentWidth) {
                    cm.setColumnWidth(ci, cw, true);

                    if (!preventUpdate) {
                        updateColumnWidth(ci, cw);
                    }
                }
            }
        }
    }

    protected void calculateVBar(boolean force) {
        if (force) {
            resize();
        }
        int sh = scroller.getHeight();
        int dh = mainBody.getHeight();
        boolean vbar = dh > sh;
        if (force || this.vbar != vbar) {
            this.vbar = vbar;
            lastViewWidth = -1;
            layout(true);
        }
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    protected GridEvent<?> createComponentEvent(Event event) {
        return new GridEvent(grid, event);
    }

    protected Menu createContextMenu(final int colIndex) {
        final Menu menu = new Menu();

        if (cm.isSortable(colIndex)) {
            MenuItem item = new MenuItem();
            item.setHtml(GXT.MESSAGES.gridView_sortAscText());
            item.setIcon(getImages().getSortAsc());
            item.addSelectionListener(new SelectionListener<MenuEvent>() {
                public void componentSelected(MenuEvent ce) {
                    doSort(colIndex, SortDir.ASC);
                }

            });
            menu.add(item);

            item = new MenuItem();
            item.setHtml(GXT.MESSAGES.gridView_sortDescText());
            item.setIcon(getImages().getSortDesc());
            item.addSelectionListener(new SelectionListener<MenuEvent>() {
                public void componentSelected(MenuEvent ce) {
                    doSort(colIndex, SortDir.DESC);
                }
            });
            menu.add(item);
        }

        MenuItem columns = new MenuItem();
        columns.setHtml(GXT.MESSAGES.gridView_columnsText());
        columns.setIcon(getImages().getColumns());
        columns.setData("gxt-columns", "true");

        final Menu columnMenu = new Menu();

        int cols = cm.getColumnCount();
        for (int i = 0; i < cols; i++) {
            if (shouldNotCount(i, false)) {
                continue;
            }
            final int fcol = i;
            final CheckMenuItem check = new CheckMenuItem();
            check.setHideOnClick(false);
            check.setHtml(cm.getColumnHeader(i));
            check.setChecked(!cm.isHidden(i));
            check.addSelectionListener(new SelectionListener<MenuEvent>() {
                public void componentSelected(MenuEvent ce) {
                    cm.setHidden(fcol, !cm.isHidden(fcol));
                    restrictMenu(columnMenu);
                }
            });
            columnMenu.add(check);
        }

        restrictMenu(columnMenu);
        columns.setEnabled(columnMenu.getItemCount() > 0);
        columns.setSubMenu(columnMenu);
        menu.add(columns);
        return menu;
    }

    protected void detachWidget(int rowIndex, boolean remove) {
        List<Widget> m = rowIndex < widgetList.size() ? widgetList.get(rowIndex) : null;
        if (m != null) {
            for (Widget w : m) {
                ComponentHelper.doDetach(w);
                if (w != null) {
                    El.fly(w.getElement()).removeFromParent();
                }
            }
            if (remove) {
                widgetList.remove(rowIndex);
            }
        }
    }

    protected void detachWidgets(int startRow, int endRow, boolean remove) {
        if (endRow == -1) {
            endRow = widgetList.size() - 1;
        }
        for (int i = endRow; i >= startRow; i--) {
            detachWidget(i, remove);
        }

    }

    protected void doAttach() {
        ComponentHelper.doAttach(header);
        ComponentHelper.doAttach(footer);
        resize();
        renderWidgets(0, -1);
    }

    protected void doDetach() {
        ComponentHelper.doDetach(header);
        ComponentHelper.doDetach(footer);
        detachWidgets(0, -1, false);
    }

    protected String doRender(List<ColumnData> cs, List<ModelData> rows, int startRow, int colCount,
            boolean stripe) {
        int last = colCount - 1;
        String tstyle = "width:" + getTotalWidth() + "px;";

        StringBuilder buf = new StringBuilder();

        for (int j = 0; j < rows.size(); j++) {
            ModelData model = (ModelData) rows.get(j);

            model = prepareData(model);

            Record r = ds.hasRecord(model) ? ds.getRecord(model) : null;

            int rowBodyColSpanCount = colCount;
            if (enableRowBody) {
                if (grid.getSelectionModel() instanceof CheckBoxSelectionModel<?>) {
                    CheckBoxSelectionModel<?> sm = (CheckBoxSelectionModel<?>) grid.getSelectionModel();
                    if (cm.getColumnById(sm.getColumn().getId()) != null) {
                        rowBodyColSpanCount--;
                    }
                }
                for (ColumnConfig c : cm.getColumns()) {
                    if (c instanceof RowNumberer) {
                        rowBodyColSpanCount--;
                    }
                }
            }
            int rowIndex = (j + startRow);

            if (GXT.isAriaEnabled()) {
                buf.append("<div role=\"row\" aria-level=\"2\" class=\"x-grid3-row ");
            } else {
                buf.append("<div class=\"x-grid3-row ");
            }

            if (stripe && ((rowIndex + 1) % 2 == 0)) {
                buf.append(" x-grid3-row-alt");
            }
            if (!selectable) {
                buf.append(" x-unselectable-single");
            }

            if (showDirtyCells && r != null && r.isDirty()) {
                buf.append(" x-grid3-dirty-row");
            }
            if (viewConfig != null) {
                buf.append(" ");
                buf.append(viewConfig.getRowStyle(model, rowIndex, ds));
            }
            buf.append("\" style=\"");
            buf.append(tstyle);
            buf.append("\" id=\"");
            buf.append(grid.getId());
            buf.append("_");
            buf.append(ds.getKeyProvider() != null ? ds.getKeyProvider().getKey(model) : XDOM.getUniqueId());
            buf.append("\" unselectable=\"");
            buf.append(selectable ? "off" : "on");
            buf.append(
                    "\"><table class=x-grid3-row-table role=presentation border=0 cellspacing=0 cellpadding=0 style=\"");

            buf.append(tstyle);
            buf.append("\"><tbody role=presentation><tr role=presentation>");
            widgetList.add(rowIndex, new ArrayList<Widget>());
            for (int i = 0; i < colCount; i++) {
                ColumnData c = cs.get(i);
                c.css = c.css == null ? "" : c.css;
                SafeHtml rv = getRenderedValue(c, rowIndex, i, model, c.name);
                String role = "gridcell";
                if (GXT.isAriaEnabled()) {
                    ColumnConfig cc = cm.getColumn(i);
                    if (cc.isRowHeader()) {
                        role = "rowheader";
                    }
                }

                String attr = c.cellAttr != null ? c.cellAttr : "";
                String cellAttr = c.cellAttr != null ? c.cellAttr : "";

                buf.append("<td id=\"" + XDOM.getUniqueId() + "\" role=\"" + role
                        + "\" class=\"x-grid3-col x-grid3-cell x-grid3-td-");
                buf.append(c.id);
                buf.append(" ");
                buf.append(i == 0 ? "x-grid-cell-first " : (i == last ? "x-grid3-cell-last " : ""));
                if (c.css != null) {
                    buf.append(c.css);
                }
                if (showInvalidCells && r != null && !r.isValid(c.name)) {
                    buf.append(" x-grid3-invalid-cell");
                }
                if (showDirtyCells && r != null && r.getChanges().containsKey(c.name)) {
                    buf.append(" x-grid3-dirty-cell");
                }

                buf.append("\" style=\"");
                buf.append(c.style);
                buf.append("\" ");
                buf.append(cellAttr);
                buf.append("><div unselectable=\"");
                buf.append(selectable ? "off" : "on");
                buf.append("\" class=\"x-grid3-cell-inner x-grid3-col-");
                buf.append(c.id);
                buf.append("\" ");
                buf.append(attr);
                buf.append(">");
                buf.append(rv.asString());
                buf.append("</div></td>");
            }

            buf.append("</tr>");
            if (enableRowBody) {
                buf.append("<tr class=x-grid3-row-body-tr style=\"\"><td colspan=");
                buf.append(rowBodyColSpanCount);
                buf.append(" class=x-grid3-body-cell><div class=x-grid3-row-body>${body}</div></td></tr>");
            }
            buf.append("</tbody></table></div>");
        }

        return buf.toString();
    }

    protected void doSort(int colIndex, SortDir sortDir) {
        ds.sort(cm.getDataIndex(colIndex), sortDir);
    }

    protected void fitColumns(boolean preventRefresh, boolean onlyExpand, int omitColumn) {
        int tw = cm.getTotalWidth(false);
        int aw = grid.el().getWidth(true) - getScrollAdjust();
        if (aw <= 0) {
            aw = grid.el().getStyleWidth();
        }

        if (aw < 20 || aw > 2000) { // not initialized, so don't screw up the
            // default widths
            return;
        }

        int extra = (int) aw - tw;

        if (extra == 0) {
            return;
        }

        int colCount = cm.getColumnCount();
        Stack<Integer> cols = new Stack<Integer>();
        int width = 0;
        int w;

        for (int i = 0; i < colCount; i++) {
            w = cm.getColumnWidth(i);
            if (!cm.isHidden(i) && !cm.isFixed(i) && i != omitColumn) {
                cols.push(i);
                cols.push(w);
                width += w;
            }
        }

        double frac = ((double) (extra)) / width;
        while (cols.size() > 0) {
            w = cols.pop();
            int i = cols.pop();
            int ww = Math.max(grid.getMinColumnWidth(), (int) Math.floor(w + w * frac));
            cm.setColumnWidth(i, ww, true);
        }

        tw = getTotalWidth();
        if (tw > aw) {
            width = 0;
            for (int i = 0; i < colCount; i++) {
                w = cm.getColumnWidth(i);
                if (!cm.isHidden(i) && !cm.isFixed(i) && w > grid.getMinColumnWidth()) {
                    cols.push(i);
                    cols.push(w);
                    width += w;
                }
            }
            frac = ((double) (aw - tw)) / width;
            while (cols.size() > 0) {
                w = cols.pop();
                int i = cols.pop();
                int ww = Math.max(grid.getMinColumnWidth(), (int) Math.floor(w + w * frac));
                cm.setColumnWidth(i, ww, true);
            }
        }

        if (!preventRefresh) {
            updateAllColumnWidths();
        }
    }

    protected El fly(Element elem) {
        return El.fly(elem, "grid");
    }

    protected void focusGrid() {
        focusEl.setFocus(true);
    }

    protected int getCellIndex(Element elem) {
        if (elem != null) {
            String id = getCellIndexId(elem);
            if (id != null) {
                return cm.getIndexById(id);
            }
        }
        return -1;
    }

    protected List<ColumnData> getColumnData() {
        int colCount = cm.getColumnCount();
        List<ColumnData> cs = new ArrayList<ColumnData>();
        for (int i = 0; i < colCount; i++) {
            String name = cm.getDataIndex(i);
            ColumnData data = new ColumnData();
            data.name = name == null ? cm.getColumnId(i) : name;
            data.renderer = cm.getRenderer(i);
            data.id = cm.getColumnId(i);
            data.style = getColumnStyle(i, false);
            cs.add(data);
        }
        return cs;
    }

    protected String getColumnStyle(int colIndex, boolean isHeader) {
        String style = !isHeader ? cm.getColumnStyle(colIndex) : "";
        if (style == null)
            style = "";
        style += "width:" + (getColumnWidth(colIndex)) + "px;";
        if (cm.isHidden(colIndex)) {
            style += "display:none;";
        }
        HorizontalAlignment align = cm.getColumnAlignment(colIndex);
        if (align != null) {
            style += "text-align:" + align.name() + ";";
        }
        return style;
    }

    protected int getColumnWidth(int col) {
        int w = cm.getColumnWidth(col);
        return (El.isBorderBoxTableFixed() ? w : (w - borderWidth > 0 ? w - borderWidth : 0));
    }

    protected int getOffsetWidth() {
        return (getTotalWidth() + getScrollAdjust());
    }

    protected SafeHtml getRenderedValue(ColumnData data, int rowIndex, int colIndex, ModelData m, String property) {
        GridCellRenderer<ModelData> r = cm.getRenderer(colIndex);
        List<Widget> rowMap = widgetList.get(rowIndex);
        rowMap.add(colIndex, null);
        if (r != null) {
            SafeHtml o = r.render(ds.getAt(rowIndex), property, data, rowIndex, colIndex, ds, grid);
            if (o != null) {
                return o;
            }
        }
        Object val = m.get(property);
        SafeHtml html = null;
        ColumnConfig c = cm.getColumn(colIndex);

        if (val != null) {
            if (val instanceof Number && c.getNumberFormat() != null) {
                Number n = (Number) val;
                html = SafeHtmlUtils.fromTrustedString(c.getNumberFormat().format(n.doubleValue()));
            } else if (val instanceof Date && c.getDateTimeFormat() != null) {
                DateTimeFormat dtf = c.getDateTimeFormat();
                html = SafeHtmlUtils.fromTrustedString(dtf.format((Date) val));
            } else {
                String valString = val.toString();
                if (valString != null) {
                    html = SafeHtmlUtils.fromString(valString);
                }
            }
        }
        return SafeGxt.emptyToNbSpace(html);
    }

    protected NodeList<Element> getRows() {
        if (!hasRows()) {
            return new JsArray().getJsObject().cast();
        }
        return mainBody.dom.getChildNodes().cast();
    }

    protected int getScrollAdjust() {
        return adjustForHScroll ? (scroller != null ? (vbar ? scrollOffset : 2) : scrollOffset) : scrollOffset;
    }

    protected SortInfo getSortState() {
        return ds.getSortState();
    }

    protected int getTotalWidth() {
        return cm.getTotalWidth();
    }

    protected Element getWidgetCell(int row, int cell) {
        Element cellEl = getCell(row, cell);
        if (cellEl != null) {
            return cellEl.getFirstChildElement();
        }
        return null;
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected void handleComponentEvent(GridEvent ge) {
        switch (ge.getEventTypeInt()) {
        case Event.ONMOUSEMOVE:
            Element row = getRow(ge.getRowIndex());
            if (overRow != null && row == null) {
                onRowOut(overRow);
            } else if (row != null && overRow != row) {
                if (overRow != null) {
                    onRowOut(overRow);
                }
                onRowOver(row);
            }
            break;

        case Event.ONMOUSEOVER:
            EventTarget from = ge.getEvent().getRelatedEventTarget();
            if (from == null || (Element.is(from) && !DOM.isOrHasChild(grid.getElement(),
                    (com.google.gwt.user.client.Element) Element.as(from)))) {
                Element r = getRow(ge.getRowIndex());
                if (r != null) {
                    onRowOver(r);
                }
            }
            break;
        case Event.ONMOUSEOUT:
            EventTarget to = ge.getEvent().getRelatedEventTarget();
            if (to == null || (Element.is(to)
                    && !DOM.isOrHasChild(grid.getElement(), (com.google.gwt.user.client.Element) Element.as(to)))) {
                if (overRow != null) {
                    onRowOut(overRow);
                }
            }
            break;
        case Event.ONMOUSEDOWN:
            onMouseDown(ge);
            break;
        case Event.ONSCROLL:
            if (scroller.isOrHasChild(ge.getTarget())) {
                syncScroll();
            }
            break;
        }
    }

    protected boolean hasRows() {
        if (mainBody == null) {
            return false;
        }
        Element e = mainBody.dom.getFirstChildElement();
        return e != null && !"x-grid-empty".equals(e.getClassName());
    }

    /**
     * Initializes the view.
     * 
     * @param grid the grid
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected void init(final Grid grid) {
        this.grid = grid;
        this.cm = grid.getColumnModel();
        selectable = !grid.isDisableTextSelection();

        initListeners();

        initTemplates();
        initData(grid.getStore(), cm);
        initUI(grid);

        newColumnHeader();

        if (cm.getAggregationRows().size() > 0) {
            footer = new ColumnFooter(grid, cm);
        }
    }

    /**
     * Initializes the data.
     * 
     * @param ds the data store
     * @param cm the column model
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected void initData(ListStore ds, ColumnModel cm) {
        if (this.ds != null) {
            this.ds.removeStoreListener(listener);
        }
        if (ds != null) {
            ds.addStoreListener(listener);
        }
        this.ds = ds;

        if (this.cm != null) {
            this.cm.removeListener(Events.HiddenChange, columnListener);
            this.cm.removeListener(Events.HeaderChange, columnListener);
            this.cm.removeListener(Events.WidthChange, columnListener);
            this.cm.removeListener(Events.ColumnMove, columnListener);
        }
        if (cm != null) {
            cm.addListener(Events.HiddenChange, columnListener);
            cm.addListener(Events.HeaderChange, columnListener);
            cm.addListener(Events.WidthChange, columnListener);
            cm.addListener(Events.ColumnMove, columnListener);
        }
        this.cm = cm;
    }

    protected void initElements() {
        NodeList<Node> cs = grid.getElement().getFirstChild().getChildNodes();

        el = grid.el().firstChild();

        mainWrap = new El((com.google.gwt.user.client.Element) cs.getItem(0));
        mainHd = mainWrap.firstChild();

        if (grid.isHideHeaders()) {
            mainHd.setVisible(false);
        }

        innerHd = mainHd.firstChild();

        scroller = mainWrap.getChild(1);
        scroller.addEventsSunk(Event.ONSCROLL);

        if (forceFit) {
            scroller.setStyleAttribute("overflowX", "hidden");
        }

        mainBody = scroller.firstChild();
        focusEl = scroller.getChild(1);
        grid.swallowEvent(Events.OnClick, focusEl.dom, true);
    }

    protected void initListeners() {
        listener = new StoreListener<ModelData>() {

            @Override
            public void storeAdd(StoreEvent<ModelData> se) {
                onAdd(ds, se.getModels(), se.getIndex());
            }

            @Override
            public void storeBeforeDataChanged(StoreEvent<ModelData> se) {
                onBeforeDataChanged(se);
            }

            @Override
            public void storeClear(StoreEvent<ModelData> se) {
                onClear(se);
            }

            @Override
            public void storeDataChanged(StoreEvent<ModelData> se) {
                onDataChanged(se);
            }

            @Override
            public void storeFilter(StoreEvent<ModelData> se) {
                onDataChanged(se);
            }

            @Override
            public void storeRemove(StoreEvent<ModelData> se) {
                onRemove(ds, se.getModel(), se.getIndex(), false);
            }

            @Override
            public void storeUpdate(StoreEvent<ModelData> se) {
                onUpdate(ds, se.getModel());
            }

        };

        columnListener = new Listener<ColumnModelEvent>() {
            public void handleEvent(ColumnModelEvent e) {
                if (grid.isViewReady()) {
                    EventType type = e.getType();
                    if (type == Events.HiddenChange) {
                        onHiddenChange(cm, e.getColIndex(), e.isHidden());
                    } else if (type == Events.HeaderChange) {
                        onHeaderChange(e.getColIndex(), e.getHeader());
                    } else if (type == Events.WidthChange) {
                        onColumnWidthChange(e.getColIndex(), e.getWidth());
                    } else if (type == Events.ColumnMove) {
                        onColumnMove(e.getColIndex());
                    }
                }
            }
        };
    }

    protected void initTemplates() {
        templates = GWT.create(GridTemplates.class);
    }

    protected void initUI(final Grid<ModelData> grid) {

    }

    protected void insertRows(ListStore<ModelData> store, int firstRow, int lastRow, boolean isUpdate) {
        Element e = mainBody.dom.getFirstChildElement();
        if (e != null && !hasRows()) {
            mainBody.dom.setInnerHTML("");
        }

        SafeHtml html = renderRows(firstRow, lastRow);
        Element before = getRow(firstRow);

        if (before != null) {
            DomHelper.insertBefore((com.google.gwt.user.client.Element) before, html.asString());
        } else {
            DomHelper.insertHtml("beforeEnd", mainBody.dom, html.asString());
        }

        if (!isUpdate) {
            processRows(firstRow, false);
        }
    }

    protected void layout(boolean skipResize) {
        if (mainBody == null) {
            return;
        }

        El c = grid.el();
        Size csize = c.getStyleSize();

        int vw = csize.width;
        int vh = 0;
        if (vw < 10 || csize.height < 20) {
            return;
        }

        if (!skipResize) {
            resize();
        }

        if (forceFit || autoFill) {
            if (lastViewWidth != vw) {
                fitColumns(false, false, -1);
                header.updateTotalWidth(getOffsetWidth(), getTotalWidth());
                if (footer != null) {
                    footer.updateTotalWidth(getOffsetWidth(), getTotalWidth());
                }
                lastViewWidth = vw;
            }
        } else {
            autoExpand(false);
            header.updateTotalWidth(getOffsetWidth(), getTotalWidth());
            if (footer != null) {
                footer.updateTotalWidth(getOffsetWidth(), getTotalWidth());
            }
            syncHeaderScroll();
        }

        templateOnLayout(vw, vh);
    }

    protected ColumnHeader newColumnHeader() {
        header = new ColumnHeader(grid, cm) {
            @SuppressWarnings("unchecked")
            @Override
            protected ComponentEvent createColumnEvent(ColumnHeader header, int column, Menu menu) {
                GridEvent<ModelData> event = (GridEvent<ModelData>) GridView.this.createComponentEvent(null);
                event.setColIndex(column);
                event.setMenu(menu);
                return event;
            }

            @Override
            protected Menu getContextMenu(int column) {
                return createContextMenu(column);
            }

            @Override
            protected void onColumnSplitterMoved(int colIndex, int width) {
                super.onColumnSplitterMoved(colIndex, width);
                GridView.this.onColumnSplitterMoved(colIndex, width);
            }

            @Override
            protected void onHeaderClick(ComponentEvent ce, int column) {
                super.onHeaderClick(ce, column);
                GridView.this.onHeaderClick(grid, column);
            }

            @Override
            protected void onKeyDown(ComponentEvent ce, int index) {
                ce.cancelBubble();
                if (grid.getSelectionModel() instanceof CellSelectionModel<?>) {
                    CellSelectionModel<?> csm = (CellSelectionModel<?>) grid.getSelectionModel();
                    csm.selectCell(0, index);
                } else {
                    grid.getSelectionModel().select(0, false);
                }
            }

        };
        header.setSplitterWidth(splitterWidth);
        header.setMinColumnWidth(grid.getMinColumnWidth());

        return header;
    }

    protected void notifyHide() {
    }

    protected void notifyShow() {
    }

    protected void onAdd(ListStore<ModelData> store, List<ModelData> models, int index) {
        if (grid != null && grid.isViewReady()) {
            insertRows(store, index, index + (models.size() - 1), false);
            renderWidgets(index, index + (models.size() - 1));
            addTask.delay(10);
        }
    }

    protected void onBeforeDataChanged(StoreEvent<ModelData> se) {
        if (grid != null && grid.isLoadMask()) {
            grid.mask(GXT.MESSAGES.loadMask_msg());
        }
    }

    protected void onCellDeselect(int row, int col) {
        Element cell = getCell(row, col);
        if (cell != null) {
            fly(cell).removeStyleName("x-grid3-cell-selected");
            if (GXT.isAriaEnabled()) {
                cell.setAttribute("aria-selected", "false");
            }
        }
    }

    protected void onCellSelect(int row, int col) {
        Element cell = getCell(row, col);
        if (cell != null) {
            fly(cell).addStyleName("x-grid3-cell-selected");
            if (GXT.isAriaEnabled()) {
                cell.setAttribute("aria-selected", "true");
                grid.setAriaState("aria-activedescendant", cell.getId());
            }
        }
    }

    protected void onClear(StoreEvent<ModelData> se) {
        refresh(false);
    }

    protected void onClick(GridEvent<ModelData> ce) {
        Element row = findRow(ce.getTarget());
        if (row != null) {
            ce.setRowIndex(findRowIndex(row));
            grid.fireEvent(Events.RowClick, ce);
        }
    }

    protected void onColumnMove(int newIndex) {
        boolean pScroll = preventScrollToTopOnRefresh;
        preventScrollToTopOnRefresh = true;
        refresh(true);
        preventScrollToTopOnRefresh = pScroll;
        templateAfterMove(newIndex);
    }

    @SuppressWarnings("unchecked")
    protected void onColumnSplitterMoved(int colIndex, int width) {
        stopEditing();
        userResized = true;
        width = Math.max(grid.getMinColumnWidth(), width);
        cm.setColumnWidth(colIndex, width);

        GridEvent<ModelData> e = (GridEvent<ModelData>) createComponentEvent(null);
        e.setColIndex(colIndex);
        e.setWidth(width);
        grid.fireEvent(Events.ColumnResize, e);
    }

    protected void onColumnWidthChange(int column, int width) {
        if (forceFit) {
            fitColumns(false, false, column);
            header.updateTotalWidth(getOffsetWidth(), getTotalWidth());
        } else {
            updateColumnWidth(column, width);
            header.updateTotalWidth(getOffsetWidth(), getTotalWidth());
            if (GXT.isIE) {
                syncHeaderScroll();
            }
        }

        if (grid.isStateful()) {
            Map<String, Object> state = grid.getState();
            state.put("width" + cm.getColumnId(column), width);
            grid.saveState();
        }
    }

    protected void onDataChanged(StoreEvent<ModelData> se) {
        refresh(false);
        if (grid != null && grid.isLoadMask()) {
            if (grid.isEnabled()) {
                grid.unmask();
            } else {
                grid.mask();
            }
        }

        if (grid != null && grid.isStateful() && ds != null && ds.getLoadConfig() != null
                && ds.getLoadConfig() instanceof PagingLoadConfig) {
            PagingLoadConfig config = (PagingLoadConfig) ds.getLoadConfig();
            Map<String, Object> state = grid.getState();
            state.put("offset", config.getOffset());
            state.put("limit", config.getLimit());
            grid.saveState();
        }
    }

    protected void onHeaderChange(int column, SafeHtml text) {
        header.setHeaderHtml(column, text);
    }

    protected void onHeaderClick(Grid<ModelData> grid, int column) {
        this.headerColumnIndex = column;
        if (!headerDisabled && cm.isSortable(column)) {
            doSort(column, null);
        }
    }

    protected void onHiddenChange(ColumnModel cm, int col, boolean hidden) {
        updateColumnHidden(col, hidden);

        if (grid.isStateful()) {
            Map<String, Object> state = grid.getState();
            state.put("hidden" + cm.getColumnId(col), hidden);
            grid.saveState();
        }
    }

    protected void onHighlightRow(int rowIndex, boolean highlight) {
        Element row = getRow(rowIndex);
        if (row != null) {
            if (highlight) {
                addRowStyle(row, "x-grid3-highlightrow");
                if (GXT.isAriaEnabled()) {
                    grid.setAriaState("aria-activedescendant", row.getId());
                }
            } else {
                removeRowStyle(row, "x-grid3-highlightrow");
            }
        }
    }

    protected void onMouseDown(GridEvent<ModelData> ge) {

    }

    protected void onRemove(ListStore<ModelData> ds, ModelData m, int index, boolean isUpdate) {
        if (grid != null && grid.isViewReady()) {
            detachWidget(index, true);
            removeRow(index);
            if (!isUpdate) {
                removeTask.delay(10);
            } else {
                removeTask.delay(0);
            }
            constrainFocusElement();
        }
    }

    protected void onRowDeselect(int rowIndex) {
        Element row = getRow(rowIndex);
        if (row != null) {
            removeRowStyle(row, "x-grid3-row-selected");
            removeRowStyle(row, "x-grid3-highlightrow");
            if (GXT.isAriaEnabled()) {
                row.setAttribute("aria-selected", "false");
            }
        }
    }

    protected void onRowOut(Element row) {
        if (grid.isTrackMouseOver()) {
            removeRowStyle(row, "x-grid3-row-over");
            overRow = null;
        }
    }

    protected void onRowOver(Element row) {
        if (grid.isTrackMouseOver()) {
            addRowStyle(row, "x-grid3-row-over");
            overRow = row;
        }
    }

    protected void onRowSelect(int rowIndex) {
        Element row = getRow(rowIndex);
        if (row != null) {
            onRowOut(row);
            addRowStyle(row, "x-grid3-row-selected");
            if (GXT.isAriaEnabled()) {
                row.setAttribute("aria-selected", "true");
                grid.getElement().setAttribute("aria-activedescendant", row.getId());
            }
        }
    }

    protected void onUpdate(ListStore<ModelData> store, ModelData model) {
        refreshRow(store.indexOf(model));
    }

    protected ModelData prepareData(ModelData model) {
        if (grid.getModelProcessor() != null) {
            boolean silent = false;
            if (model instanceof BaseModel) {
                silent = ((BaseModel) model).isSilent();
                ((BaseModel) model).setSilent(true);
            }

            ModelData m = grid.getModelProcessor().prepareData(model);

            if (model instanceof BaseModel) {
                ((BaseModel) model).setSilent(silent);
            }

            return m;
        }
        return model;
    }

    protected void processRows(int startRow, boolean skipStripe) {
        if (ds.getCount() < 1) {
            return;
        }
        skipStripe = skipStripe || !grid.isStripeRows();
        NodeList<Element> rows = getRows();
        String cls = "x-grid3-row-alt";
        for (int i = 0, len = rows.getLength(); i < len; i++) {
            Element row = rows.getItem(i);
            row.setPropertyInt("rowIndex", i);
            if (!skipStripe) {
                boolean isAlt = (i + 1) % 2 == 0;
                boolean hasAlt = row.getClassName() != null && row.getClassName().indexOf(cls) != -1;
                if (isAlt == hasAlt) {
                    continue;
                }
                if (isAlt) {
                    El.fly(row).addStyleName(cls);
                } else {
                    El.fly(row).removeStyleName(cls);
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    protected void refreshRow(int row) {
        if (grid != null && grid.isViewReady()) {
            ModelData m = ds.getAt(row);
            if (m != null) {
                // do not change focus on refresh
                // handles situation with changing cell value with field binding
                focusEnabled = false;

                insertRows(ds, row, row, true);
                getRow(row).setPropertyInt("rowIndex", row);
                onRemove(ds, m, row + 1, true);

                renderWidgets(row, row);

                GridEvent<ModelData> e = (GridEvent<ModelData>) createComponentEvent(null);
                e.setRowIndex(row);
                e.setModel(ds.getAt(row));
                fireEvent(Events.RowUpdated, e);
                focusEnabled = true;
            }
        }
    }

    protected void removeRow(int row) {
        Element r = getRow(row);
        if (r != null) {
            fly(r).removeFromParent();
        }
    }

    protected void removeRowStyle(Element row, String style) {
        fly(row).removeStyleName(style);
    }

    protected void render() {
        renderUI();
        grid.sinkEvents(Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS | Event.FOCUSEVENTS);
    }

    protected void renderFooter() {
        if (!footer.isRendered()) {
            footer.disableTextSelection(true);
            footer.render(mainWrap.dom);
        } else {
            mainWrap.appendChild(footer.getElement());
        }
    }

    protected void renderHeader() {
        El head = grid.el().selectNode(".x-grid3-hh");
        head.removeChildren();
        if (!header.isRendered()) {
            header.render(head.dom);
        } else {
            head.appendChild(header.getElement());
        }
    }

    protected SafeHtml renderRows(int startRow, int endRow) {
        int colCount = cm.getColumnCount();

        if (ds.getCount() < 1) {
            return SafeHtmlUtils.EMPTY_SAFE_HTML;
        }

        List<ColumnData> cs = getColumnData();

        if (endRow == -1) {
            endRow = ds.getCount() - 1;
        }

        List<ModelData> rs = ds.getRange(startRow, endRow);
        return SafeHtmlUtils.fromTrustedString(doRender(cs, rs, startRow, colCount, grid.isStripeRows()));
    }

    protected void renderUI() {
        String h = "<div class='x-grid3-hh' role='row'></div>";
        String body = templates.body("");

        String html = templates.master(body, h);

        grid.getElement().setInnerHTML(html);

        renderHeader();

        initElements();

        header.setEnableColumnResizing(grid.isColumnResize());
        header.setEnableColumnReorder(grid.isColumnReordering());

        if (footer != null) {
            renderFooter();
        }

        updateHeaderSortState();
    }

    protected void renderWidgets(int startRow, int endRow) {
        if (grid.isViewReady()) {
            if (endRow == -1) {
                endRow = widgetList.size() - 1;
            }
            for (int i = startRow; i <= endRow; i++) {
                List<Widget> m = i < widgetList.size() ? widgetList.get(i) : null;
                if (m != null) {
                    for (int j = 0; j < grid.getColumnModel().getColumnCount(); j++) {
                        Widget w = j < m.size() ? m.get(j) : null;
                        if (w != null) {
                            Element cell = getWidgetCell(i, j);
                            if (cell != null) {
                                if (w.getElement().getParentElement() != cell) {
                                    fly(cell).removeChildren();
                                    cell.appendChild(w.getElement());
                                }
                                if (grid.isAttached()) {
                                    ComponentHelper.doAttach(w);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    protected void resize() {
        if (mainBody == null) {
            return;
        }

        El c = grid.el();
        Size csize = c.getStyleSize();

        int vw = csize.width;
        int vh = 0;
        if (vw < 10 || csize.height < 20) {
            return;
        }

        if (grid.isAutoHeight()) {
            el.setWidth(csize.width);
            scroller.setWidth(vw);
        } else {
            el.setSize(csize.width, csize.height);
        }

        int hdHeight = innerHd.getHeight();
        vh = csize.height - hdHeight;

        if (footer != null) {
            vh -= footer.getHeight();
        }

        if (!grid.isAutoHeight()) {
            scroller.setSize(vw, vh);
        }

        if (innerHd != null) {
            innerHd.setWidth(vw);
        }
        if (footer != null) {
            footer.setWidth(vw);
        }

    }

    protected void restoreScroll(Point state) {
        if (state.y < scroller.getWidth()) {
            scroller.setScrollLeft(state.x);
        }
        if (state.x < scroller.getHeight()) {
            scroller.setScrollTop(state.y);
        }
    }

    protected void stopEditing() {
        if (grid.editSupport != null) {
            grid.editSupport.stopEditing(true);
        }
    }

    protected void syncHeaderScroll() {
        int sl = scroller.getScrollLeft();
        innerHd.setScrollLeft(sl);
        // second time for IE (1/2 time first fails, other browsers ignore)
        innerHd.setScrollLeft(sl);

        if (footer != null) {
            footer.el().setScrollLeft(sl);
            footer.el().setScrollLeft(sl);
        }
    }

    protected void syncHScroll() {
        if (!hasRows()) {
            El child = mainBody.firstChild();

            if (child != null) {
                child.setWidth(cm.getTotalWidth(), true);
            }
        }
    }

    @SuppressWarnings("unchecked")
    protected void syncScroll() {
        syncHeaderScroll();
        int scrollLeft = scroller.getScrollLeft();
        int scrollTop = scroller.getScrollTop();
        GridEvent<ModelData> ge = (GridEvent<ModelData>) createComponentEvent(null);
        ge.setScrollLeft(scrollLeft);
        ge.setScrollTop(scrollTop);
        constrainFocusElement();
        grid.fireEvent(Events.BodyScroll, ge);
    }

    protected void templateAfterMove(int index) {
        // template method
    }

    protected void templateOnAllColumnWidthsUpdated(List<Integer> ws, int tw) {
        // template method
    }

    protected void templateOnColumnHiddenUpdated(int col, boolean hidden, int tw) {
        // template method
    }

    protected void templateOnColumnWidthUpdated(int col, int w, int tw) {
        // template method
    }

    protected void templateOnLayout(int vw, int vh) {
        // template method
    }

    protected void templateUpdateColumnText(int col, String text) {
        // template method
    }

    protected void updateAllColumnWidths() {
        int tw = getTotalWidth();
        int clen = cm.getColumnCount();
        Stack<Integer> ws = new Stack<Integer>();

        header.updateAllColumnWidths();

        for (int i = 0; i < clen; i++) {
            ws.push(getColumnWidth(i));
            if (footer != null) {
                footer.updateColumnWidth(i, cm.getColumnWidth(i));
            }
        }

        NodeList<Element> ns = getRows();
        for (int i = 0, len = ns.getLength(); i < len; i++) {
            Element row = ns.getItem(i);
            row.getStyle().setPropertyPx("width", tw);
            if (row.getFirstChild() != null) {
                row.getFirstChildElement().getStyle().setPropertyPx("width", tw);
                TableSectionElement e = row.getFirstChild().cast();
                TableRowElement nodeList = e.getRows().getItem(0);
                for (int j = 0; j < clen; j++) {
                    ((Element) nodeList.getChildNodes().getItem(j)).getStyle().setPropertyPx("width", ws.get(j));
                }
            }
        }

        templateOnAllColumnWidthsUpdated(ws, tw);
        syncHScroll();
    }

    protected void updateColumnHidden(int index, boolean hidden) {
        int tw = getTotalWidth();
        String display = hidden ? "none" : "";

        El.fly(innerHd.dom.getFirstChildElement()).setWidth(getOffsetWidth());
        El.fly(innerHd.dom.getFirstChildElement().getFirstChildElement()).setWidth(tw);

        header.updateColumnHidden(index, hidden);
        if (footer != null) {
            footer.updateTotalWidth(getOffsetWidth(), tw);
            footer.updateColumnHidden(index, hidden);
        }

        NodeList<Element> ns = getRows();
        for (int i = 0, len = ns.getLength(); i < len; i++) {
            Element elem = ns.getItem(i);
            elem.getStyle().setProperty("width", tw + "px");
            TableSectionElement e = (TableSectionElement) elem.getFirstChild();
            if (e != null) {
                e.getStyle().setProperty("width", tw + "px");
                Element cell = e.getRows().getItem(0).getChildNodes().getItem(index).cast();
                cell.getStyle().setProperty("display", display);
            }
        }

        templateOnColumnHiddenUpdated(index, hidden, tw);

        lastViewWidth = -1;
        layout();
        syncHScroll();
    }

    protected void updateColumnWidth(int col, int width) {
        int tw = getTotalWidth();
        int w = getColumnWidth(col);

        header.updateTotalWidth(-1, tw);
        header.updateColumnWidth(col, width);

        if (footer != null) {
            footer.updateTotalWidth(getOffsetWidth(), tw);
            footer.updateColumnWidth(col, width);
        }

        NodeList<Element> ns = getRows();
        for (int i = 0, len = ns.getLength(); i < len; i++) {
            Element row = ns.getItem(i);
            row.getStyle().setPropertyPx("width", tw);
            if (row.getFirstChild() != null) {
                row.getFirstChildElement().getStyle().setPropertyPx("width", tw);
                TableSectionElement e = row.getFirstChild().cast();
                ((Element) e.getRows().getItem(0).getChildNodes().getItem(col)).getStyle().setPropertyPx("width",
                        w);
            }
        }

        templateOnColumnWidthUpdated(col, w, tw);
        syncHScroll();
    }

    @SuppressWarnings("unchecked")
    protected void updateHeaderSortState() {
        SortInfo state = getSortState();
        if (state == null || Util.isEmptyString(state.getSortField())) {
            return;
        }
        if (sortState == null || (!sortState.getSortField().equals(state.getSortField()))
                || sortState.getSortDir() != state.getSortDir()) {

            GridEvent<ModelData> e = (GridEvent<ModelData>) createComponentEvent(null);
            e.setSortInfo(state);

            sortState = new SortInfo(state.getSortField(), state.getSortDir());
            int sortColumn = cm.findColumnIndex(state.getSortField());
            if (sortColumn != -1) {
                updateSortIcon(sortColumn, sortState.getSortDir());
            }
            if (grid.isStateful()) {
                Map<String, Object> st = grid.getState();
                st.put("sortField", sortState.getSortField());
                st.put("sortDir", sortState.getSortDir().toString());
                grid.saveState();
            }
            grid.fireEvent(Events.SortChange, e);
        }
    }

    protected void updateSortIcon(int colIndex, SortDir dir) {
        header.updateSortIcon(colIndex, dir);
    }

    private native String getCellIndexId(Element elem) /*-{
                                                       if (!@com.extjs.gxt.ui.client.widget.grid.GridView::colRe) {
                                                       @com.extjs.gxt.ui.client.widget.grid.GridView::colRe = new RegExp(
                                                       "x-grid3-td-([^\\s]+)");
                                                       }
                                                       if (elem) {
                                                       var m = elem.className
                                                       .match(@com.extjs.gxt.ui.client.widget.grid.GridView::colRe);
                                                       if (m && m[1]) {
                                                       return m[1];
                                                       }
                                                       }
                                                       return null;
                                                       }-*/;

    private void refreshFooterData() {
        if (footer != null) {
            footer.refresh();
        }
    }

    private void restrictMenu(Menu columns) {
        int count = 0;
        for (int i = 0, len = cm.getColumnCount(); i < len; i++) {
            if (!shouldNotCount(i, true)) {
                count++;
            }
        }

        if (count == 1) {
            for (Component item : columns.getItems()) {
                CheckMenuItem ci = (CheckMenuItem) item;
                if (ci.isChecked()) {
                    ci.disable();
                }
            }
        } else {
            for (Component item : columns.getItems()) {
                item.enable();
            }
        }
    }

    private boolean shouldNotCount(int columnIndex, boolean includeHidden) {
        return cm.getColumnHeader(columnIndex) == null || cm.getColumnHeader(columnIndex).equals("")
                || (includeHidden && cm.isHidden(columnIndex)) || cm.isFixed(columnIndex);
    }

    private void constrainFocusElement() {
        int scrollLeft = scroller.dom.getScrollLeft();
        int scrollTop = scroller.dom.getScrollTop();
        int left = scroller.getWidth(true) / 2 + scrollLeft;
        int top = scroller.getHeight(true) / 2 + scrollTop;
        focusEl.setLeftTop(left, top);
    }

}