Java tutorial
/* * Copyright 2008 Sander Berents * * 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 org.gwtlib.client.table.ui; import java.util.ArrayList; import java.util.List; import org.gwtlib.client.table.ColumnLayout; import org.gwtlib.client.table.ContentProvider; import org.gwtlib.client.table.Row; import org.gwtlib.client.table.Rows; import org.gwtlib.client.table.RowsCache; import org.gwtlib.client.ui.Messages; import com.google.gwt.core.client.GWT; import com.google.gwt.event.dom.client.ChangeEvent; import com.google.gwt.event.dom.client.ChangeHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.HasChangeHandlers; import com.google.gwt.event.dom.client.HasClickHandlers; import com.google.gwt.event.logical.shared.ResizeEvent; import com.google.gwt.event.logical.shared.ResizeHandler; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.DeferredCommand; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.FlexTable; import com.google.gwt.user.client.ui.HTMLTable; import com.google.gwt.user.client.ui.HasVerticalAlignment; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.ScrollPanel; import com.google.gwt.user.client.ui.Widget; /** * Table. * * CSS Style Rules: * <ul> * <li>.gwtlib-Table { the table itself }</li> * <li>.gwtlib-Table-empty { no data message }</li> * <li>.gwtlib-Table-error { error message }</li> * <li>.gwtlib-Header { the table header cells }</li> * <li>.gwtlib-Header-sortable { the table header cell for a sortable column }</li> * <li>.gwtlib-Column-ascending { the table header cell for a ascending sorted column }</li> * <li>.gwtlib-Column-descending { the table header cell for a descending sorted column }</li> * <li>.gwtlib-Row { a row }</li> * <li>.gwtlib-Row-even { an even row }</li> * <li>.gwtlib-Row-odd { an odd row }</li> * <li>.gwtlib-Row-empty { a row without data (when there is less data than table rows) }</li> * <li>.gwtlib-Row-selected { a row that has the <code>Row.State.SELECT</code> state set }</li> * </ul> * * @author Sander Berents */ public class Table extends AbstractComposite implements SourcesTableEvents { private static final String STYLE = "gwtlib-Table"; private static final String STYLE_HEADER_ROW = "gwtlib-Header-Row"; private static final String STYLE_HEADER = "gwtlib-Header"; private static final String STYLE_SORTABLE = "gwtlib-Header-sortable"; private static final String STYLE_ASCENDING = "gwtlib-Column-ascending"; private static final String STYLE_DESCENDING = "gwtlib-Column-descending"; private static final String STYLE_ROW = "gwtlib-Row"; private static final String STYLE_ROW_EVEN = "gwtlib-Row-even"; private static final String STYLE_ROW_ODD = "gwtlib-Row-odd"; private static final String STYLE_ROW_EMPTY = "gwtlib-Row-empty"; private static final String STYLE_ROW_SELECT = "gwtlib-Row-selected"; private static final String STYLE_COLUMN_SELECT = "gwtlib-Column-selected"; private static final String STYLE_CELL = "gwtlib-Cell"; private static final String STYLE_CELL_SELECT = "gwtlib-Cell-selected"; private static final String STYLE_NO_DATA = "gwtlib-Table-empty"; private static final String STYLE_ERROR = "gwtlib-Table-error"; public interface Style { public static final int SINGLE_SELECT = 1 << 0; public static final int MULTI_SELECT = 1 << 1; }; protected FlexTable _panel; protected FlexTable _table; protected ScrollPanel _scroll; protected ColumnLayout _layout; protected ContentProvider _provider; protected RowsCache _cache; protected int _begin = 0; protected int _size = 10; protected int _minsize = _size; protected List<TableListener> _listeners; protected Messages _messages = (Messages) GWT.create(Messages.class); public Table(ColumnLayout layout) { this(layout, true); } @Deprecated public Table(Messages messages, ColumnLayout layout) { this(layout, true); _messages = messages; } protected Table(ColumnLayout layout, boolean initWidget) { super(Style.SINGLE_SELECT); _layout = layout; _cache = new RowsCache(); _listeners = new ArrayList<TableListener>(); _table = new FlexTable(); _table.setCellSpacing(0); _table.setCellSpacing(0); _table.setSize("100%", "auto"); _scroll = new ScrollPanel(_table); _panel = new FlexTable(); _panel.setCellSpacing(0); _panel.setCellPadding(0); _panel.setWidget(0, 0, _scroll); _panel.getRowFormatter().setVerticalAlign(0, HasVerticalAlignment.ALIGN_TOP); _panel.getFlexCellFormatter().addStyleName(0, 0, "scroll-cell"); if (initWidget) initWidget(_panel); _table.setStylePrimaryName(STYLE); initOptimalSize(); Window.addResizeHandler(new ResizeHandler() { @Override public void onResize(ResizeEvent event) { initOptimalSize(); } }); _table.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { HTMLTable.Cell cell = _table.getCellForEvent(event); if (cell != null) { int row = cell.getRowIndex(); int col = cell.getCellIndex(); GWT.log("onCellClicked " + row + "," + col, null); fireCellClickedEvent(row, col); int c = toActualColumnPos(col); Column column = _layout.getColumn(c); if (row == 0) { if (column.isSortable()) { sort(c, column.getSortDirection() != Column.Sort.ASCENDING); } } else { Row r = _cache.getRow(_begin + row - 1); if (r != null) { fireCellClickedEvent(r, column); fireRowClickedEvent(r); } } } } }); } protected void initOptimalSize() { _scroll.setVisible(false); DeferredCommand.addCommand(new Command() { public void execute() { Element e = _panel.getCellFormatter().getElement(0, 0); int w = DOM.getElementPropertyInt(e, "offsetWidth"); int h = DOM.getElementPropertyInt(e, "offsetHeight"); GWT.log("Initial table size is " + w + "," + h, null); if (w == 0 || h == 0) { _scroll.setVisible(true); // Skip all this if it is used inside a TabPanel } else { w -= 2; if (w < 0) w = 0; h -= 2; if (h < 0) h = 0; _scroll.setSize("" + w + "px", "" + h + "px"); _scroll.setVisible(true); w = DOM.getElementPropertyInt(e, "offsetWidth"); h = DOM.getElementPropertyInt(e, "offsetHeight"); GWT.log("Now table size is " + w + "," + h, null); } } }); } /** * Sets the content provider. This is required. * @param provider */ public void setContentProvider(ContentProvider provider) { _provider = provider; } /** * Sets the number of rows to display in the table and redraws it. * @param size */ public void setSize(int size) { clear(); _size = size; update(); } /** * Sets the minimal number of rows to display in the table. * @param size */ public void setMinimumSize(int size) { _minsize = size; } /** * Sets the top row offset and redraws the table. * @param pos Zero based position. */ public void setPosition(int pos) { _begin = pos; update(); } public void refreshRowState() { Rows rows = getRows(_begin); int r = 1; for (int i = 0; i < rows.size(); ++i, ++r) { Row row = rows.getRow(i); _table.getRowFormatter().setStyleName(r, STYLE_ROW); _table.getRowFormatter().addStyleName(r, i % 2 == 0 ? STYLE_ROW_EVEN : STYLE_ROW_ODD); if (row.hasState(Row.State.SELECT)) _table.getRowFormatter().addStyleName(r, STYLE_ROW_SELECT); for (int j = 0, c = 0; j < _layout.getTotalColumnCount(); ++j) { Column column = _layout.getColumn(j); if (column.isVisible()) { _table.getCellFormatter().removeStyleName(r, c, STYLE_COLUMN_SELECT); _table.getCellFormatter().removeStyleName(r, c, STYLE_CELL_SELECT); if (column.hasState(Column.State.SELECT)) { _table.getCellFormatter().addStyleName(r, c, STYLE_COLUMN_SELECT); if (row.hasState(Row.State.SELECT)) _table.getCellFormatter().addStyleName(r, c, STYLE_CELL_SELECT); } c++; } } } int nclear = Math.min(_size - rows.size(), _table.getRowCount() - rows.size() - 1); while (nclear-- > 0) { for (int j = 0, c = 0; j < _layout.getTotalColumnCount(); ++j) { Column column = _layout.getColumn(j); if (column.isVisible()) { _table.getCellFormatter().removeStyleName(r, c, STYLE_COLUMN_SELECT); _table.getCellFormatter().removeStyleName(r, c, STYLE_CELL_SELECT); _table.getRowFormatter().setStyleName(r, STYLE_ROW); _table.getRowFormatter().addStyleName(r, STYLE_ROW_EMPTY); _table.clearCell(r, c++); } } ++r; } } /** * Redraws the table. */ public void update() { fetch(_begin, _begin + _size); } /** * Clears the row cache, resets the current position to zero, fetches * up to <code>size</code> rows of data and redraws the table. */ public void reset() { _begin = 0; _cache.clear(); update(); } /** * Clears the row cache, resets the current position to zero. */ public void clear() { _begin = 0; _cache.clear(); while (_table.getRowCount() > 0) _table.removeRow(0); renderHeader(); render(0); } protected void fetch(int begin, int end) { int sortId = -1; boolean ascending = false; int sortColumn = _layout.getSortColumn(); if (sortColumn >= 0) { sortId = _layout.getColumn(sortColumn).getId(); ascending = _layout.getColumn(sortColumn).getSortDirection() == Column.Sort.ASCENDING; } Rows rows = _cache.getRows(begin, end, sortId, ascending); if (rows.size() == end - begin) { onSuccess(rows); } else { fireLoadEvent(); _provider.load(begin, end, sortId, ascending); } } public void show(int column, boolean visible) { Column col = _layout.getColumn(column); if (fireShowColumnEvent(col, visible)) { col.setVisible(visible); } } public void sort(int column, boolean ascending) { Column col = _layout.getColumn(column); if (fireSortColumnEvent(col, ascending)) { int sortColumn = _layout.getSortColumn(); if (sortColumn != -1) { setColumnSortStyle(toVisibleColumnPos(sortColumn), Column.Sort.NONE); _layout.getColumn(sortColumn).setSortDirection(Column.Sort.NONE); } int dir = ascending ? Column.Sort.ASCENDING : Column.Sort.DESCENDING; _layout.getColumn(column).setSortDirection(dir); _begin = 0; fetch(_begin, _size); } } /** * Returns the column layout information. * @return ColumnLayout object. */ public ColumnLayout getColumnLayout() { return _layout; } /** * Returns currently displayed rows. * @return Rows object. */ public Rows getRows() { return getRows(_begin); } protected void renderHeader() { _table.getRowFormatter().setStylePrimaryName(0, STYLE_HEADER_ROW); for (int i = 0, c = 0; i < _layout.getTotalColumnCount(); ++i) { Column column = _layout.getColumn(i); if (column.isVisible()) { _table.setWidget(0, c, column); _table.getCellFormatter().setStylePrimaryName(0, c, STYLE_HEADER); if (column.isSortable()) _table.getCellFormatter().addStyleName(0, c, STYLE_SORTABLE); setColumnSortStyle(c, column.getSortDirection()); c++; } } } protected void render(int pos) { Rows rows = getRows(pos); GWT.log("Rendering " + rows.size() + " rows", null); int r = 1; for (int i = 0; i < rows.size(); ++i, ++r) { final Row row = rows.getRow(i); for (int j = 0, c = 0; j < _layout.getTotalColumnCount(); ++j) { final Column column = _layout.getColumn(j); if (column.isVisible()) { final Widget widget = column.getRenderer().render(row, column, row.getValue(j)); if (widget instanceof HasClickHandlers) { ((HasClickHandlers) widget).addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { fireClickEvent(row, column, widget); } }); } if (widget instanceof HasChangeHandlers) { ((HasChangeHandlers) widget).addChangeHandler(new ChangeHandler() { @Override public void onChange(ChangeEvent event) { fireChangeEvent(row, column, widget); } }); } fireRenderCellEvent(row, column, widget); _table.setWidget(r, c, widget); _table.getFlexCellFormatter().addStyleName(r, c, STYLE_CELL); c++; } } } for (; r <= Math.min(_minsize, _size); ++r) { for (int j = 0, c = 0; j < _layout.getTotalColumnCount(); ++j) { final Column column = _layout.getColumn(j); if (column.isVisible()) _table.setWidget(r, c++, new Label()); } } resetEmpty(); refreshRowState(); } protected void renderEmpty(String message, String style) { int r = 1; while (_table.getRowCount() > 1) _table.removeRow(1); _table.setWidget(r, 0, new Label(message)); FlexTable.FlexCellFormatter formatter = (FlexTable.FlexCellFormatter) _table.getCellFormatter(); formatter.setColSpan(r, 0, _layout.getVisibleColumnCount()); formatter.setRowSpan(r, 0, _size); formatter.setStylePrimaryName(r, 0, style); _table.getRowFormatter().setStyleName(r, STYLE_ROW); _table.getRowFormatter().addStyleName(r, STYLE_ROW_EMPTY); } protected void resetEmpty() { int r = 1; FlexTable.FlexCellFormatter formatter = (FlexTable.FlexCellFormatter) _table.getCellFormatter(); if (_table.getRowCount() > 1) { formatter.removeStyleName(r, 0, STYLE_NO_DATA); formatter.removeStyleName(r, 0, STYLE_ERROR); formatter.setColSpan(r, 0, 1); formatter.setRowSpan(r, 0, 1); } } protected Rows getRows(int begin) { int sortId = -1; boolean ascending = false; int sortColumn = _layout.getSortColumn(); if (sortColumn >= 0) { sortId = _layout.getColumn(sortColumn).getId(); ascending = _layout.getColumn(sortColumn).getSortDirection() == Column.Sort.ASCENDING; } Rows rows = _cache.getRows(begin, begin + _size, sortId, ascending); return rows; } /** * Should be called by the handler of <code>ContentProvider</code> requests to * pass the loaded rows to the table. Usually this will be called by an asynchronous * callback <code>AsyncCallback.onSuccess</code> implementation. * @param rows */ public void onSuccess(Rows rows) { while (_table.getRowCount() > 0) _table.removeRow(0); _cache.merge(rows); fireLoadedEvent(true); fireRenderEvent(); renderHeader(); if (rows.size() == 0) { renderEmpty(_messages.none(), STYLE_NO_DATA); } else { render(rows.getBegin()); } _begin = rows.getBegin(); fireRenderedEvent(); initOptimalSize(); } /** * Should be called by the handler of <code>ContentProvider</code> requests when * a failure occurs. Usually this will be called by an asynchronous callback * <code>AsyncCallback.onFailure</code> implementation. * @param caught */ public void onFailure(Throwable caught) { while (_table.getRowCount() > 0) _table.removeRow(0); _cache.clear(); fireLoadedEvent(false); fireRenderEvent(); renderHeader(); render(_begin); renderEmpty(_messages.error(caught == null ? null : caught.getMessage()), STYLE_ERROR); fireRenderedEvent(); initOptimalSize(); } public void addTableListener(TableListener listener) { _listeners.add(listener); } public void removeTableListener(TableListener listener) { _listeners.remove(listener); } protected void fireCellClickedEvent(int row, int column) { for (TableListener listener : _listeners) { listener.onCellClicked(this, row, column); } } protected void fireCellClickedEvent(Row row, Column column) { for (TableListener listener : _listeners) { listener.onCellClicked(this, row, column); } } protected void fireRowClickedEvent(Row row) { for (TableListener listener : _listeners) { listener.onRowClicked(this, row); } } protected void fireClickEvent(Row row, Column column, Widget widget) { for (TableListener listener : _listeners) { listener.onClick(this, row, column, widget); } } protected void fireChangeEvent(Row row, Column column, Widget widget) { for (TableListener listener : _listeners) { listener.onChange(this, row, column, widget); } } protected boolean fireShowColumnEvent(Column column, boolean visible) { for (TableListener listener : _listeners) { if (!listener.onShowColumn(this, column, visible)) return false; } return true; } protected boolean fireSortColumnEvent(Column column, boolean ascending) { for (TableListener listener : _listeners) { if (!listener.onSortColumn(this, column, ascending)) return false; } return true; } protected void fireLoadEvent() { for (TableListener listener : _listeners) { listener.onLoad(this); } } protected void fireLoadedEvent(boolean success) { for (TableListener listener : _listeners) { listener.onLoaded(this, success); } } protected void fireRenderEvent() { for (TableListener listener : _listeners) { listener.onRender(this); } } protected void fireRenderedEvent() { for (TableListener listener : _listeners) { listener.onRendered(this); } } protected void fireRenderCellEvent(Row row, Column column, Widget widget) { for (TableListener listener : _listeners) { listener.onRenderCell(this, row, column, widget); } } /** * Converts visible column to actual column. * @param col * @return */ private int toActualColumnPos(int col) { int pos = -1; while (col >= 0) { if (_layout.getColumn(++pos).isVisible()) --col; } return pos; } /** * Converts visible column to actual column. * @param col * @return */ private int toVisibleColumnPos(int col) { int pos = -1; while (col >= 0) { if (_layout.getColumn(col--).isVisible()) ++pos; } return pos; } public void setColumnSortStyle(int col, int dir) { int row = 0; FlexTable.FlexCellFormatter formatter = _table.getFlexCellFormatter(); switch (dir) { case Column.Sort.ASCENDING: formatter.removeStyleName(row, col, STYLE_DESCENDING); formatter.addStyleName(row, col, STYLE_ASCENDING); break; case Column.Sort.DESCENDING: formatter.removeStyleName(row, col, STYLE_ASCENDING); formatter.addStyleName(row, col, STYLE_DESCENDING); break; default: formatter.removeStyleName(row, col, STYLE_ASCENDING); formatter.removeStyleName(row, col, STYLE_DESCENDING); break; } } }