com.google.gwt.user.client.ui.HTMLTable.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.user.client.ui.HTMLTable.java

Source

/*
 * Copyright 2007 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.user.client.ui;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.shared.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.ElementRemote;
import com.google.gwt.dom.client.LocalDom;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.dom.client.TableCellElement;
import com.google.gwt.dom.client.TableRowElement;
import com.google.gwt.dom.client.TableSectionElement;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.DoubleClickEvent;
import com.google.gwt.event.dom.client.DoubleClickHandler;
import com.google.gwt.event.dom.client.DragEndEvent;
import com.google.gwt.event.dom.client.DragEndHandler;
import com.google.gwt.event.dom.client.DragEnterEvent;
import com.google.gwt.event.dom.client.DragEnterHandler;
import com.google.gwt.event.dom.client.DragEvent;
import com.google.gwt.event.dom.client.DragHandler;
import com.google.gwt.event.dom.client.DragLeaveEvent;
import com.google.gwt.event.dom.client.DragLeaveHandler;
import com.google.gwt.event.dom.client.DragOverEvent;
import com.google.gwt.event.dom.client.DragOverHandler;
import com.google.gwt.event.dom.client.DragStartEvent;
import com.google.gwt.event.dom.client.DragStartHandler;
import com.google.gwt.event.dom.client.DropEvent;
import com.google.gwt.event.dom.client.DropHandler;
import com.google.gwt.event.dom.client.HasAllDragAndDropHandlers;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.dom.client.HasDoubleClickHandlers;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.annotations.IsSafeHtml;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.impl.ElementMapperImpl;
import com.google.gwt.user.client.ui.HasHorizontalAlignment.HorizontalAlignmentConstant;
import com.google.gwt.user.client.ui.HasVerticalAlignment.VerticalAlignmentConstant;

/**
 * HTMLTable contains the common table algorithms for
 * {@link com.google.gwt.user.client.ui.Grid} and
 * {@link com.google.gwt.user.client.ui.FlexTable}.
 * <p>
 * <img class='gallery' src='doc-files/Table.png'/>
 * </p>
 */
@SuppressWarnings("deprecation")
public abstract class HTMLTable extends Panel
        implements SourcesTableEvents, HasAllDragAndDropHandlers, HasClickHandlers, HasDoubleClickHandlers {
    private static final HTMLTableImpl impl = GWT.create(HTMLTableImpl.class);

    /**
     * Table's body.
     */
    private final Element bodyElem;

    /**
     * Current cell formatter.
     */
    private CellFormatter cellFormatter;

    /**
     * Column Formatter.
     */
    private ColumnFormatter columnFormatter;

    /**
     * Current row formatter.
     */
    private RowFormatter rowFormatter;

    /**
     * Table element.
     */
    private final Element tableElem;

    private ElementMapperImpl<Widget> widgetMap = new ElementMapperImpl<Widget>();

    /**
     * Create a new empty HTML Table.
     */
    public HTMLTable() {
        tableElem = DOM.createTable();
        bodyElem = DOM.createTBody();
        DOM.appendChild(tableElem, bodyElem);
        setElement(tableElem);
    }

    public HandlerRegistration addClickHandler(ClickHandler handler) {
        return addDomHandler(handler, ClickEvent.getType());
    }

    public HandlerRegistration addDoubleClickHandler(DoubleClickHandler handler) {
        return addDomHandler(handler, DoubleClickEvent.getType());
    }

    public HandlerRegistration addDragEndHandler(DragEndHandler handler) {
        return addBitlessDomHandler(handler, DragEndEvent.getType());
    }

    public HandlerRegistration addDragEnterHandler(DragEnterHandler handler) {
        return addBitlessDomHandler(handler, DragEnterEvent.getType());
    }

    public HandlerRegistration addDragHandler(DragHandler handler) {
        return addBitlessDomHandler(handler, DragEvent.getType());
    }

    public HandlerRegistration addDragLeaveHandler(DragLeaveHandler handler) {
        return addBitlessDomHandler(handler, DragLeaveEvent.getType());
    }

    public HandlerRegistration addDragOverHandler(DragOverHandler handler) {
        return addBitlessDomHandler(handler, DragOverEvent.getType());
    }

    public HandlerRegistration addDragStartHandler(DragStartHandler handler) {
        return addBitlessDomHandler(handler, DragStartEvent.getType());
    }

    public HandlerRegistration addDropHandler(DropHandler handler) {
        return addBitlessDomHandler(handler, DropEvent.getType());
    }

    /**
     * Adds a listener to the current table.
     * 
     * @param listener
     *            listener to add
     * @deprecated add a click handler instead and use
     *             {@link HTMLTable#getCellForEvent(ClickEvent)} to get the cell
     *             information (remember to check for a null return value)
     */
    @Deprecated
    public void addTableListener(TableListener listener) {
        ListenerWrapper.WrappedTableListener.add(this, listener);
    }

    /**
     * Removes all widgets from this table, but does not remove other HTML or
     * text contents of cells.
     */
    @Override
    public void clear() {
        clear(false);
    }

    /**
     * Removes all widgets from this table, optionally clearing the inner HTML
     * of each cell. Note that this method does not remove any cells or rows.
     * 
     * @param clearInnerHTML
     *            should the cell's inner html be cleared?
     */
    public void clear(boolean clearInnerHTML) {
        for (int row = 0; row < getRowCount(); ++row) {
            for (int col = 0; col < getCellCount(row); ++col) {
                cleanCell(row, col, clearInnerHTML);
            }
        }
    }

    /**
     * Clears the cell at the given row and column. If it contains a Widget, it
     * will be removed from the table. If not, its contents will simply be
     * cleared.
     * 
     * @param row
     *            the widget's row
     * @param column
     *            the widget's column
     * @return true if a widget was removed
     * @throws IndexOutOfBoundsException
     */
    public boolean clearCell(int row, int column) {
        Element td = getCellFormatter().getElement(row, column);
        return internalClearCell(td, true);
    }

    /**
     * Gets the number of cells in a given row.
     * 
     * @param row
     *            the row whose cells are to be counted
     * @return the number of cells present in the row
     */
    public abstract int getCellCount(int row);

    /**
     * Given a click event, return the Cell that was clicked, or null if the
     * event did not hit this table. The cell can also be null if the click
     * event does not occur on a specific cell.
     * 
     * @param event
     *            A click event of indeterminate origin
     * @return The appropriate cell, or null
     */
    public Cell getCellForEvent(ClickEvent event) {
        Element td = getEventTargetCell(Event.as(event.getNativeEvent()));
        if (td == null) {
            return null;
        }
        int row = TableRowElement.as(td.getParentElement()).getSectionRowIndex();
        int column = TableCellElement.as(td).getCellIndex();
        return new Cell(row, column);
    }

    /**
     * Gets the {@link CellFormatter} associated with this table. Use casting to
     * get subclass-specific functionality
     * 
     * @return this table's cell formatter
     */
    public CellFormatter getCellFormatter() {
        return cellFormatter;
    }

    /**
     * Gets the amount of padding that is added around all cells.
     * 
     * @return the cell padding, in pixels
     */
    public int getCellPadding() {
        return tableElem.getPropertyInt("cellPadding");
    }

    /**
     * Gets the amount of spacing that is added around all cells.
     * 
     * @return the cell spacing, in pixels
     */
    public int getCellSpacing() {
        return tableElem.getPropertyInt("cellSpacing");
    }

    /**
     * Gets the column formatter.
     * 
     * @return the column formatter
     */
    public ColumnFormatter getColumnFormatter() {
        return columnFormatter;
    }

    /**
     * Gets the HTML contents of the specified cell.
     * 
     * @param row
     *            the cell's row
     * @param column
     *            the cell's column
     * @return the cell's HTML contents
     * @throws IndexOutOfBoundsException
     */
    public String getHTML(int row, int column) {
        return cellFormatter.getElement(row, column).getInnerHTML();
    }

    /**
     * Gets the number of rows present in this table.
     * 
     * @return the table's row count
     */
    public abstract int getRowCount();

    /**
     * Gets the RowFormatter associated with this table.
     * 
     * @return the table's row formatter
     */
    public RowFormatter getRowFormatter() {
        return rowFormatter;
    }

    /**
     * Gets the text within the specified cell.
     * 
     * @param row
     *            the cell's row
     * @param column
     *            the cell's column
     * @return the cell's text contents
     * @throws IndexOutOfBoundsException
     */
    public String getText(int row, int column) {
        checkCellBounds(row, column);
        Element e = cellFormatter.getElement(row, column);
        return e.getInnerText();
    }

    /**
     * Gets the widget in the specified cell.
     * 
     * @param row
     *            the cell's row
     * @param column
     *            the cell's column
     * @return the widget in the specified cell, or <code>null</code> if none is
     *         present
     * @throws IndexOutOfBoundsException
     */
    public Widget getWidget(int row, int column) {
        checkCellBounds(row, column);
        return getWidgetImpl(row, column);
    }

    /**
     * Determines whether the specified cell exists.
     * 
     * @param row
     *            the cell's row
     * @param column
     *            the cell's column
     * @return <code>true</code> if the specified cell exists
     */
    public boolean isCellPresent(int row, int column) {
        if ((row >= getRowCount()) || (row < 0)) {
            return false;
        }
        if ((column < 0) || (column >= getCellCount(row))) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * Returns an iterator containing all the widgets in this table.
     * 
     * @return the iterator
     */
    public Iterator<Widget> iterator() {
        return new Iterator<Widget>() {
            final ArrayList<Widget> widgetList = widgetMap.getObjectList();

            int lastIndex = -1;

            int nextIndex = -1;
            {
                findNext();
            }

            public boolean hasNext() {
                return nextIndex < widgetList.size();
            }

            public Widget next() {
                if (!hasNext()) {
                    throw new NoSuchElementException();
                }
                Widget result = widgetList.get(nextIndex);
                lastIndex = nextIndex;
                findNext();
                return result;
            }

            public void remove() {
                if (lastIndex < 0) {
                    throw new IllegalStateException();
                }
                Widget w = widgetList.get(lastIndex);
                assert (w.getParent() instanceof HTMLTable);
                w.removeFromParent();
                lastIndex = -1;
            }

            private void findNext() {
                while (++nextIndex < widgetList.size()) {
                    if (widgetList.get(nextIndex) != null) {
                        return;
                    }
                }
            }
        };
    }

    /**
     * Remove the specified widget from the table.
     * 
     * @param widget
     *            widget to remove
     * @return was the widget removed from the table.
     */
    @Override
    public boolean remove(Widget widget) {
        // Validate.
        if (widget.getParent() != this) {
            return false;
        }
        // Orphan.
        try {
            orphan(widget);
        } finally {
            // Physical detach.
            Element elem = widget.getElement();
            DOM.getParent(elem).removeChild(elem);
            // Logical detach.
            widgetMap.removeByElement(elem);
        }
        return true;
    }

    /**
     * Removes the specified table listener.
     * 
     * @param listener
     *            listener to remove
     *
     * @deprecated Use the {@link HandlerRegistration#removeHandler} method on
     *             the object returned by an add*Handler method instead
     */
    @Deprecated
    public void removeTableListener(TableListener listener) {
        ListenerWrapper.WrappedTableListener.remove(this, listener);
    }

    /**
     * Sets the width of the table's border. This border is displayed around all
     * cells in the table.
     * 
     * @param width
     *            the width of the border, in pixels
     */
    public void setBorderWidth(int width) {
        tableElem.setPropertyString("border", "" + width);
    }

    /**
     * Sets the amount of padding to be added around all cells.
     * 
     * @param padding
     *            the cell padding, in pixels
     */
    public void setCellPadding(int padding) {
        tableElem.setPropertyInt("cellPadding", padding);
    }

    /**
     * Sets the amount of spacing to be added around all cells.
     * 
     * @param spacing
     *            the cell spacing, in pixels
     */
    public void setCellSpacing(int spacing) {
        tableElem.setPropertyInt("cellSpacing", spacing);
    }

    /**
     * Sets the HTML contents of the specified cell.
     *
     * @param row
     *            the cell's row
     * @param column
     *            the cell's column
     * @param html
     *            the cell's safe html contents
     * @throws IndexOutOfBoundsException
     */
    public void setHTML(int row, int column, SafeHtml html) {
        setHTML(row, column, html.asString());
    }

    /**
     * Sets the HTML contents of the specified cell.
     *
     * @param row
     *            the cell's row
     * @param column
     *            the cell's column
     * @param html
     *            the cell's HTML contents
     * @throws IndexOutOfBoundsException
     */
    public void setHTML(int row, int column, @IsSafeHtml String html) {
        prepareCell(row, column);
        Element td = cleanCell(row, column, html == null);
        if (html != null) {
            td.setInnerHTML(html);
        }
    }

    /**
     * Sets the text within the specified cell.
     *
     * @param row
     *            the cell's row
     * @param column
     *            cell's column
     * @param text
     *            the cell's text contents
     * @throws IndexOutOfBoundsException
     */
    public void setText(int row, int column, String text) {
        prepareCell(row, column);
        Element td;
        td = cleanCell(row, column, text == null);
        if (text != null) {
            td.setInnerText(text);
        }
    }

    /**
     * Overloaded version for IsWidget.
     * 
     * @see #setWidget(int,int,Widget)
     */
    public void setWidget(int row, int column, IsWidget widget) {
        this.setWidget(row, column, asWidgetOrNull(widget));
    }

    /**
     * Sets the widget within the specified cell.
     * <p>
     * Inherited implementations may either throw IndexOutOfBounds exception if
     * the cell does not exist, or allocate a new cell to store the content.
     * </p>
     * <p>
     * FlexTable will automatically allocate the cell at the correct location
     * and then set the widget. Grid will set the widget if and only if the cell
     * is within the Grid's bounding box.
     * </p>
     * 
     * @param widget
     *            The widget to be added, or null to clear the cell
     * @param row
     *            the cell's row
     * @param column
     *            the cell's column
     * @throws IndexOutOfBoundsException
     */
    public void setWidget(int row, int column, Widget widget) {
        prepareCell(row, column);
        // Removes any existing widget.
        Element td = cleanCell(row, column, true);
        if (widget != null) {
            widget.removeFromParent();
            // Logical attach.
            widgetMap.put(widget);
            // Physical attach.
            DOM.appendChild(td, widget.getElement());
            adopt(widget);
        }
    }

    /**
     * Removes any widgets, text, and HTML within the cell. This method assumes
     * that the requested cell already exists.
     * 
     * @param row
     *            the cell's row
     * @param column
     *            the cell's column
     * @param clearInnerHTML
     *            should the cell's inner html be cleared?
     * @return element that has been cleaned
     */
    private Element cleanCell(int row, int column, boolean clearInnerHTML) {
        // Clear whatever is in the cell.
        Element td = getCellFormatter().getRawElement(row, column);
        internalClearCell(td, clearInnerHTML);
        return td;
    }

    /**
     * Gets the Widget associated with the given cell.
     * 
     * @param row
     *            the cell's row
     * @param column
     *            the cell's column
     * @return the widget
     */
    private Widget getWidgetImpl(int row, int column) {
        Element e = cellFormatter.getRawElement(row, column);
        Element child = DOM.getFirstChild(e);
        if (child == null) {
            return null;
        } else {
            return widgetMap.get(child);
        }
    }

    /**
     * Bounds checks that the cell exists at the specified location.
     * 
     * @param row
     *            cell's row
     * @param column
     *            cell's column
     * @throws IndexOutOfBoundsException
     */
    protected void checkCellBounds(int row, int column) {
        checkRowBounds(row);
        if (column < 0) {
            throw new IndexOutOfBoundsException("Column " + column + " must be non-negative: " + column);
        }
        int cellSize = getCellCount(row);
        if (cellSize <= column) {
            throw new IndexOutOfBoundsException("Column index: " + column + ", Column size: " + getCellCount(row));
        }
    }

    /**
     * Checks that the row is within the correct bounds.
     * 
     * @param row
     *            row index to check
     * @throws IndexOutOfBoundsException
     */
    protected void checkRowBounds(int row) {
        int rowSize = getRowCount();
        if ((row >= rowSize) || (row < 0)) {
            throw new IndexOutOfBoundsException("Row index: " + row + ", Row size: " + rowSize);
        }
    }

    /**
     * Creates a new cell. Override this method if the cell should have initial
     * contents.
     * 
     * @return the newly created TD
     */
    protected Element createCell() {
        return DOM.createTD();
    }

    /**
     * Gets the table's TBODY element.
     * 
     * @return the TBODY element
     */
    protected Element getBodyElement() {
        return DOM.asOld(bodyElem);
    }

    /**
     * Directly ask the underlying DOM what the cell count on the given row is.
     * 
     * @param tableBody
     *            the element
     * @param row
     *            the row
     * @return number of columns in the row
     */
    protected int getDOMCellCount(Element tableBody, int row) {
        Element rowElement = impl.getRows(tableBody).get(row);
        return impl.getCells(rowElement).length();
    }

    /**
     * Directly ask the underlying DOM what the cell count on the given row is.
     * 
     * @param row
     *            the row
     * @return number of columns in the row
     */
    protected int getDOMCellCount(int row) {
        return getDOMCellCount(bodyElem, row);
    }

    /**
     * Directly ask the underlying DOM what the row count is.
     * 
     * @return Returns the number of rows in the table
     */
    protected int getDOMRowCount() {
        return getDOMRowCount(bodyElem);
    }

    @Deprecated
    protected int getDOMRowCount(Element tbody) {
        return impl.getRows(tbody).length();
    }

    /**
     * Determines the TD associated with the specified event.
     * 
     * @param event
     *            the event to be queried
     * @return the TD associated with the event, or <code>null</code> if none is
     *         found.
     */
    protected Element getEventTargetCell(Event event) {
        Element td = DOM.eventGetTarget(event);
        for (; td != null; td = DOM.getParent(td)) {
            // If it's a TD, it might be the one we're looking for.
            if (td.getPropertyString("tagName").equalsIgnoreCase("td")) {
                // Make sure it's directly a part of this table before returning
                // it.
                Element tr = DOM.getParent(td);
                Element body = DOM.getParent(tr);
                if (body == bodyElem) {
                    return DOM.asOld(td);
                }
            }
            // If we run into this table's body, we're out of options.
            if (td == bodyElem) {
                return null;
            }
        }
        return null;
    }

    /**
     * Inserts a new cell into the specified row.
     * 
     * @param row
     *            the row into which the new cell will be inserted
     * @param column
     *            the column before which the cell will be inserted
     * @throws IndexOutOfBoundsException
     */
    protected void insertCell(int row, int column) {
        Element tr = rowFormatter.getRow(bodyElem, row);
        Element td = createCell();
        DOM.insertChild(tr, td, column);
    }

    /**
     * Inserts a number of cells before the specified cell.
     * 
     * @param row
     *            the row into which the new cells will be inserted
     * @param column
     *            the column before which the new cells will be inserted
     * @param count
     *            number of cells to be inserted
     * @throws IndexOutOfBoundsException
     */
    protected void insertCells(int row, int column, int count) {
        Element tr = rowFormatter.getRow(bodyElem, row);
        for (int i = column; i < column + count; i++) {
            Element td = createCell();
            DOM.insertChild(tr, td, i);
        }
    }

    /**
     * Inserts a new row into the table.
     * 
     * @param beforeRow
     *            the index before which the new row will be inserted
     * @return the index of the newly-created row
     * @throws IndexOutOfBoundsException
     */
    protected int insertRow(int beforeRow) {
        // Specifically allow the row count as an insert position.
        if (beforeRow != getRowCount()) {
            checkRowBounds(beforeRow);
        }
        Element tr = DOM.createTR();
        DOM.insertChild(bodyElem, tr, beforeRow);
        return beforeRow;
    }

    /**
     * Does actual clearing, used by clearCell and cleanCell. All HTMLTable
     * methods should use internalClearCell rather than clearCell, as clearCell
     * may be overridden in subclasses to format an empty cell.
     * 
     * @param td
     *            element to clear
     * @param clearInnerHTML
     *            should the cell's inner html be cleared?
     * @return returns whether a widget was cleared
     */
    protected boolean internalClearCell(Element td, boolean clearInnerHTML) {
        Element maybeChild = DOM.getFirstChild(td);
        Widget widget = null;
        if (maybeChild != null) {
            widget = widgetMap.get(maybeChild);
        }
        if (widget != null) {
            // If there is a widget, remove it.
            remove(widget);
            return true;
        } else {
            // Otherwise, simply clear whatever text and/or HTML may be there.
            if (clearInnerHTML) {
                td.setInnerHTML("");
            }
            return false;
        }
    }

    /**
     * <b>Affected Elements:</b>
     * <ul>
     * <li>-(row)#-(cell)# = the cell at the given row and cell index.</li>
     * </ul>
     * 
     * @see UIObject#onEnsureDebugId(String)
     */
    @Override
    protected void onEnsureDebugId(String baseID) {
        super.onEnsureDebugId(baseID);
        int rowCount = getRowCount();
        for (int row = 0; row < rowCount; row++) {
            int cellCount = getCellCount(row);
            for (int cell = 0; cell < cellCount; cell++) {
                Element cellElem = cellFormatter.getRawElement(row, cell);
                ensureDebugId(cellElem, baseID, row + "-" + cell);
            }
        }
    }

    /**
     * Subclasses must implement this method. It allows them to decide what to
     * do just before a cell is accessed. If the cell already exists, this
     * method must do nothing. Otherwise, a subclass must either ensure that the
     * cell exists or throw an {@link IndexOutOfBoundsException}.
     * 
     * @param row
     *            the cell's row
     * @param column
     *            the cell's column
     */
    protected abstract void prepareCell(int row, int column);

    /**
     * Subclasses can implement this method. It allows them to decide what to do
     * just before a column is accessed. For classes, such as
     * <code>FlexTable</code>, that do not have a concept of a global column
     * length can ignore this method.
     * 
     * @param column
     *            the cell's column
     * @throws IndexOutOfBoundsException
     */
    protected void prepareColumn(int column) {
        // Ensure that the indices are not negative.
        if (column < 0) {
            throw new IndexOutOfBoundsException("Cannot access a column with a negative index: " + column);
        }
    }

    /**
     * Subclasses must implement this method. If the row already exists, this
     * method must do nothing. Otherwise, a subclass must either ensure that the
     * row exists or throw an {@link IndexOutOfBoundsException}.
     * 
     * @param row
     *            the cell's row
     */
    protected abstract void prepareRow(int row);

    /**
     * Removes the specified cell from the table.
     * 
     * @param row
     *            the row of the cell to remove
     * @param column
     *            the column of cell to remove
     * @throws IndexOutOfBoundsException
     */
    protected void removeCell(int row, int column) {
        checkCellBounds(row, column);
        Element td = cleanCell(row, column, false);
        Element tr = rowFormatter.getRow(bodyElem, row);
        tr.removeChild(td);
    }

    /**
     * Removes the specified row from the table.
     * 
     * @param row
     *            the index of the row to be removed
     * @throws IndexOutOfBoundsException
     */
    protected void removeRow(int row) {
        int columnCount = getCellCount(row);
        for (int column = 0; column < columnCount; ++column) {
            cleanCell(row, column, false);
        }
        bodyElem.removeChild(rowFormatter.getRow(bodyElem, row));
    }

    /**
     * Sets the table's CellFormatter.
     * 
     * @param cellFormatter
     *            the table's cell formatter
     */
    protected void setCellFormatter(CellFormatter cellFormatter) {
        this.cellFormatter = cellFormatter;
    }

    protected void setColumnFormatter(ColumnFormatter formatter) {
        // Copy the columnGroup element to the new formatter so we don't create
        // a
        // second colgroup element.
        if (columnFormatter != null) {
            formatter.columnGroup = columnFormatter.columnGroup;
        }
        columnFormatter = formatter;
        columnFormatter.prepareColumnGroup();
    }

    /**
     * Sets the table's RowFormatter.
     * 
     * @param rowFormatter
     *            the table's row formatter
     */
    protected void setRowFormatter(RowFormatter rowFormatter) {
        this.rowFormatter = rowFormatter;
    }

    void addCells(Element tbody, int row, int num) {
        com.google.gwt.dom.client.Element rowElem = impl.getRows(tbody).get(row);
        for (int i = 0; i < num; i++) {
            TableCellElement tdElement = Document.get().createTDElement();
            rowElem.appendChild(tdElement);
        }
    }

    /**
     * Return value for {@link HTMLTable#getCellForEvent}.
     */
    public class Cell {
        private final int rowIndex;

        private final int cellIndex;

        /**
         * Creates a cell.
         * 
         * @param rowIndex
         *            the cell's row
         * @param cellIndex
         *            the cell's index
         */
        protected Cell(int rowIndex, int cellIndex) {
            this.cellIndex = cellIndex;
            this.rowIndex = rowIndex;
        }

        /**
         * Gets the cell index.
         * 
         * @return the cell index
         */
        public int getCellIndex() {
            return cellIndex;
        }

        /**
         * Gets the cell's element.
         * 
         * @return the cell's element.
         */
        public Element getElement() {
            return DOM.asOld(getCellFormatter().getElement(rowIndex, cellIndex));
        }

        /**
         * Get row index.
         * 
         * @return the row index
         */
        public int getRowIndex() {
            return rowIndex;
        }
    }

    /**
     * This class contains methods used to format a table's cells.
     */
    public class CellFormatter {
        /**
         * Adds a style to the specified cell.
         * 
         * @param row
         *            the cell's row
         * @param column
         *            the cell's column
         * @param styleName
         *            the style name to be added
         * @see UIObject#addStyleName(String)
         */
        public void addStyleName(int row, int column, String styleName) {
            prepareCell(row, column);
            Element td = getCellElement(bodyElem, row, column);
            UIObject.setStyleName(td, styleName, true);
        }

        /**
         * Gets the TD element representing the specified cell.
         * 
         * @param row
         *            the row of the cell to be retrieved
         * @param column
         *            the column of the cell to be retrieved
         * @return the column's TD element
         * @throws IndexOutOfBoundsException
         */
        public Element getElement(int row, int column) {
            checkCellBounds(row, column);
            return DOM.asOld(getCellElement(bodyElem, row, column));
        }

        /**
         * Gets the style of a specified cell.
         * 
         * @param row
         *            the cell's row
         * @param column
         *            the cell's column
         * @see UIObject#getStyleName()
         * @return returns the style name
         * @throws IndexOutOfBoundsException
         */
        public String getStyleName(int row, int column) {
            return UIObject.getStyleName(getElement(row, column));
        }

        /**
         * Gets the primary style of a specified cell.
         * 
         * @param row
         *            the cell's row
         * @param column
         *            the cell's column
         * @see UIObject#getStylePrimaryName()
         * @return returns the style name
         * @throws IndexOutOfBoundsException
         */
        public String getStylePrimaryName(int row, int column) {
            return UIObject.getStylePrimaryName(getElement(row, column));
        }

        /**
         * Determines whether or not this cell is visible.
         * 
         * @param row
         *            the row of the cell whose visibility is to be set
         * @param column
         *            the column of the cell whose visibility is to be set
         * @return <code>true</code> if the object is visible
         */
        public boolean isVisible(int row, int column) {
            Element e = getElement(row, column);
            return UIObject.isVisible(e);
        }

        /**
         * Removes a style from the specified cell.
         * 
         * @param row
         *            the cell's row
         * @param column
         *            the cell's column
         * @param styleName
         *            the style name to be removed
         * @see UIObject#removeStyleName(String)
         * @throws IndexOutOfBoundsException
         */
        public void removeStyleName(int row, int column, String styleName) {
            checkCellBounds(row, column);
            Element td = getCellElement(bodyElem, row, column);
            UIObject.setStyleName(td, styleName, false);
        }

        /**
         * Sets the horizontal and vertical alignment of the specified cell's
         * contents.
         * 
         * @param row
         *            the row of the cell whose alignment is to be set
         * @param column
         *            the column of the cell whose alignment is to be set
         * @param hAlign
         *            the cell's new horizontal alignment as specified in
         *            {@link HasHorizontalAlignment}
         * @param vAlign
         *            the cell's new vertical alignment as specified in
         *            {@link HasVerticalAlignment}
         * @throws IndexOutOfBoundsException
         */
        public void setAlignment(int row, int column, HorizontalAlignmentConstant hAlign,
                VerticalAlignmentConstant vAlign) {
            setHorizontalAlignment(row, column, hAlign);
            setVerticalAlignment(row, column, vAlign);
        }

        /**
         * Sets the height of the specified cell.
         * 
         * @param row
         *            the row of the cell whose height is to be set
         * @param column
         *            the column of the cell whose height is to be set
         * @param height
         *            the cell's new height, in CSS units
         * @throws IndexOutOfBoundsException
         */
        public void setHeight(int row, int column, String height) {
            prepareCell(row, column);
            Element elem = getCellElement(bodyElem, row, column);
            elem.setPropertyString("height", height);
        }

        /**
         * Sets the horizontal alignment of the specified cell.
         * 
         * @param row
         *            the row of the cell whose alignment is to be set
         * @param column
         *            the column of the cell whose alignment is to be set
         * @param align
         *            the cell's new horizontal alignment as specified in
         *            {@link HasHorizontalAlignment}.
         * @throws IndexOutOfBoundsException
         */
        public void setHorizontalAlignment(int row, int column, HorizontalAlignmentConstant align) {
            prepareCell(row, column);
            Element elem = getCellElement(bodyElem, row, column);
            elem.setPropertyString("align", align.getTextAlignString());
        }

        /**
         * Sets the style name associated with the specified cell.
         *
         * @param row
         *            the row of the cell whose style name is to be set
         * @param column
         *            the column of the cell whose style name is to be set
         * @param styleName
         *            the new style name
         * @see UIObject#setStyleName(String)
         * @throws IndexOutOfBoundsException
         */
        public void setStyleName(int row, int column, String styleName) {
            prepareCell(row, column);
            UIObject.setStyleName(getCellElement(bodyElem, row, column), styleName);
        }

        /**
         * Sets the primary style name associated with the specified cell.
         * 
         * @param row
         *            the row of the cell whose style name is to be set
         * @param column
         *            the column of the cell whose style name is to be set
         * @param styleName
         *            the new style name
         * @see UIObject#setStylePrimaryName(String)
         * @throws IndexOutOfBoundsException
         */
        public void setStylePrimaryName(int row, int column, String styleName) {
            UIObject.setStylePrimaryName(getCellElement(bodyElem, row, column), styleName);
        }

        /**
         * Sets the vertical alignment of the specified cell.
         * 
         * @param row
         *            the row of the cell whose alignment is to be set
         * @param column
         *            the column of the cell whose alignment is to be set
         * @param align
         *            the cell's new vertical alignment as specified in
         *            {@link HasVerticalAlignment}.
         * @throws IndexOutOfBoundsException
         */
        public void setVerticalAlignment(int row, int column, VerticalAlignmentConstant align) {
            prepareCell(row, column);
            getCellElement(bodyElem, row, column).getStyle().setProperty("verticalAlign",
                    align.getVerticalAlignString());
        }

        /**
         * Sets whether this cell is visible via the display style property. The
         * other cells in the row will all shift left to fill the cell's space.
         * So, for example a table with (0,1,2) will become (1,2) if cell 1 is
         * hidden.
         * 
         * @param row
         *            the row of the cell whose visibility is to be set
         * @param column
         *            the column of the cell whose visibility is to be set
         * @param visible
         *            <code>true</code> to show the cell, <code>false</code> to
         *            hide it
         */
        public void setVisible(int row, int column, boolean visible) {
            Element e = ensureElement(row, column);
            UIObject.setVisible(e, visible);
        }

        /**
         * Sets the width of the specified cell.
         * 
         * @param row
         *            the row of the cell whose width is to be set
         * @param column
         *            the column of the cell whose width is to be set
         * @param width
         *            the cell's new width, in CSS units
         * @throws IndexOutOfBoundsException
         */
        public void setWidth(int row, int column, String width) {
            // Give the subclass a chance to prepare the cell.
            prepareCell(row, column);
            getCellElement(bodyElem, row, column).setPropertyString("width", width);
        }

        /**
         * Sets whether the specified cell will allow word wrapping of its
         * contents.
         * 
         * @param row
         *            the row of the cell whose word-wrap is to be set
         * @param column
         *            the column of the cell whose word-wrap is to be set
         * @param wrap
         *            <code>false </code> to disable word wrapping in this cell
         * @throws IndexOutOfBoundsException
         */
        public void setWordWrap(int row, int column, boolean wrap) {
            prepareCell(row, column);
            String wrapValue = wrap ? "" : "nowrap";
            getElement(row, column).getStyle().setProperty("whiteSpace", wrapValue);
        }

        /**
         * Get a cell's element.
         * 
         * @param tbody
         *            the table element
         * @param row
         *            the row of the cell
         * @param col
         *            the column of the cell
         * @return the element
         */
        private Element getCellElement(Element tbody, int row, int col) {
            Element rowObj = impl.getRows(tbody).get(row);
            ElementArray<Element> cells = impl.getCells(rowObj);
            return cells.get(col);
        }

        /**
         * Gets the TD element representing the specified cell unsafely (meaning
         * that it doesn't ensure that <code>row</code> and <code>column</code>
         * are valid).
         * 
         * @param row
         *            the row of the cell to be retrieved
         * @param column
         *            the column of the cell to be retrieved
         * @return the cell's TD element
         */
        private Element getRawElement(int row, int column) {
            return getCellElement(bodyElem, row, column);
        }

        /**
         * Gets the element associated with a cell. If it does not exist and the
         * subtype allows creation of elements, creates it.
         * 
         * @param row
         *            the cell's row
         * @param column
         *            the cell's column
         * @return the cell's element
         * @throws IndexOutOfBoundsException
         */
        protected Element ensureElement(int row, int column) {
            prepareCell(row, column);
            return DOM.asOld(getCellElement(bodyElem, row, column));
        }

        /**
         * Convenience methods to get an attribute on a cell.
         * 
         * @param row
         *            cell's row
         * @param column
         *            cell's column
         * @param attr
         *            attribute to get
         * @return the attribute's value
         * @throws IndexOutOfBoundsException
         */
        protected String getAttr(int row, int column, String attr) {
            Element elem = getElement(row, column);
            return elem.getAttribute(attr);
        }

        /**
         * Convenience methods to set an attribute on a cell.
         * 
         * @param row
         *            cell's row
         * @param column
         *            cell's column
         * @param attrName
         *            attribute to set
         * @param value
         *            value to set
         * @throws IndexOutOfBoundsException
         */
        protected void setAttr(int row, int column, String attrName, String value) {
            Element elem = ensureElement(row, column);
            elem.setAttribute(attrName, value);
        }
    }

    /**
     * This class contains methods used to format a table's columns. It is
     * limited by the support cross-browser HTML support for column formatting.
     */
    public class ColumnFormatter {
        protected Element columnGroup;

        /**
         * Adds a style to the specified column.
         * 
         * @param col
         *            the col to which the style will be added
         * @param styleName
         *            the style name to be added
         * @see UIObject#addStyleName(String)
         * @throws IndexOutOfBoundsException
         */
        public void addStyleName(int col, String styleName) {
            UIObject.setStyleName(ensureColumn(col), styleName, true);
        }

        /**
         * Get the col element for the column.
         * 
         * @param column
         *            the column index
         * @return the col element
         */
        public Element getElement(int column) {
            return DOM.asOld(ensureColumn(column));
        }

        /**
         * Gets the style of the specified column.
         * 
         * @param column
         *            the column to be queried
         * @return the style name
         * @see UIObject#getStyleName()
         * @throws IndexOutOfBoundsException
         */
        public String getStyleName(int column) {
            return UIObject.getStyleName(ensureColumn(column));
        }

        /**
         * Gets the primary style of the specified column.
         * 
         * @param column
         *            the column to be queried
         * @return the style name
         * @see UIObject#getStylePrimaryName()
         * @throws IndexOutOfBoundsException
         */
        public String getStylePrimaryName(int column) {
            return UIObject.getStylePrimaryName(ensureColumn(column));
        }

        /**
         * Removes a style from the specified column.
         * 
         * @param column
         *            the column from which the style will be removed
         * @param styleName
         *            the style name to be removed
         * @see UIObject#removeStyleName(String)
         * @throws IndexOutOfBoundsException
         */
        public void removeStyleName(int column, String styleName) {
            UIObject.setStyleName(ensureColumn(column), styleName, false);
        }

        /**
         * Sets the style name associated with the specified column.
         * 
         * @param column
         *            the column whose style name is to be set
         * @param styleName
         *            the new style name
         * @see UIObject#setStyleName(String)
         * @throws IndexOutOfBoundsException
         */
        public void setStyleName(int column, String styleName) {
            UIObject.setStyleName(ensureColumn(column), styleName);
        }

        /**
         * Sets the primary style name associated with the specified column.
         * 
         * @param column
         *            the column whose style name is to be set
         * @param styleName
         *            the new style name
         * @see UIObject#setStylePrimaryName(String)
         * @throws IndexOutOfBoundsException
         */
        public void setStylePrimaryName(int column, String styleName) {
            UIObject.setStylePrimaryName(ensureColumn(column), styleName);
        }

        /**
         * Sets whether this row is visible.
         * 
         * @param row
         *            the row whose visibility is to be set
         * @param visible
         *            <code>true</code> to show the row, <code>false</code> to
         *            hide it
         */
        public void setVisible(int column, boolean visible) {
            Element e = ensureColumn(column);
            UIObject.setVisible(e, visible);
        }

        /**
         * Sets the width of the specified column.
         * 
         * @param column
         *            the column of the cell whose width is to be set
         * @param width
         *            the cell's new width, in percentage or pixel units
         * @throws IndexOutOfBoundsException
         */
        public void setWidth(int column, String width) {
            ensureColumn(column).setPropertyString("width", width);
        }

        private Element ensureColumn(int col) {
            prepareColumn(col);
            prepareColumnGroup();
            resizeColumnGroup(col + 1, true);
            return columnGroup.getChild(col).cast();
        }

        /**
         * Prepare the colgroup tag for the first time, guaranteeing that it
         * exists and has at least one col tag in it. This method corrects a
         * Mozilla issue where the col tag will affect the wrong column if a col
         * tag doesn't exist when the element is attached to the page.
         */
        private void prepareColumnGroup() {
            if (columnGroup == null) {
                columnGroup = DOM.createElement("colgroup");
                DOM.insertChild(tableElem, columnGroup, 0);
                DOM.appendChild(columnGroup, DOM.createElement("col"));
            }
        }

        /**
         * Resize the column group element.
         * 
         * @param columns
         *            the number of columns
         * @param growOnly
         *            true to only grow, false to shrink if needed
         */
        void resizeColumnGroup(int columns, boolean growOnly) {
            // The colgroup should always have at least one element. See
            // prepareColumnGroup() for more details.
            columns = Math.max(columns, 1);
            int num = columnGroup.getChildCount();
            if (num < columns) {
                for (int i = num; i < columns; i++) {
                    columnGroup.appendChild(Document.get().createColElement());
                }
            } else if (!growOnly && num > columns) {
                for (int i = num; i > columns; i--) {
                    columnGroup.removeChild(columnGroup.getLastChild());
                }
            }
        }
    }

    /**
     * This class contains methods used to format a table's rows.
     */
    public class RowFormatter {
        /**
         * Adds a style to the specified row.
         * 
         * @param row
         *            the row to which the style will be added
         * @param styleName
         *            the style name to be added
         * @see UIObject#addStyleName(String)
         * @throws IndexOutOfBoundsException
         */
        public void addStyleName(int row, String styleName) {
            UIObject.setStyleName(ensureElement(row), styleName, true);
        }

        /**
         * Gets the TR element representing the specified row.
         * 
         * @param row
         *            the row whose TR element is to be retrieved
         * @return the row's TR element
         * @throws IndexOutOfBoundsException
         */
        public Element getElement(int row) {
            checkRowBounds(row);
            return DOM.asOld(getRow(bodyElem, row));
        }

        /**
         * Gets the style of the specified row.
         * 
         * @param row
         *            the row to be queried
         * @return the style name
         * @see UIObject#getStyleName()
         * @throws IndexOutOfBoundsException
         */
        public String getStyleName(int row) {
            return UIObject.getStyleName(getElement(row));
        }

        /**
         * Gets the primary style of the specified row.
         * 
         * @param row
         *            the row to be queried
         * @return the style name
         * @see UIObject#getStylePrimaryName()
         * @throws IndexOutOfBoundsException
         */
        public String getStylePrimaryName(int row) {
            return UIObject.getStylePrimaryName(getElement(row));
        }

        /**
         * Determines whether or not this row is visible via the display style
         * attribute.
         * 
         * @param row
         *            the row whose visibility is to be set
         * @return <code>true</code> if the row is visible
         */
        public boolean isVisible(int row) {
            Element e = getElement(row);
            return UIObject.isVisible(e);
        }

        /**
         * Removes a style from the specified row.
         * 
         * @param row
         *            the row from which the style will be removed
         * @param styleName
         *            the style name to be removed
         * @see UIObject#removeStyleName(String)
         * @throws IndexOutOfBoundsException
         */
        public void removeStyleName(int row, String styleName) {
            UIObject.setStyleName(ensureElement(row), styleName, false);
        }

        /**
         * Sets the style name associated with the specified row.
         * 
         * @param row
         *            the row whose style name is to be set
         * @param styleName
         *            the new style name
         * @see UIObject#setStyleName(String)
         * @throws IndexOutOfBoundsException
         */
        public void setStyleName(int row, String styleName) {
            UIObject.setStyleName(ensureElement(row), styleName);
        }

        /**
         * Sets the primary style name associated with the specified row.
         * 
         * @param row
         *            the row whose style name is to be set
         * @param styleName
         *            the new style name
         * @see UIObject#setStylePrimaryName(String)
         * @throws IndexOutOfBoundsException
         */
        public void setStylePrimaryName(int row, String styleName) {
            UIObject.setStylePrimaryName(ensureElement(row), styleName);
        }

        /**
         * Sets the vertical alignment of the specified row.
         * 
         * @param row
         *            the row whose alignment is to be set
         * @param align
         *            the row's new vertical alignment as specified in
         *            {@link HasVerticalAlignment}
         * @throws IndexOutOfBoundsException
         */
        public void setVerticalAlign(int row, VerticalAlignmentConstant align) {
            ensureElement(row).getStyle().setProperty("verticalAlign", align.getVerticalAlignString());
        }

        /**
         * Sets whether this row is visible.
         * 
         * @param row
         *            the row whose visibility is to be set
         * @param visible
         *            <code>true</code> to show the row, <code>false</code> to
         *            hide it
         */
        public void setVisible(int row, boolean visible) {
            Element e = ensureElement(row);
            UIObject.setVisible(e, visible);
        }

        /**
         * Ensure the TR element representing the specified row exists for
         * subclasses that allow dynamic addition of elements.
         * 
         * @param row
         *            the row whose TR element is to be retrieved
         * @return the row's TR element
         * @throws IndexOutOfBoundsException
         */
        protected Element ensureElement(int row) {
            prepareRow(row);
            return DOM.asOld(getRow(bodyElem, row));
        }

        /**
         * @deprecated Call and override {@link #getRow(Element, int)} instead.
         */
        protected Element getRow(Element tbody, int row) {
            return DOM.asOld(impl.getRows(tbody).get(row));
        }

        /**
         * Convenience methods to set an attribute on a row.
         * 
         * @param row
         *            cell's row
         * @param attrName
         *            attribute to set
         * @param value
         *            value to set
         * @throws IndexOutOfBoundsException
         */
        protected void setAttr(int row, String attrName, String value) {
            Element elem = ensureElement(row);
            elem.setAttribute(attrName, value);
        }
    }

    /**
     * IE specific implementation for accessing the Table DOM. see: issue 6938
     */
    @SuppressWarnings("unused") // used due to rebinding
    private static class HTMLTableIEImpl extends HTMLTableStandardImpl {
        native JsArray<ElementRemote> getCells0(ElementRemote row) /*-{
                                                                   return row.children;
                                                                   }-*/;

        native JsArray<ElementRemote> getRows0(ElementRemote tbody) /*-{
                                                                    return tbody.children;
                                                                    }-*/;
    }

    /**
     * Interface to access {@link HTMLTable}'s DOM.
     */
    private interface HTMLTableImpl {
        ElementArray<Element> getCells(Element row);

        ElementArray<Element> getRows(Element tbody);
    }

    /**
     * Standard implementation for accessing the Table DOM.
     */
    private static class HTMLTableStandardImpl implements HTMLTableImpl {
        @Override
        public ElementArray<Element> getCells(Element row) {
            return new ElementArray<Element>((List) ((TableRowElement) row).provideChildNodeList());
        }

        @Override
        public ElementArray<Element> getRows(Element tbody) {
            return new ElementArray<Element>((List) ((TableSectionElement) tbody).provideChildNodeList());
        }

        native JsArray<ElementRemote> getCells0(ElementRemote row) /*-{
                                                                   return row.cells;
                                                                   }-*/;

        native JsArray<ElementRemote> getRows0(ElementRemote tbody) /*-{
                                                                    return tbody.rows;
                                                                    }-*/;
    }

    static class ElementArray<T extends Node> {
        private JsArray<ElementRemote> jsArray;

        private NodeList<? extends Element> nodeList;

        private List<? extends Element> jvmList;

        public ElementArray(JsArray<ElementRemote> elements) {
            this.jsArray = elements;
        }

        public ElementArray(List<? extends Element> jvmList) {
            this.jvmList = jvmList;
        }

        public ElementArray(NodeList<? extends Element> nodeList) {
            this.nodeList = nodeList;
        }

        public Element get(int idx) {
            if (jvmList != null) {
                return jvmList.get(idx);
            }
            return nodeList != null ? nodeList.getItem(idx) : LocalDom.nodeFor(jsArray.get(idx));
        }

        public int length() {
            if (jvmList != null) {
                return jvmList.size();
            }
            return nodeList != null ? nodeList.getLength() : jsArray.length();
        }
    }
}