com.google.gwt.user.cellview.client.AbstractCellTable Maven / Gradle / Ivy
/*
* Copyright 2010 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.cellview.client;
import com.google.gwt.cell.client.Cell;
import com.google.gwt.cell.client.Cell.Context;
import com.google.gwt.cell.client.FieldUpdater;
import com.google.gwt.cell.client.HasCell;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.builder.shared.HtmlTableSectionBuilder;
import com.google.gwt.dom.builder.shared.TableSectionBuilder;
import com.google.gwt.dom.client.BrowserEvents;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.TableCellElement;
import com.google.gwt.dom.client.TableElement;
import com.google.gwt.dom.client.TableRowElement;
import com.google.gwt.dom.client.TableSectionElement;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.safehtml.client.SafeHtmlTemplates;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
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.Window;
import com.google.gwt.user.client.ui.HasHorizontalAlignment.HorizontalAlignmentConstant;
import com.google.gwt.user.client.ui.HasVerticalAlignment.VerticalAlignmentConstant;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.view.client.CellPreviewEvent;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.SelectionModel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Abstract base class for tabular views that supports paging and columns.
*
*
*
Columns
The {@link Column} class defines the {@link Cell} used to
* render a column. Implement {@link Column#getValue(Object)} to retrieve the
* field value from the row object that will be rendered in the {@link Cell}.
*
*
*
*
Headers and Footers
A {@link Header} can be placed at the top
* (header) or bottom (footer) of the {@link AbstractCellTable}. You can specify
* a header as text using {@link #addColumn(Column, String)}, or you can create
* a custom {@link Header} that can change with the value of the cells, such as
* a column total. The {@link Header} will be rendered every time the row data
* changes or the table is redrawn. If you pass the same header instance (==)
* into adjacent columns, the header will span the columns.
*
*
* @param the data type of each row
*/
public abstract class AbstractCellTable extends AbstractHasData {
/**
* Default implementation of a keyboard navigation handler for tables that
* supports navigation between cells.
*
* @param the data type of each row
*/
public static class CellTableKeyboardSelectionHandler extends
DefaultKeyboardSelectionHandler {
private final AbstractCellTable table;
/**
* Construct a new keyboard selection handler for the specified table.
*
* @param table the display being handled
*/
public CellTableKeyboardSelectionHandler(AbstractCellTable table) {
super(table);
this.table = table;
}
@Override
public AbstractCellTable getDisplay() {
return table;
}
@Override
public void onCellPreview(CellPreviewEvent event) {
NativeEvent nativeEvent = event.getNativeEvent();
String eventType = event.getNativeEvent().getType();
if (BrowserEvents.KEYDOWN.equals(eventType) && !event.isCellEditing()) {
/*
* Handle keyboard navigation, unless the cell is being edited. If the
* cell is being edited, we do not want to change rows.
*
* Prevent default on navigation events to prevent default scrollbar
* behavior.
*/
int oldRow = table.getKeyboardSelectedRow();
int oldColumn = table.getKeyboardSelectedColumn();
boolean isRtl = LocaleInfo.getCurrentLocale().isRTL();
int keyCodeLineEnd = isRtl ? KeyCodes.KEY_LEFT : KeyCodes.KEY_RIGHT;
int keyCodeLineStart = isRtl ? KeyCodes.KEY_RIGHT : KeyCodes.KEY_LEFT;
int keyCode = nativeEvent.getKeyCode();
if (keyCode == keyCodeLineEnd) {
int nextColumn = findInteractiveColumn(oldColumn, false);
if (nextColumn <= oldColumn) {
// Wrap to the next row.
table.setKeyboardSelectedRow(oldRow + 1);
if (table.getKeyboardSelectedRow() != oldRow) {
// If the row didn't change, we are at the end of the table.
table.setKeyboardSelectedColumn(nextColumn);
handledEvent(event);
return;
}
} else {
table.setKeyboardSelectedColumn(nextColumn);
handledEvent(event);
return;
}
} else if (keyCode == keyCodeLineStart) {
int prevColumn = findInteractiveColumn(oldColumn, true);
if (prevColumn >= oldColumn) {
// Wrap to the previous row.
table.setKeyboardSelectedRow(oldRow - 1);
if (table.getKeyboardSelectedRow() != oldRow) {
// If the row didn't change, we are at the start of the table.
table.setKeyboardSelectedColumn(prevColumn);
handledEvent(event);
return;
}
} else {
table.setKeyboardSelectedColumn(prevColumn);
handledEvent(event);
return;
}
}
} else if (BrowserEvents.CLICK.equals(eventType) || BrowserEvents.FOCUS.equals(eventType)) {
/*
* Move keyboard focus to the clicked column, even if the cell is being
* edited. Unlike key events, we aren't moving the currently selected
* row, just updating it based on where the user clicked.
*
* Since the user clicked, allow focus to go to a non-interactive
* column.
*/
int col = event.getColumn();
int relRow = event.getIndex() - table.getPageStart();
int subrow = event.getContext().getSubIndex();
if ((table.getKeyboardSelectedColumn() != col)
|| (table.getKeyboardSelectedRow() != relRow)
|| (table.getKeyboardSelectedSubRow() != subrow)) {
boolean stealFocus = false;
if (BrowserEvents.CLICK.equals(eventType)) {
// If a natively focusable element was just clicked, then do not
// steal focus.
Element target = Element.as(event.getNativeEvent().getEventTarget());
stealFocus = !CellBasedWidgetImpl.get().isFocusable(target);
}
// Update the row and subrow.
table.setKeyboardSelectedRow(relRow, subrow, stealFocus);
// Update the column index.
table.setKeyboardSelectedColumn(col, stealFocus);
}
// Do not cancel the event as the click may have occurred on a Cell.
return;
}
// Let the parent class handle the event.
super.onCellPreview(event);
}
/**
* Find and return the index of the next interactive column. If no column is
* interactive, 0 is returned. If the start index is the only interactive
* column, it is returned.
*
* @param start the start index, exclusive unless it is the only option
* @param reverse true to do a reverse search
* @return the interactive column index, or 0 if not interactive
*/
private int findInteractiveColumn(int start, boolean reverse) {
if (!table.isInteractive) {
return 0;
} else if (reverse) {
for (int i = start - 1; i >= 0; i--) {
if (isColumnInteractive(table.getColumn(i))) {
return i;
}
}
// Wrap to the end.
for (int i = table.getColumnCount() - 1; i >= start; i--) {
if (isColumnInteractive(table.getColumn(i))) {
return i;
}
}
} else {
for (int i = start + 1; i < table.getColumnCount(); i++) {
if (isColumnInteractive(table.getColumn(i))) {
return i;
}
}
// Wrap to the start.
for (int i = 0; i <= start; i++) {
if (isColumnInteractive(table.getColumn(i))) {
return i;
}
}
}
return 0;
}
}
/**
* A ClientBundle that provides images for this widget.
*/
public interface Resources {
/**
* Icon used when a column is sorted in ascending order.
*/
ImageResource sortAscending();
/**
* Icon used when a column is sorted in descending order.
*/
ImageResource sortDescending();
/**
* The styles used in this widget.
*/
Style style();
}
/**
* Styles used by this widget.
*/
public interface Style {
/**
* Applied to every cell.
*/
String cell();
/**
* Applied to even rows.
*/
String evenRow();
/**
* Applied to cells in even rows.
*/
String evenRowCell();
/**
* Applied to the first column.
*/
String firstColumn();
/**
* Applied to the first column footers.
*/
String firstColumnFooter();
/**
* Applied to the first column headers.
*/
String firstColumnHeader();
/**
* Applied to footers cells.
*/
String footer();
/**
* Applied to headers cells.
*/
String header();
/**
* Applied to the hovered row.
*/
String hoveredRow();
/**
* Applied to the cells in the hovered row.
*/
String hoveredRowCell();
/**
* Applied to the keyboard selected cell.
*/
String keyboardSelectedCell();
/**
* Applied to the keyboard selected row.
*/
String keyboardSelectedRow();
/**
* Applied to the cells in the keyboard selected row.
*/
String keyboardSelectedRowCell();
/**
* Applied to the last column.
*/
String lastColumn();
/**
* Applied to the last column footers.
*/
String lastColumnFooter();
/**
* Applied to the last column headers.
*/
String lastColumnHeader();
/**
* Applied to odd rows.
*/
String oddRow();
/**
* Applied to cells in odd rows.
*/
String oddRowCell();
/**
* Applied to selected rows.
*/
String selectedRow();
/**
* Applied to cells in selected rows.
*/
String selectedRowCell();
/**
* Applied to header cells that are sortable.
*/
String sortableHeader();
/**
* Applied to header cells that are sorted in ascending order.
*/
String sortedHeaderAscending();
/**
* Applied to header cells that are sorted in descending order.
*/
String sortedHeaderDescending();
/**
* Applied to the table.
*/
String widget();
}
/**
* Interface that this class's subclass may implement to get notified with table section change
* event. During rendering, a faster method based on swaping the entire section will be used iff
* it's in IE - since all other optimizations have been turned off
* the table implements TableSectionChangeHandler interface
* When a section is being replaced by another table with the new table html, the methods in this
* interface will be invoked with the changed section. The table should update its internal
* references to the sections properly so that when {@link #getTableBodyElement},
* {@link #getTableHeadElement}, or {@link #getTableFootElement} are called, the correct section
* will be returned.
*/
protected interface TableSectionChangeHandler {
/**
* Notify that a table body section has been changed.
* @param newTBody the new body section
*/
void onTableBodyChange(TableSectionElement newTBody);
/**
* Notify that a table body section has been changed.
* @param newTFoot the new foot section
*/
void onTableFootChange(TableSectionElement newTFoot);
/**
* Notify that a table head section has been changed.
* @param newTHead the new head section
*/
void onTableHeadChange(TableSectionElement newTHead);
}
interface Template extends SafeHtmlTemplates {
@SafeHtmlTemplates.Template("{0}")
SafeHtml div(SafeHtml contents);
@SafeHtmlTemplates.Template("{0}
")
SafeHtml tbody(SafeHtml rowHtml);
@SafeHtmlTemplates.Template("{1} ")
SafeHtml td(String classes, SafeHtml contents);
@SafeHtmlTemplates.Template("{3} ")
SafeHtml tdBothAlign(String classes, String hAlign, String vAlign, SafeHtml contents);
@SafeHtmlTemplates.Template("{2} ")
SafeHtml tdHorizontalAlign(String classes, String hAlign, SafeHtml contents);
@SafeHtmlTemplates.Template("{2} ")
SafeHtml tdVerticalAlign(String classes, String vAlign, SafeHtml contents);
@SafeHtmlTemplates.Template("{0}
")
SafeHtml tfoot(SafeHtml rowHtml);
@SafeHtmlTemplates.Template("{0}
")
SafeHtml thead(SafeHtml rowHtml);
@SafeHtmlTemplates.Template("{1} ")
SafeHtml tr(String classes, SafeHtml contents);
}
/**
* Implementation of {@link AbstractCellTable}.
*/
private static class Impl {
private final com.google.gwt.user.client.Element tmpElem = Document.get().createDivElement()
.cast();
/**
* Convert the rowHtml into Elements wrapped by the specified table section.
*
* @param table the {@link AbstractCellTable}
* @param sectionTag the table section tag
* @param rowHtml the Html for the rows
* @return the section element
*/
public TableSectionElement convertToSectionElement(AbstractCellTable table,
String sectionTag, SafeHtml rowHtml) {
// Attach an event listener so we can catch synchronous load events from
// cached images.
DOM.setEventListener(tmpElem, table);
/*
* Render the rows into a table.
*
* IE doesn't support innerHtml on a TableSection or Table element, so we
* generate the entire table. We do the same for all browsers to avoid any
* future bugs, since setting innerHTML on a table section seems brittle.
*/
sectionTag = sectionTag.toLowerCase();
if ("tbody".equals(sectionTag)) {
tmpElem.setInnerSafeHtml(template.tbody(rowHtml));
} else if ("thead".equals(sectionTag)) {
tmpElem.setInnerSafeHtml(template.thead(rowHtml));
} else if ("tfoot".equals(sectionTag)) {
tmpElem.setInnerSafeHtml(template.tfoot(rowHtml));
} else {
throw new IllegalArgumentException("Invalid table section tag: " + sectionTag);
}
TableElement tableElem = tmpElem.getFirstChildElement().cast();
// Detach the event listener.
DOM.setEventListener(tmpElem, null);
// Get the section out of the table.
if ("tbody".equals(sectionTag)) {
return tableElem.getTBodies().getItem(0);
} else if ("thead".equals(sectionTag)) {
return tableElem.getTHead();
} else if ("tfoot".equals(sectionTag)) {
return tableElem.getTFoot();
} else {
throw new IllegalArgumentException("Invalid table section tag: " + sectionTag);
}
}
/**
* Render a table section in the table.
*
* @param table the {@link AbstractCellTable}
* @param section the {@link TableSectionElement} to replace
* @param html the html of a table section element containing the rows
*/
public final void replaceAllRows(AbstractCellTable table, TableSectionElement section,
SafeHtml html) {
// If the widget is not attached, attach an event listener so we can catch
// synchronous load events from cached images.
if (!table.isAttached()) {
DOM.setEventListener(table.getElement(), table);
}
// Remove the section from the tbody.
Element parent = section.getParentElement();
Element nextSection = section.getNextSiblingElement();
detachSectionElement(section);
// Render the html.
replaceAllRowsImpl(table, section, html);
/*
* Reattach the section. If next section is null, the section will be
* appended instead.
*/
reattachSectionElement(parent, section, nextSection);
// Detach the event listener.
if (!table.isAttached()) {
DOM.setEventListener(table.getElement(), null);
}
}
/**
* Replace a set of row values with newly rendered values.
*
* This method does not necessarily perform a one to one replacement. Some
* row values may be rendered as multiple row elements, while others are
* rendered as only one row element.
*
* @param table the {@link AbstractCellTable}
* @param section the {@link TableSectionElement} to replace
* @param html the html of a table section element containing the rows
* @param startIndex the start index to replace
* @param childCount the number of row values to replace
*/
public final void replaceChildren(AbstractCellTable table, TableSectionElement section,
SafeHtml html, int startIndex, int childCount) {
// If the widget is not attached, attach an event listener so we can catch
// synchronous load events from cached images.
if (!table.isAttached()) {
DOM.setEventListener(table.getElement(), table);
}
// Remove the section from the tbody.
Element parent = section.getParentElement();
Element nextSection = section.getNextSiblingElement();
detachSectionElement(section);
// Remove all children in the range.
final int absEndIndex = table.getPageStart() + startIndex + childCount;
TableRowElement insertBefore = table.getChildElement(startIndex).cast();
if (table.legacyRenderRowValues) {
int count = 0;
while (insertBefore != null && count < childCount) {
Element next = insertBefore.getNextSiblingElement();
section.removeChild(insertBefore);
insertBefore = (next == null) ? null : next. cast();
count++;
}
} else {
while (insertBefore != null
&& table.tableBuilder.getRowValueIndex(insertBefore) < absEndIndex) {
Element next = insertBefore.getNextSiblingElement();
section.removeChild(insertBefore);
insertBefore = (next == null) ? null : next. cast();
}
}
// Add new child elements.
TableSectionElement newSection = convertToSectionElement(table, section.getTagName(), html);
Element newChild = newSection.getFirstChildElement();
while (newChild != null) {
Element next = newChild.getNextSiblingElement();
section.insertBefore(newChild, insertBefore);
newChild = next;
}
/*
* Reattach the section. If next section is null, the section will be
* appended instead.
*/
reattachSectionElement(parent, section, nextSection);
// Detach the event listener.
if (!table.isAttached()) {
DOM.setEventListener(table.getElement(), null);
}
}
/**
* Detach a table section element from its parent.
*
* @param section the element to detach
*/
protected void detachSectionElement(TableSectionElement section) {
section.removeFromParent();
}
/**
* Reattach a table section element from its parent.
*
* @param parent the parent element
* @param section the element to reattach
* @param nextSection the next section
*/
protected void reattachSectionElement(Element parent, TableSectionElement section,
Element nextSection) {
parent.insertBefore(section, nextSection);
}
/**
* Render a table section in the table.
*
* @param table the {@link AbstractCellTable}
* @param section the {@link TableSectionElement} to replace
* @param html the html of a table section element containing the rows
*/
protected void replaceAllRowsImpl(AbstractCellTable table, TableSectionElement section,
SafeHtml html) {
section.setInnerSafeHtml(html);
}
}
/**
* Implementation of {@link CellTable} used by Firefox.
*/
@SuppressWarnings("unused")
private static class ImplMozilla extends Impl {
/**
* Firefox 3.6 and earlier convert td elements to divs if the tbody is
* removed from the table element.
*/
@Override
protected void detachSectionElement(TableSectionElement section) {
if (isGecko192OrBefore()) {
return;
}
super.detachSectionElement(section);
}
@Override
protected void reattachSectionElement(Element parent, TableSectionElement section,
Element nextSection) {
if (isGecko192OrBefore()) {
return;
}
super.reattachSectionElement(parent, section, nextSection);
}
/**
* Return true if using Gecko 1.9.2 (Firefox 3.6) or earlier.
*/
private native boolean isGecko192OrBefore() /*-{
return @com.google.gwt.dom.client.DOMImplMozilla::isGecko192OrBefore()();
}-*/;
}
/**
* Implementation of {@link AbstractCellTable} used by IE.
*/
@SuppressWarnings("unused")
private static class ImplTrident extends Impl {
/**
* A different optimization is used in IE.
*/
@Override
protected void detachSectionElement(TableSectionElement section) {
return;
}
@Override
protected void reattachSectionElement(Element parent, TableSectionElement section,
Element nextSection) {
return;
}
/**
* Instead of replacing each TR element, swaping out the entire section is much faster. If
* the table has a sectionChangeHandler, this method will be used.
*/
@Override
protected void replaceAllRowsImpl(AbstractCellTable table, TableSectionElement section,
SafeHtml html) {
if (table instanceof TableSectionChangeHandler) {
replaceTableSection(table, section, html);
} else {
replaceAllRowsImplLegacy(table, section, html);
}
}
/**
* This method is used for legacy AbstractCellTable that's not a
* {@link TableSectionChangeHandler}.
*/
protected void replaceAllRowsImplLegacy(AbstractCellTable table, TableSectionElement section,
SafeHtml html) {
// Remove all children.
Element child = section.getFirstChildElement();
while (child != null) {
Element next = child.getNextSiblingElement();
section.removeChild(child);
child = next;
}
// Add new child elements.
TableSectionElement newSection = convertToSectionElement(table, section.getTagName(), html);
child = newSection.getFirstChildElement();
while (child != null) {
Element next = child.getNextSiblingElement();
section.appendChild(child);
child = next;
}
}
/**
* Render html into a table section. This is achieved by first setting the html in a DIV
* element, and then swap the table section with the corresponding element in the DIV. This
* method is used in IE since the normal optimizations are not feasible.
*
* @param table the {@link AbstractCellTable}
* @param section the {@link TableSectionElement} to replace
* @param html the html of a table section element containing the rows
*/
private void replaceTableSection(AbstractCellTable table, TableSectionElement section,
SafeHtml html) {
String sectionName = section.getTagName().toLowerCase();
TableSectionElement newSection = convertToSectionElement(table, sectionName, html);
TableElement tableElement = table.getElement().cast();
tableElement.replaceChild(newSection, section);
if ("tbody".equals(sectionName)) {
((TableSectionChangeHandler) table).onTableBodyChange(newSection);
} else if ("thead".equals(sectionName)) {
((TableSectionChangeHandler) table).onTableHeadChange(newSection);
} else if ("tfoot".equals(sectionName)) {
((TableSectionChangeHandler) table).onTableFootChange(newSection);
}
}
}
/**
* The error message used when {@link HeaderBuilder} returns malformed table
* section HTML.
*/
private static final String MALFORMED_HTML_SECTION =
"Malformed HTML: The table section returned by HeaderBuilder or FooterBuilder must use the "
+ "tag name thead or tfoot, as appropriate, and cannot contain any attributes or styles.";
/*
* The table specific {@link Impl}.
*/
private static Impl TABLE_IMPL;
private static Template template;
/**
* Check if a column consumes events.
*/
private static boolean isColumnInteractive(HasCell column) {
Set consumedEvents = column.getCell().getConsumedEvents();
return consumedEvents != null && consumedEvents.size() > 0;
}
/**
* Get the {@link TableSectionElement} containing the children.
*
* @param tag the expected tag (tbody, tfoot, or thead)
*/
private static SafeHtml tableSectionToSafeHtml(TableSectionBuilder section, String tag) {
if (!(section instanceof HtmlTableSectionBuilder)) {
throw new IllegalArgumentException("Only HtmlTableSectionBuilder is supported at this time");
}
// Strip the table section tags off of the tbody.
HtmlTableSectionBuilder htmlSection = (HtmlTableSectionBuilder) section;
String rawHtml = htmlSection.asSafeHtml().asString();
assert (tag.length()) == 5 : "Unrecognized tag: " + tag;
assert rawHtml.startsWith("<" + tag + ">") : MALFORMED_HTML_SECTION;
assert rawHtml.endsWith("") : MALFORMED_HTML_SECTION;
rawHtml = rawHtml.substring(7, rawHtml.length() - 8);
return SafeHtmlUtils.fromTrustedString(rawHtml);
}
private boolean cellIsEditing;
private final List> columns = new ArrayList>();
private final Map, String> columnWidths = new HashMap, String>();
private boolean columnWidthsDirty;
private final Map columnWidthsByIndex = new HashMap();
/**
* The maximum column index specified in column widths by index.
*/
private int maxColumnIndex = -1;
/**
* Indicates that at least one column depends on selection.
*/
private boolean dependsOnSelection;
private Widget emptyTableWidget;
private FooterBuilder footerBuilder;
private boolean footerRefreshDisabled;
private final List> footers = new ArrayList>();
/**
* Indicates that at least one column handles selection.
*/
private boolean handlesSelection;
private HeaderBuilder headerBuilder;
private boolean headerRefreshDisabled;
private final List> headers = new ArrayList>();
/**
* Indicates that either the headers or footers are dirty, and both should be
* refreshed the next time the table is redrawn.
*/
private boolean headersDirty;
private TableRowElement hoveringRow;
/**
* Indicates that at least one column is interactive.
*/
private boolean isInteractive;
private int keyboardSelectedColumn = 0;
private int keyboardSelectedSubrow = 0;
private int lastKeyboardSelectedSubrow = 0;
private Widget loadingIndicator;
private boolean legacyRenderRowValues = true;
private final Resources resources;
private RowStyles rowStyles;
private final ColumnSortList sortList = new ColumnSortList(new ColumnSortList.Delegate() {
@Override
public void onModification() {
if (!updatingSortList) {
createHeaders(false);
}
}
});
private final Style style;
private CellTableBuilder tableBuilder;
private boolean updatingSortList;
private boolean skipRowHoverCheck;
private boolean skipRowHoverFloatElementCheck;
private boolean skipRowHoverStyleUpdate;
/**
* Constructs a table with the given page size, the specified {@link Style},
* and the given key provider.
*
* @param elem the parent {@link Element}
* @param pageSize the page size
* @param resources the resources to apply to the widget
* @param keyProvider an instance of ProvidesKey, or null if the record
* object should act as its own key
*/
public AbstractCellTable(Element elem, final int pageSize, Resources resources,
ProvidesKey keyProvider) {
super(elem, pageSize, keyProvider);
this.resources = resources;
this.style = resources.style();
init();
}
/**
* Constructs a table with the given page size, the specified {@link Style},
* and the given key provider.
*
* @param widget the parent widget
* @param pageSize the page size
* @param resources the resources to apply to the widget
* @param keyProvider an instance of ProvidesKey, or null if the record
* object should act as its own key
*/
public AbstractCellTable(Widget widget, final int pageSize, Resources resources,
ProvidesKey keyProvider) {
super(widget, pageSize, keyProvider);
this.resources = resources;
this.style = resources.style();
init();
}
/**
* Adds a column to the end of the table.
*
* @param col the column to be added
*/
public void addColumn(Column col) {
insertColumn(getColumnCount(), col);
}
/**
* Adds a column to the end of the table with an associated header.
*
* @param col the column to be added
* @param header the associated {@link Header}
*/
public void addColumn(Column col, Header header) {
insertColumn(getColumnCount(), col, header);
}
/**
* Adds a column to the end of the table with an associated header and footer.
*
* @param col the column to be added
* @param header the associated {@link Header}
* @param footer the associated footer (as a {@link Header} object)
*/
public void addColumn(Column col, Header header, Header footer) {
insertColumn(getColumnCount(), col, header, footer);
}
/**
* Adds a column to the end of the table with an associated String header.
*
* @param col the column to be added
* @param headerString the associated header text, as a String
*/
public void addColumn(Column col, String headerString) {
insertColumn(getColumnCount(), col, headerString);
}
/**
* Adds a column to the end of the table with an associated {@link SafeHtml}
* header.
*
* @param col the column to be added
* @param headerHtml the associated header text, as safe HTML
*/
public void addColumn(Column col, SafeHtml headerHtml) {
insertColumn(getColumnCount(), col, headerHtml);
}
/**
* Adds a column to the end of the table with an associated String header and
* footer.
*
* @param col the column to be added
* @param headerString the associated header text, as a String
* @param footerString the associated footer text, as a String
*/
public void addColumn(Column col, String headerString, String footerString) {
insertColumn(getColumnCount(), col, headerString, footerString);
}
/**
* Adds a column to the end of the table with an associated {@link SafeHtml}
* header and footer.
*
* @param col the column to be added
* @param headerHtml the associated header text, as safe HTML
* @param footerHtml the associated footer text, as safe HTML
*/
public void addColumn(Column col, SafeHtml headerHtml, SafeHtml footerHtml) {
insertColumn(getColumnCount(), col, headerHtml, footerHtml);
}
/**
* Add a handler to handle {@link ColumnSortEvent}s.
*
* @param handler the {@link ColumnSortEvent.Handler} to add
* @return a {@link HandlerRegistration} to remove the handler
*/
public HandlerRegistration addColumnSortHandler(ColumnSortEvent.Handler handler) {
return addHandler(handler, ColumnSortEvent.getType());
}
/**
* Add a style name to the col
element at the specified index,
* creating it if necessary.
*
* @param index the column index
* @param styleName the style name to add
*/
public abstract void addColumnStyleName(int index, String styleName);
/**
* Add a handler to handle {@link RowHoverEvent}s.
*
* @param handler the {@link RowHoverEvent.Handler} to add
* @return a {@link HandlerRegistration} to remove the handler
*/
public HandlerRegistration addRowHoverHandler(RowHoverEvent.Handler handler) {
return addHandler(handler, RowHoverEvent.getType());
}
/**
* Clear the width of the specified {@link Column}.
*
* @param column the column
*/
public void clearColumnWidth(Column column) {
columnWidths.remove(column);
updateColumnWidthImpl(column, null);
}
/**
* Clear the width of the specified {@link Column}.
*
* @param column the column index
*/
public void clearColumnWidth(Integer column) {
columnWidthsByIndex.remove(column);
// Recalculate the maximum column index.
if (column >= maxColumnIndex) {
maxColumnIndex = -1;
for (Integer index : columnWidthsByIndex.keySet()) {
maxColumnIndex = Math.max(maxColumnIndex, index);
}
}
// Update the width of the column.
if (column < getRealColumnCount()) {
doSetColumnWidth(column, null);
}
}
/**
* Flush all pending changes to the table and render immediately.
*
*
* Modifications to the table, such as adding columns or setting data, are not
* rendered immediately. Instead, changes are coalesced at the end of the
* current event loop to avoid rendering the table multiple times. Use this
* method to force the table to render all pending modifications immediately.
*
*/
public void flush() {
getPresenter().flush();
}
/**
* Get the column at the specified index.
*
* @param col the index of the column to retrieve
* @return the {@link Column} at the index
*/
public Column getColumn(int col) {
checkColumnBounds(col);
return columns.get(col);
}
/**
* Get the number of columns in the table.
*
* @return the column count
*/
public int getColumnCount() {
return columns.size();
}
/**
* Get the index of the specified column.
*
* @param column the column to search for
* @return the index of the column, or -1 if not found
*/
public int getColumnIndex(Column column) {
return columns.indexOf(column);
}
/**
* Get the {@link ColumnSortList} that specifies which columns are sorted.
* Modifications to the {@link ColumnSortList} will be reflected in the table
* header.
*
*
* Note that the implementation may redraw the headers on every modification
* to the {@link ColumnSortList}.
*
*
* @return the {@link ColumnSortList}
*/
public ColumnSortList getColumnSortList() {
return sortList;
}
/**
* Get the width of a {@link Column}.
*
* @param column the column
* @return the width of the column, or null if not set
* @see #setColumnWidth(Column, double, Unit)
*/
public String getColumnWidth(Column column) {
return columnWidths.get(column);
}
/**
* Get the widget displayed when the table has no rows.
*
* @return the empty table widget
*/
public Widget getEmptyTableWidget() {
return emptyTableWidget;
}
/**
* Get the {@link Header} from the footer section that was added with a
* {@link Column}.
*/
public Header getFooter(int index) {
return footers.get(index);
}
/**
* Get the {@link HeaderBuilder} used to generate the footer section.
*/
public FooterBuilder getFooterBuilder() {
return footerBuilder;
}
/**
* Get the {@link Header} from the header section that was added with a
* {@link Column}.
*/
public Header getHeader(int index) {
return headers.get(index);
}
/**
* Get the {@link HeaderBuilder} used to generate the header section.
*/
public HeaderBuilder getHeaderBuilder() {
return headerBuilder;
}
/**
* Get the index of the column that is currently selected via the keyboard.
*
* @return the currently selected column, or -1 if none selected
*/
public int getKeyboardSelectedColumn() {
return KeyboardSelectionPolicy.DISABLED == getKeyboardSelectionPolicy() ? -1
: keyboardSelectedColumn;
}
/**
* Get the index of the sub row that is currently selected via the keyboard.
* If the row value maps to one rendered row element, the subrow is 0.
*
* @return the currently selected subrow, or -1 if none selected
*/
public int getKeyboardSelectedSubRow() {
return KeyboardSelectionPolicy.DISABLED == getKeyboardSelectionPolicy() ? -1
: keyboardSelectedSubrow;
}
/**
* Get the widget displayed when the data is loading.
*
* @return the loading indicator
*/
public Widget getLoadingIndicator() {
return loadingIndicator;
}
/**
* Get the resources used by this table.
*/
public Resources getResources() {
return resources;
}
/**
* Get the {@link TableRowElement} for the specified row. If the row element
* has not been created, null is returned.
*
* @param row the row index
* @return the row element, or null if it doesn't exists
* @throws IndexOutOfBoundsException if the row index is outside of the
* current page
*/
public TableRowElement getRowElement(int row) {
flush();
return getChildElement(row);
}
/**
* Gets the object used to determine how a row is styled.
*
* @return the {@link RowStyles} object if set, null if not
*/
public RowStyles getRowStyles() {
return this.rowStyles;
}
/**
* Inserts a column into the table at the specified index.
*
* @param beforeIndex the index to insert the column
* @param col the column to be added
*/
public void insertColumn(int beforeIndex, Column col) {
insertColumn(beforeIndex, col, (Header) null, (Header) null);
}
/**
* Inserts a column into the table at the specified index with an associated
* header.
*
* @param beforeIndex the index to insert the column
* @param col the column to be added
* @param header the associated {@link Header}
*/
public void insertColumn(int beforeIndex, Column col, Header header) {
insertColumn(beforeIndex, col, header, null);
}
/**
* Inserts a column into the table at the specified index with an associated
* header and footer.
*
* @param beforeIndex the index to insert the column
* @param col the column to be added
* @param header the associated {@link Header}
* @param footer the associated footer (as a {@link Header} object)
* @throws IndexOutOfBoundsException if the index is out of range
*/
public void insertColumn(int beforeIndex, Column col, Header header, Header footer) {
// Allow insert at the end.
if (beforeIndex != getColumnCount()) {
checkColumnBounds(beforeIndex);
}
headers.add(beforeIndex, header);
footers.add(beforeIndex, footer);
columns.add(beforeIndex, col);
// Increment the keyboard selected column.
if (beforeIndex <= keyboardSelectedColumn) {
keyboardSelectedColumn = Math.min(keyboardSelectedColumn + 1, columns.size() - 1);
}
// Move the keyboard selected column if the current column is not
// interactive.
if (isColumnInteractive(col)
&& ((keyboardSelectedColumn >= columns.size()) || !isColumnInteractive(columns
.get(keyboardSelectedColumn)))) {
keyboardSelectedColumn = beforeIndex;
}
// Sink events used by the new column.
Set consumedEvents = new HashSet();
{
Set cellEvents = col.getCell().getConsumedEvents();
if (cellEvents != null) {
consumedEvents.addAll(cellEvents);
}
}
if (header != null) {
Set headerEvents = header.getCell().getConsumedEvents();
if (headerEvents != null) {
consumedEvents.addAll(headerEvents);
}
}
if (footer != null) {
Set footerEvents = footer.getCell().getConsumedEvents();
if (footerEvents != null) {
consumedEvents.addAll(footerEvents);
}
}
CellBasedWidgetImpl.get().sinkEvents(this, consumedEvents);
headersDirty = true;
refreshColumnsAndRedraw();
}
/**
* Inserts a column into the table at the specified index with an associated
* String header.
*
* @param beforeIndex the index to insert the column
* @param col the column to be added
* @param headerString the associated header text, as a String
*/
public void insertColumn(int beforeIndex, Column col, String headerString) {
insertColumn(beforeIndex, col, new TextHeader(headerString), null);
}
/**
* Inserts a column into the table at the specified index with an associated
* {@link SafeHtml} header.
*
* @param beforeIndex the index to insert the column
* @param col the column to be added
* @param headerHtml the associated header text, as safe HTML
*/
public void insertColumn(int beforeIndex, Column col, SafeHtml headerHtml) {
insertColumn(beforeIndex, col, new SafeHtmlHeader(headerHtml), null);
}
/**
* Inserts a column into the table at the specified index with an associated
* String header and footer.
*
* @param beforeIndex the index to insert the column
* @param col the column to be added
* @param headerString the associated header text, as a String
* @param footerString the associated footer text, as a String
*/
public void insertColumn(int beforeIndex, Column col, String headerString,
String footerString) {
insertColumn(beforeIndex, col, new TextHeader(headerString), new TextHeader(footerString));
}
/**
* Inserts a column into the table at the specified index with an associated
* {@link SafeHtml} header and footer.
*
* @param beforeIndex the index to insert the column
* @param col the column to be added
* @param headerHtml the associated header text, as safe HTML
* @param footerHtml the associated footer text, as safe HTML
*/
public void insertColumn(int beforeIndex, Column col, SafeHtml headerHtml,
SafeHtml footerHtml) {
insertColumn(beforeIndex, col, new SafeHtmlHeader(headerHtml), new SafeHtmlHeader(footerHtml));
}
/**
* Check if auto footer refresh is enabled or disabled.
*
* @return true if disabled, false if enabled
* @see #setAutoFooterRefreshDisabled(boolean)
*/
public boolean isAutoFooterRefreshDisabled() {
return footerRefreshDisabled;
}
/**
* Check if auto header refresh is enabled or disabled.
*
* @return true if disabled, false if enabled
* @see #setAutoHeaderRefreshDisabled(boolean)
*/
public boolean isAutoHeaderRefreshDisabled() {
return headerRefreshDisabled;
}
/**
* Gets the skipRowHoverCheck flag. If true, the CellTable will not check for
* row-level hover events (MOUSEOVER and MOUSEOUT).
*
* @return the flag value
*/
public boolean isSkipRowHoverCheck() {
return this.skipRowHoverCheck;
}
/**
* Gets the skipRowHoverFloatElementCheck flag. If true, the CellTable will
* not check for floating (fixed position) elements over the hovered row.
*
* @return the flag value
*/
public boolean isSkipRowHoverFloatElementCheck() {
return this.skipRowHoverFloatElementCheck;
}
/**
* Gets the skipRowHoverStyleUpdate flag. If true, the CellTable will not update
* the row's style on row-level hover events (MOUSEOVER and MOUSEOUT).
*
* @return the flag value
*/
public boolean isSkipRowHoverStyleUpdate() {
return this.skipRowHoverStyleUpdate;
}
/**
* Redraw the table's footers. The footers will be re-rendered synchronously.
*/
public void redrawFooters() {
createHeaders(true);
}
/**
* Redraw the table's headers. The headers will be re-rendered synchronously.
*/
public void redrawHeaders() {
createHeaders(false);
}
/**
* Remove a column.
*
* @param col the column to remove
*/
public void removeColumn(Column col) {
int index = columns.indexOf(col);
if (index < 0) {
throw new IllegalArgumentException("The specified column is not part of this table.");
}
removeColumn(index);
}
/**
* Remove a column.
*
* @param index the column index
*/
public void removeColumn(int index) {
if (index < 0 || index >= columns.size()) {
throw new IndexOutOfBoundsException("The specified column index is out of bounds.");
}
columns.remove(index);
headers.remove(index);
footers.remove(index);
// Decrement the keyboard selected column.
if (index <= keyboardSelectedColumn && keyboardSelectedColumn > 0) {
keyboardSelectedColumn--;
}
// Redraw the table asynchronously.
headersDirty = true;
refreshColumnsAndRedraw();
// We don't unsink events because other handlers or user code may have sunk
// them intentionally.
}
/**
* Remove a style from the col
element at the specified index.
*
* @param index the column index
* @param styleName the style name to remove
*/
public abstract void removeColumnStyleName(int index, String styleName);
/**
* Enable or disable auto footer refresh when row data is changed. By default,
* footers are refreshed every time the row data changes in case the headers
* depend on the current row data. If the headers do not depend on the current
* row data, you can disable this feature to improve performance.
*
*
* Note that headers will still refresh when columns are added or removed,
* regardless of whether or not this feature is enabled.
*
*/
public void setAutoFooterRefreshDisabled(boolean disabled) {
this.footerRefreshDisabled = disabled;
}
/**
* Enable or disable auto header refresh when row data is changed. By default,
* headers are refreshed every time the row data changes in case the footers
* depend on the current row data. If the footers do not depend on the current
* row data, you can disable this feature to improve performance.
*
*
* Note that footers will still refresh when columns are added or removed,
* regardless of whether or not this feature is enabled.
*
*/
public void setAutoHeaderRefreshDisabled(boolean disabled) {
this.headerRefreshDisabled = disabled;
}
/**
* Set the width of a {@link Column}. The width will persist with the column
* and takes precedence of any width set via
* {@link #setColumnWidth(int, String)}.
*
* @param column the column
* @param width the width of the column
*/
public void setColumnWidth(Column column, String width) {
columnWidths.put(column, width);
updateColumnWidthImpl(column, width);
}
/**
* Set the width of a {@link Column}. The width will persist with the column
* and takes precedence of any width set via
* {@link #setColumnWidth(int, double, Unit)}.
*
* @param column the column
* @param width the width of the column
* @param unit the {@link Unit} of measurement
*/
public void setColumnWidth(Column column, double width, Unit unit) {
setColumnWidth(column, width + unit.getType());
}
/**
* Set the width of a {@link Column}.
*
* @param column the column
* @param width the width of the column
* @param unit the {@link Unit} of measurement
*/
public void setColumnWidth(int column, double width, Unit unit) {
setColumnWidth(column, width + unit.getType());
}
/**
* Set the width of a {@link Column}.
*
* @param column the column
* @param width the width of the column
*/
public void setColumnWidth(int column, String width) {
columnWidthsByIndex.put(column, width);
maxColumnIndex = Math.max(maxColumnIndex, column);
// Update the column width.
if (column < getRealColumnCount()) {
doSetColumnWidth(column, width);
}
}
/**
* Set the widget to display when the table has no rows.
*
* @param widget the empty table widget, or null to disable
*/
public void setEmptyTableWidget(Widget widget) {
this.emptyTableWidget = widget;
}
/**
* Set the {@link HeaderBuilder} used to build the footer section of the
* table.
*/
public void setFooterBuilder(FooterBuilder builder) {
assert builder != null : "builder cannot be null";
this.footerBuilder = builder;
redrawFooters();
}
/**
* Set the {@link HeaderBuilder} used to build the header section of the
* table.
*/
public void setHeaderBuilder(HeaderBuilder builder) {
assert builder != null : "builder cannot be null";
this.headerBuilder = builder;
redrawHeaders();
}
/**
* Set the keyboard selected column index.
*
*
* If keyboard selection is disabled, this method does nothing.
*
*
*
* If the keyboard selected column is greater than the number of columns in
* the keyboard selected row, the last column in the row is selected, but the
* column index is remembered.
*
*
* @param column the column index, greater than or equal to zero
*/
public final void setKeyboardSelectedColumn(int column) {
setKeyboardSelectedColumn(column, true);
}
/**
* Set the keyboard selected column index and optionally focus on the new
* cell.
*
* @param column the column index, greater than or equal to zero
* @param stealFocus true to focus on the new column
* @see #setKeyboardSelectedColumn(int)
*/
public void setKeyboardSelectedColumn(int column, boolean stealFocus) {
assert column >= 0 : "Column must be zero or greater";
if (KeyboardSelectionPolicy.DISABLED == getKeyboardSelectionPolicy()) {
return;
}
this.keyboardSelectedColumn = column;
// Reselect the row to move the selected column.
setKeyboardSelectedRow(getKeyboardSelectedRow(), keyboardSelectedSubrow, stealFocus);
}
@Override
public void setKeyboardSelectedRow(int row, boolean stealFocus) {
setKeyboardSelectedRow(row, 0, stealFocus);
}
/**
* Set the keyboard selected row and subrow, optionally focus on the new row.
*
* @param row the row index relative to the page start
* @param subrow the row index of the child row
* @param stealFocus true to focus on the new row
* @see #setKeyboardSelectedRow(int)
*/
public void setKeyboardSelectedRow(int row, int subrow, boolean stealFocus) {
this.keyboardSelectedSubrow = subrow;
super.setKeyboardSelectedRow(row, stealFocus);
}
/**
* Set the widget to display when the data is loading.
*
* @param widget the loading indicator, or null to disable
*/
public void setLoadingIndicator(Widget widget) {
loadingIndicator = widget;
}
/**
* Sets the object used to determine how a row is styled; the change will take
* effect the next time that the table is rendered.
*
* @param rowStyles a {@link RowStyles} object
*/
public void setRowStyles(RowStyles rowStyles) {
this.rowStyles = rowStyles;
}
/**
* Sets the skipRowHoverCheck flag. If set, the CellTable will not check for
* row-level hover events (MOUSEOVER and MOUSEOUT).
*
* @param skipRowHoverCheck the new flag value
*/
public void setSkipRowHoverCheck(boolean skipRowHoverCheck) {
this.skipRowHoverCheck = skipRowHoverCheck;
}
/**
* Sets the skipRowHoverFloatElementCheck flag. If set, the CellTable will not
* not check for floating (fixed position) elements over the hovered row.
*
* @param skipRowHoverFloatElementCheck the new flag value
*/
public void setSkipRowHoverFloatElementCheck(boolean skipRowHoverFloatElementCheck) {
this.skipRowHoverFloatElementCheck = skipRowHoverFloatElementCheck;
}
/**
* Sets the skipRowHoverStyleUpdate flag. If set, the CellTable will not update
* the row's style on row-level hover events (MOUSEOVER and MOUSEOUT).
*
* @param skipRowHoverCheck the new flag value
*/
public void setSkipRowHoverStyleUpdate(boolean skipRowHoverStyleUpdate) {
this.skipRowHoverStyleUpdate = skipRowHoverStyleUpdate;
}
/**
* Specify the {@link CellTableBuilder} that will be used to render the row
* values into the table.
*/
public void setTableBuilder(CellTableBuilder tableBuilder) {
assert tableBuilder != null : "tableBuilder cannot be null";
this.tableBuilder = tableBuilder;
redraw();
}
@Override
protected Element convertToElements(SafeHtml html) {
return TABLE_IMPL.convertToSectionElement(AbstractCellTable.this, "tbody", html);
}
@Override
protected boolean dependsOnSelection() {
return dependsOnSelection;
}
/**
* Set the width of a column.
*
* @param column the column index
* @param width the width, or null to clear the width
*/
protected abstract void doSetColumnWidth(int column, String width);
/**
* Show or hide a header section.
*
* @param isFooter true for the footer, false for the header
* @param isVisible true to show, false to hide
*/
protected abstract void doSetHeaderVisible(boolean isFooter, boolean isVisible);
@Override
protected Element getChildContainer() {
return getTableBodyElement();
}
/**
* {@inheritDoc}
*
*
* The row element may not be the same as the TR element at the specified
* index if some row values are rendered with additional rows.
*
*
* @param row the row index, relative to the page start
* @return the row element, or null if it doesn't exists
* @throws IndexOutOfBoundsException if the row index is outside of the
* current page
*/
@Override
protected TableRowElement getChildElement(int row) {
return getSubRowElement(row + getPageStart(), 0);
}
@Override
protected Element getKeyboardSelectedElement() {
return getKeyboardSelectedElement(getKeyboardSelectedTableCellElement());
}
/**
* Get the real column count, which is the greater of the number of Columns or
* the maximum index of a column with a defined column width.
*/
protected int getRealColumnCount() {
return Math.max(getColumnCount(), maxColumnIndex + 1);
}
/**
* Get the tbody element that contains the render row values.
*/
protected abstract TableSectionElement getTableBodyElement();
/**
* Get the tfoot element that contains the footers.
*/
protected abstract TableSectionElement getTableFootElement();
/**
* Get the thead element that contains the headers.
*/
protected abstract TableSectionElement getTableHeadElement();
@Override
protected boolean isKeyboardNavigationSuppressed() {
return cellIsEditing;
}
@Override
protected void onBlur() {
TableCellElement td = getKeyboardSelectedTableCellElement();
if (td != null) {
TableRowElement tr = td.getParentElement().cast();
td.removeClassName(style.keyboardSelectedCell());
setRowStyleName(tr, style.keyboardSelectedRow(), style.keyboardSelectedRowCell(), false);
}
}
@SuppressWarnings("deprecation")
@Override
protected void onBrowserEvent2(Event event) {
// Get the event target.
EventTarget eventTarget = event.getEventTarget();
if (!Element.is(eventTarget)) {
return;
}
final Element target = event.getEventTarget().cast();
// Find the cell where the event occurred.
TableSectionElement tbody = getTableBodyElement();
TableSectionElement tfoot = getTableFootElement();
TableSectionElement thead = getTableHeadElement();
TableSectionElement targetTableSection = null;
TableCellElement targetTableCell = null;
Element cellParent = null;
Element headerParent = null; // Header in the headerBuilder.
Element headerColumnParent = null; // Column in the headerBuilder.
Element footerParent = null; // Header in the footerBuilder.
Element footerColumnParent = null; // Column in the footerBuilder.
{
Element maybeTableCell = null;
Element cur = target;
/*
* If an event happens in the TD element but outside the cell's div, we want
* to handle it as if it happened within the table cell.
*/
if (TableCellElement.TAG_TD.equalsIgnoreCase(cur.getTagName()) &&
tableBuilder.isColumn(cur.getFirstChildElement())) {
cur = cur.getFirstChildElement();
}
while (cur != null && targetTableSection == null) {
/*
* Found the table section. Return the most recent cell element that we
* discovered.
*/
if (cur == tbody || cur == tfoot || cur == thead) {
targetTableSection = cur.cast(); // We found the table section.
if (maybeTableCell != null) {
targetTableCell = maybeTableCell.cast();
break;
}
}
// Look for a table cell.
String tagName = cur.getTagName();
if (TableCellElement.TAG_TD.equalsIgnoreCase(tagName)
|| TableCellElement.TAG_TH.equalsIgnoreCase(tagName)) {
/*
* Found a table cell, but we can't return yet because it may be part
* of a sub table within the a CellTable cell.
*/
maybeTableCell = cur;
}
// Look for the most immediate cell parent if not already found.
if (cellParent == null && tableBuilder.isColumn(cur)) {
cellParent = cur;
}
/*
* Look for the most immediate header parent if not already found. Its
* possible that the footer or header will mistakenly identify a header
* from the other section, so we remember both. When we eventually reach
* the target table section element, we'll know for sure if its a header
* of footer.
*/
if (headerParent == null && headerBuilder.isHeader(cur)) {
headerParent = cur;
}
if (footerParent == null && footerBuilder.isHeader(cur)) {
footerParent = cur;
}
// Look for the most immediate column parent if not already found.
if (headerColumnParent == null && headerBuilder.isColumn(cur)) {
headerColumnParent = cur;
}
if (footerColumnParent == null && footerBuilder.isColumn(cur)) {
footerColumnParent = cur;
}
// Iterate.
cur = cur.getParentElement();
}
}
if (targetTableCell == null) {
return;
}
// Support the legacy mode where the div inside of the TD is the cell
// parent.
if (legacyRenderRowValues) {
cellParent = targetTableCell.getFirstChildElement();
}
/*
* Forward the event to the associated header, footer, or column.
*/
TableRowElement targetTableRow = targetTableCell.getParentElement().cast();
String eventType = event.getType();
boolean isSelect = BrowserEvents.CLICK.equals(eventType)
|| (BrowserEvents.KEYDOWN.equals(eventType) && event.getKeyCode() == KeyCodes.KEY_ENTER);
int col = targetTableCell.getCellIndex();
if (targetTableSection == thead || targetTableSection == tfoot) {
boolean isHeader = (targetTableSection == thead);
headerParent = isHeader ? headerParent : footerParent;
Element columnParent = isHeader ? headerColumnParent : footerColumnParent;
boolean shouldSortColumn = true;
// Fire the event to the header.
if (headerParent != null) {
Header header =
isHeader ? headerBuilder.getHeader(headerParent) : footerBuilder
.getHeader(footerParent);
if (header != null) {
int headerIndex = isHeader ? headerBuilder.getRowIndex(targetTableRow) :
footerBuilder.getRowIndex(targetTableRow);
Context context = new Context(headerIndex, col, header.getKey());
if (cellConsumesEventType(header.getCell(), eventType)) {
header.onBrowserEvent(context, headerParent, event);
}
if (isSelect) {
// Preview the event, and possibily disable the column sort event. The event preview is
// forced even if the header cell does not consume click event
shouldSortColumn = header.onPreviewColumnSortEvent(context, headerParent, event);
}
}
}
// Sort the header.
if (isSelect && shouldSortColumn && columnParent != null) {
Column column =
isHeader ? headerBuilder.getColumn(columnParent) : footerBuilder
.getColumn(columnParent);
if (column != null && column.isSortable()) {
/*
* Force the headers to refresh the next time data is pushed so we
* update the sort icon in the header.
*/
headersDirty = true;
updatingSortList = true;
sortList.push(column);
updatingSortList = false;
ColumnSortEvent.fire(this, sortList);
}
}
} else if (targetTableSection == tbody) {
/*
* Get the row index of the data value. This may not correspond to the DOM
* row index if the user specifies multiple table rows per row object.
*/
int absRow = tableBuilder.getRowValueIndex(targetTableRow);
int relRow = absRow - getPageStart();
int subrow = tableBuilder.getSubrowValueIndex(targetTableRow);
if (!skipRowHoverCheck) {
boolean isRowChange = hoveringRow != targetTableRow;
if (BrowserEvents.MOUSEOVER.equals(eventType)) {
// Unstyle the old row if it is still part of the table.
if (hoveringRow != null && getTableBodyElement().isOrHasChild(hoveringRow)) {
setRowHover(hoveringRow, event, false, isRowChange);
}
hoveringRow = targetTableRow;
setRowHover(hoveringRow, event, true, isRowChange);
} else if (BrowserEvents.MOUSEOUT.equals(eventType) && hoveringRow != null) {
boolean unhover = true;
if (!skipRowHoverFloatElementCheck) {
// Ignore events happening directly over the hovering row. If there are floating element
// on top of the row, mouseout event should not be triggered. This is to avoid the flickring
// effect if the floating element is shown/hide based on hover event.
int clientX = event.getClientX() + Window.getScrollLeft();
int clientY = event.getClientY() + Window.getScrollTop();
int rowLeft = hoveringRow.getAbsoluteLeft();
int rowTop = hoveringRow.getAbsoluteTop();
int rowWidth = hoveringRow.getOffsetWidth();
int rowHeight = hoveringRow.getOffsetHeight();
int rowBottom = rowTop + rowHeight;
int rowRight = rowLeft + rowWidth;
unhover = clientX < rowLeft || clientX > rowRight || clientY < rowTop || clientY > rowBottom;
}
if (unhover) {
setRowHover(hoveringRow, event, false, isRowChange);
hoveringRow = null;
}
}
}
// If the event causes us to page, then the physical index will be out
// of bounds of the underlying data.
if (!isRowWithinBounds(relRow)) {
return;
}
/*
* Fire a preview event. The preview event is fired even if the TD does
* not contain a cell so the selection handler and keyboard handler have a
* chance to act.
*/
boolean isSelectionHandled =
handlesSelection
|| KeyboardSelectionPolicy.BOUND_TO_SELECTION == getKeyboardSelectionPolicy();
T value = getVisibleItem(relRow);
/*
* Create a new context based on the dom column index instead of using the
* user provided one from TableBuilder. We trigger cell preview events for
* table cells even if there is no associated Cell instance. If we used
* the user provided context, we could get inconsistent states where the
* Context is sometimes user provided and sometimes generated based on the
* DOM column index.
*/
Context context = new Context(absRow, col, getValueKey(value), subrow);
CellPreviewEvent previewEvent =
CellPreviewEvent.fire(this, event, this, context, value, cellIsEditing,
isSelectionHandled);
// Pass the event to the cell.
if (cellParent != null && !previewEvent.isCanceled()) {
HasCell column;
if (legacyRenderRowValues) {
column = columns.get(col);
} else {
column = tableBuilder.getColumn(context, value, cellParent);
}
if (column != null) {
fireEventToCell(event, eventType, cellParent, value, context, column);
}
}
}
}
@Override
protected void onFocus() {
TableCellElement td = getKeyboardSelectedTableCellElement();
if (td != null) {
TableRowElement tr = td.getParentElement().cast();
td.addClassName(style.keyboardSelectedCell());
setRowStyleName(tr, style.keyboardSelectedRow(), style.keyboardSelectedRowCell(), true);
}
}
protected void refreshColumnWidths() {
int columnCount = getRealColumnCount();
for (int i = 0; i < columnCount; i++) {
doSetColumnWidth(i, getColumnWidth(i));
}
}
/**
* Throws an {@link UnsupportedOperationException}.
*
* @deprecated as of GWT 2.5, use a {@link CellTableBuilder} to customize the
* table structure instead
* @see #renderRowValuesLegacy(SafeHtmlBuilder, List, int, SelectionModel)
*/
@Override
@Deprecated
protected void renderRowValues(SafeHtmlBuilder sb, List values, int start,
SelectionModel selectionModel) {
legacyRenderRowValues = false;
throw new UnsupportedOperationException();
}
/**
* Render all row values into the specified {@link SafeHtmlBuilder}.
*
*
* This method is here for legacy reasons, to support subclasses that call
* {@link #renderRowValues(SafeHtmlBuilder, List, int, SelectionModel)}.
*
*
* @param sb the {@link SafeHtmlBuilder} to render into
* @param values the row values
* @param start the absolute start index of the values
* @param selectionModel the {@link SelectionModel}
* @deprecated as of GWT 2.5, use a {@link CellTableBuilder} to customize the
* table structure instead
*/
@Deprecated
protected final void renderRowValuesLegacy(SafeHtmlBuilder sb, List values, int start,
SelectionModel selectionModel) {
int keyboardSelectedRow = getKeyboardSelectedRow() + getPageStart();
String evenRowStyle = style.evenRow();
String oddRowStyle = style.oddRow();
String cellStyle = style.cell();
String evenCellStyle = " " + style.evenRowCell();
String oddCellStyle = " " + style.oddRowCell();
String firstColumnStyle = " " + style.firstColumn();
String lastColumnStyle = " " + style.lastColumn();
String selectedRowStyle = " " + style.selectedRow();
String selectedCellStyle = " " + style.selectedRowCell();
int columnCount = columns.size();
int length = values.size();
int end = start + length;
for (int i = start; i < end; i++) {
T value = values.get(i - start);
boolean isSelected =
(selectionModel == null || value == null) ? false : selectionModel.isSelected(value);
boolean isEven = i % 2 == 0;
String trClasses = isEven ? evenRowStyle : oddRowStyle;
if (isSelected) {
trClasses += selectedRowStyle;
}
if (rowStyles != null) {
String extraRowStyles = rowStyles.getStyleNames(value, i);
if (extraRowStyles != null) {
trClasses += " ";
trClasses += extraRowStyles;
}
}
SafeHtmlBuilder trBuilder = new SafeHtmlBuilder();
int curColumn = 0;
for (Column column : columns) {
String tdClasses = cellStyle;
tdClasses += isEven ? evenCellStyle : oddCellStyle;
if (curColumn == 0) {
tdClasses += firstColumnStyle;
}
if (isSelected) {
tdClasses += selectedCellStyle;
}
// The first and last column could be the same column.
if (curColumn == columnCount - 1) {
tdClasses += lastColumnStyle;
}
// Add class names specific to the cell.
Context context = new Context(i, curColumn, getValueKey(value));
String cellStyles = column.getCellStyleNames(context, value);
if (cellStyles != null) {
tdClasses += " " + cellStyles;
}
SafeHtmlBuilder cellBuilder = new SafeHtmlBuilder();
if (value != null) {
column.render(context, value, cellBuilder);
}
// Build the contents.
SafeHtml contents = SafeHtmlUtils.EMPTY_SAFE_HTML;
contents = template.div(cellBuilder.toSafeHtml());
// Build the cell.
HorizontalAlignmentConstant hAlign = column.getHorizontalAlignment();
VerticalAlignmentConstant vAlign = column.getVerticalAlignment();
if (hAlign != null && vAlign != null) {
trBuilder.append(template.tdBothAlign(tdClasses, hAlign.getTextAlignString(), vAlign
.getVerticalAlignString(), contents));
} else if (hAlign != null) {
trBuilder.append(template.tdHorizontalAlign(tdClasses, hAlign.getTextAlignString(),
contents));
} else if (vAlign != null) {
trBuilder.append(template.tdVerticalAlign(tdClasses, vAlign.getVerticalAlignString(),
contents));
} else {
trBuilder.append(template.td(tdClasses, contents));
}
curColumn++;
}
sb.append(template.tr(trClasses, trBuilder.toSafeHtml()));
}
}
@Override
protected void replaceAllChildren(List values, SafeHtml html) {
refreshHeadersAndColumnsImpl();
/*
* If html is not null, then the user overrode renderRowValues() and
* rendered directly into a SafeHtmlBuilder. The legacy method is deprecated
* but still supported.
*/
if (html == null) {
html = buildRowValues(values, getPageStart(), true);
}
TABLE_IMPL.replaceAllRows(this, getTableBodyElement(), CellBasedWidgetImpl.get().processHtml(
html));
}
@SuppressWarnings("deprecation")
@Override
protected void replaceChildren(List values, int start, SafeHtml html) {
refreshHeadersAndColumnsImpl();
/*
* If html is not null, then the user override renderRowValues() and
* rendered directly into a SafeHtmlBuilder. The legacy method is deprecated
* but still supported.
*/
if (html == null) {
html = buildRowValues(values, getPageStart() + start, false);
}
TABLE_IMPL.replaceChildren(this, getTableBodyElement(), CellBasedWidgetImpl.get().processHtml(
html), start, values.size());
}
@Override
protected boolean resetFocusOnCell() {
Element elem = getKeyboardSelectedElement();
if (elem == null) {
// There is no selected element.
return false;
}
int row = getKeyboardSelectedRow();
int col = getKeyboardSelectedColumn();
T value = getVisibleItem(row);
Object key = getValueKey(value);
// TODO(pengzhuang): this doesn't support sub row selection?
Context context = new Context(row + getPageStart(), col, key);
HasCell column = tableBuilder.getColumn(context, value, elem);
if (column == null) {
// The selected element does not contain a Cell.
return false;
}
resetFocusOnCellImpl(context, value, column, elem);
return false;
}
@Override
protected void setKeyboardSelected(int index, boolean selected, boolean stealFocus) {
if (KeyboardSelectionPolicy.DISABLED == getKeyboardSelectionPolicy()
|| !isRowWithinBounds(index)) {
return;
}
// If deselecting, we deselect the previous subrow.
int subrow = lastKeyboardSelectedSubrow;
if (selected) {
subrow = keyboardSelectedSubrow;
lastKeyboardSelectedSubrow = keyboardSelectedSubrow;
}
// Deselect the row.
TableRowElement tr = getSubRowElement(index + getPageStart(), subrow);
if (tr == null) {
// The row does not exist.
return;
}
String cellStyle = style.keyboardSelectedCell();
boolean updatedSelection = !selected || isFocused || stealFocus;
setRowStyleName(tr, style.keyboardSelectedRow(), style.keyboardSelectedRowCell(), selected);
NodeList cells = tr.getCells();
int keyboardColumn = Math.min(getKeyboardSelectedColumn(), cells.getLength() - 1);
for (int i = 0; i < cells.getLength(); i++) {
TableCellElement td = cells.getItem(i);
boolean isKeyboardSelected = (i == keyboardColumn);
// Update the selected style.
setStyleName(td, cellStyle, updatedSelection && selected && isKeyboardSelected);
// Mark as focusable.
final Element focusable = getKeyboardSelectedElement(td);
setFocusable(focusable, selected && isKeyboardSelected);
// Move focus to the cell.
if (selected && stealFocus && !cellIsEditing && isKeyboardSelected) {
CellBasedWidgetImpl.get().resetFocus(new Scheduler.ScheduledCommand() {
@Override
public void execute() {
focusable.focus();
}
});
}
}
}
/**
* Get the column width. Associating a width with a {@link Column} takes
* precedence over setting the width of a column index.
*
* @param columnIndex the column index
* @return the column width, or null if none specified
*/
String getColumnWidth(int columnIndex) {
String width = null;
if (columns.size() > columnIndex) {
// Look for the width by Column.
width = columnWidths.get(columns.get(columnIndex));
}
if (width == null) {
// Look for the width by index.
width = columnWidthsByIndex.get(columnIndex);
}
return width;
}
/**
* Get a subrow element given the index of the row value and the sub row
* index.
*
* @param absRow the absolute row value index
* @param subrow the index of the subrow beneath the row.
* @return the row element, or null if not found
*/
// Visible for testing.
TableRowElement getSubRowElement(int absRow, int subrow) {
int relRow = absRow - getPageStart();
checkRowBounds(relRow);
/*
* In most tables, the row element that represents the row object at the
* specified index will be at the same index in the DOM. However, if the
* user provides a TableBuilder that renders multiple rows per row value,
* that will not be the case.
*
* We use a binary search to find the row, but we start at the index as that
* is the most likely location.
*/
NodeList rows = getTableBodyElement().getRows();
int rowCount = rows.getLength();
if (rowCount == 0) {
return null;
}
int frameStart = 0;
int frameEnd = rowCount - 1;
int domIndex = Math.min(relRow, frameEnd);
while (domIndex >= frameStart && domIndex <= frameEnd) {
TableRowElement curRow = rows.getItem(domIndex);
int rowValueIndex = tableBuilder.getRowValueIndex(curRow);
if (rowValueIndex == absRow) {
// Found a subrow in the row index.
int subrowValueIndex = tableBuilder.getSubrowValueIndex(curRow);
if (subrow != subrowValueIndex) {
// Shift to the correct subrow.
int offset = subrow - subrowValueIndex;
int subrowIndex = domIndex + offset;
if (subrowIndex >= rows.getLength()) {
// The subrow is out of range of the table.
return null;
}
curRow = rows.getItem(subrowIndex);
if (tableBuilder.getRowValueIndex(curRow) != absRow) {
// The "subrow" is actually part of the next row.
return null;
}
}
return curRow;
} else if (rowValueIndex > absRow) {
// Shift the frame to lower indexes.
frameEnd = domIndex - 1;
} else {
// Shift the frame to higher indexes.
frameStart = domIndex + 1;
}
// Move the dom index.
domIndex = (frameStart + frameEnd) / 2;
}
// The element wasn't found.
return null;
}
/**
* Build a list of row values.
*
* @param values the row values to render
* @param start the absolute start index
* @param isRebuildingAllRows is this going to rebuild all rows
* @return a {@link SafeHtml} string containing the row values
*/
private SafeHtml buildRowValues(List values, int start, boolean isRebuildingAllRows) {
int length = values.size();
int end = start + length;
tableBuilder.start(isRebuildingAllRows);
for (int i = start; i < end; i++) {
T value = values.get(i - start);
tableBuilder.buildRow(value, i);
}
// Update the properties of the table.
coalesceCellProperties();
TableSectionBuilder tableSectionBuilder = tableBuilder.finish();
return tableSectionToSafeHtml(tableSectionBuilder, "tbody");
}
/**
* Check that the specified column is within bounds.
*
* @param col the column index
* @throws IndexOutOfBoundsException if the column is out of bounds
*/
private void checkColumnBounds(int col) {
if (col < 0 || col >= getColumnCount()) {
throw new IndexOutOfBoundsException("Column index is out of bounds: " + col);
}
}
/**
* Coalesce the various cell properties (dependsOnSelection, handlesSelection,
* isInteractive) into a table policy.
*/
private void coalesceCellProperties() {
dependsOnSelection = false;
handlesSelection = false;
isInteractive = false;
for (HasCell column : tableBuilder.getColumns()) {
Cell cell = column.getCell();
if (cell.dependsOnSelection()) {
dependsOnSelection = true;
}
if (cell.handlesSelection()) {
handlesSelection = true;
}
if (isColumnInteractive(column)) {
isInteractive = true;
}
}
}
/**
* Render the header or footer.
*
* @param isFooter true if this is the footer table, false if the header table
*/
private void createHeaders(boolean isFooter) {
TableSectionBuilder section =
isFooter ? footerBuilder.buildFooter() : headerBuilder.buildHeader();
if (section != null) {
TABLE_IMPL.replaceAllRows(this, isFooter ? getTableFootElement() : getTableHeadElement(),
tableSectionToSafeHtml(section, isFooter ? "tfoot" : "thead"));
doSetHeaderVisible(isFooter, true);
} else {
// If the section isn't used, hide it.
doSetHeaderVisible(isFooter, false);
}
}
/**
* Fire an event to the Cell within the specified {@link TableCellElement}.
*/
private void fireEventToCell(Event event, String eventType, Element parentElem,
final T rowValue, Context context, HasCell column) {
// Check if the cell consumes the event.
Cell cell = column.getCell();
if (!cellConsumesEventType(cell, eventType)) {
return;
}
C cellValue = column.getValue(rowValue);
boolean cellWasEditing = cell.isEditing(context, parentElem, cellValue);
if (column instanceof Column) {
/*
* If the HasCell is a Column, let it handle the event itself. This is
* here for legacy support.
*/
Column col = (Column) column;
col.onBrowserEvent(context, parentElem, rowValue, event);
} else {
// Create a FieldUpdater.
final FieldUpdater fieldUpdater = column.getFieldUpdater();
final int index = context.getIndex();
ValueUpdater valueUpdater = (fieldUpdater == null) ? null : new ValueUpdater() {
@Override
public void update(C value) {
fieldUpdater.update(index, rowValue, value);
}
};
// Fire the event to the cell.
cell.onBrowserEvent(context, parentElem, cellValue, event, valueUpdater);
}
// Reset focus if needed.
cellIsEditing = cell.isEditing(context, parentElem, cellValue);
if (cellWasEditing && !cellIsEditing) {
CellBasedWidgetImpl.get().resetFocus(new Scheduler.ScheduledCommand() {
@Override
public void execute() {
setFocus(true);
}
});
}
}
/**
* Get the keyboard selected element from the selected table cell.
*
* @return the keyboard selected element, or null if there is none
*/
private Element getKeyboardSelectedElement(TableCellElement td) {
if (td == null) {
return null;
}
/*
* The TD itself is a cell parent, which means its internal structure
* (including the tabIndex that we set) could be modified by its Cell. We
* return the TD to be safe.
*/
if (tableBuilder.isColumn(td)) {
return td;
}
/*
* The default table builder adds a focusable div to the table cell because
* TDs aren't focusable in all browsers. If the user defines a custom table
* builder with a different structure, we must assume the keyboard selected
* element is the TD itself.
*/
Element firstChild = td.getFirstChildElement();
if (firstChild != null && td.getChildCount() == 1
&& "div".equalsIgnoreCase(firstChild.getTagName())) {
return firstChild;
}
return td;
}
/**
* Get the {@link TableCellElement} that is currently keyboard selected.
*
* @return the table cell element, or null if not selected
*/
private TableCellElement getKeyboardSelectedTableCellElement() {
int colIndex = getKeyboardSelectedColumn();
if (colIndex < 0) {
return null;
}
// Do not use getRowElement() because that will flush the presenter.
int rowIndex = getKeyboardSelectedRow();
if (rowIndex < 0 || rowIndex >= getTableBodyElement().getRows().getLength()) {
return null;
}
TableRowElement tr = getSubRowElement(rowIndex + getPageStart(), keyboardSelectedSubrow);
if (tr != null) {
int cellCount = tr.getCells().getLength();
if (cellCount > 0) {
int column = Math.min(colIndex, cellCount - 1);
return tr.getCells().getItem(column);
}
}
return null;
}
/**
* Initialize the widget.
*/
private void init() {
if (TABLE_IMPL == null) {
TABLE_IMPL = GWT.create(Impl.class);
}
if (template == null) {
template = GWT.create(Template.class);
}
// Sink events.
Set eventTypes = new HashSet();
eventTypes.add(BrowserEvents.MOUSEOVER);
eventTypes.add(BrowserEvents.MOUSEOUT);
CellBasedWidgetImpl.get().sinkEvents(this, eventTypes);
// Set the table builder.
tableBuilder = new DefaultCellTableBuilder(this);
headerBuilder = new DefaultHeaderOrFooterBuilder(this, false);
footerBuilder = new DefaultHeaderOrFooterBuilder(this, true);
// Set the keyboard handler.
setKeyboardSelectionHandler(new CellTableKeyboardSelectionHandler(this));
}
/**
* Mark the column widths as dirty and redraw the table.
*/
private void refreshColumnsAndRedraw() {
columnWidthsDirty = true;
redraw();
}
/**
* Refresh the headers and column widths.
*/
private void refreshHeadersAndColumnsImpl() {
// Refresh the column widths if needed.
if (columnWidthsDirty) {
columnWidthsDirty = false;
refreshColumnWidths();
}
// Render the headers and footers.
boolean wereHeadersDirty = headersDirty;
headersDirty = false;
if (wereHeadersDirty || !headerRefreshDisabled) {
createHeaders(false);
}
if (wereHeadersDirty || !footerRefreshDisabled) {
createHeaders(true);
}
}
private boolean resetFocusOnCellImpl(Context context, T value, HasCell column,
Element cellParent) {
C cellValue = column.getValue(value);
Cell cell = column.getCell();
return cell.resetFocus(context, cellParent, cellValue);
}
/**
* Set a row's hovering style and fire a {@link RowHoverEvent}
*
* @param tr the row element
* @param event the original event
* @param isHovering false if this is an unhover event
* @param isRowChange true if the hover event is a full row change, false if it is a hover on a
* cell. Row style update is called only on full row change.
*/
private void setRowHover(TableRowElement tr, Event event, boolean isHovering,
boolean isRowChange) {
if (!skipRowHoverStyleUpdate) {
setRowStyleName(tr, style.hoveredRow(), style.hoveredRowCell(), isHovering);
}
RowHoverEvent.fire(this, tr, event, !isHovering,
isRowChange ? RowHoverEvent.HoveringScope.ROW_HOVER
: RowHoverEvent.HoveringScope.CELL_HOVER);
}
/**
* Apply a style to a row and all cells in the row.
*
* @param tr the row element
* @param rowStyle the style to apply to the row
* @param cellStyle the style to apply to the cells
* @param add true to add the style, false to remove
*/
private void setRowStyleName(TableRowElement tr, String rowStyle, String cellStyle, boolean add) {
setStyleName(tr, rowStyle, add);
NodeList cells = tr.getCells();
for (int i = 0; i < cells.getLength(); i++) {
setStyleName(cells.getItem(i), cellStyle, add);
}
}
/**
* Update the width of all instances of the specified column. A column
* instance may appear multiple times in the table.
*
* @param column the column to update
* @param width the width of the column, or null to clear the width
*/
private void updateColumnWidthImpl(Column column, String width) {
int columnCount = getColumnCount();
for (int i = 0; i < columnCount; i++) {
if (columns.get(i) == column) {
doSetColumnWidth(i, width);
}
}
}
}