Java tutorial
/* * Copyright (C) 2011 Openismus GmbH * Copyright (c) 2011 Ben Konrath <ben@bagu.org> * * This file is part of GWT-Glom. * * GWT-Glom is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your * option) any later version. * * GWT-Glom is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with GWT-Glom. If not, see <http://www.gnu.org/licenses/>. */ package org.glom.web.client.ui.list; import java.util.List; import org.glom.web.client.Utils; import org.glom.web.client.ui.cell.BooleanCell; import org.glom.web.client.ui.cell.NavigationButtonCell; import org.glom.web.client.ui.cell.NumericCell; import org.glom.web.client.ui.cell.TextCell; import org.glom.web.shared.DataItem; import org.glom.web.shared.libglom.Field.GlomFieldType; import org.glom.web.shared.libglom.NumericFormat; import org.glom.web.shared.libglom.layout.Formatting; import org.glom.web.shared.libglom.layout.LayoutGroup; import org.glom.web.shared.libglom.layout.LayoutItem; import org.glom.web.shared.libglom.layout.LayoutItemField; import com.google.gwt.cell.client.ImageCell; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.Style.Visibility; import com.google.gwt.event.shared.EventBus; import com.google.gwt.i18n.client.NumberFormat; import com.google.gwt.safehtml.shared.SafeHtmlBuilder; import com.google.gwt.safehtml.shared.SafeHtmlUtils; import com.google.gwt.user.cellview.client.CellTable; import com.google.gwt.user.cellview.client.Column; import com.google.gwt.user.cellview.client.ColumnSortEvent.AsyncHandler; import com.google.gwt.user.cellview.client.LoadingStateChangeEvent; import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState; import com.google.gwt.user.cellview.client.SafeHtmlHeader; import com.google.gwt.user.cellview.client.SimplePager; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.HasHorizontalAlignment; import com.google.gwt.user.client.ui.HasHorizontalAlignment.HorizontalAlignmentConstant; import com.google.gwt.user.client.ui.Widget; import com.google.gwt.view.client.AbstractDataProvider; import com.google.gwt.view.client.ProvidesKey; /** * */ public abstract class ListTable extends Composite { private class ListTablePager extends SimplePager { public ListTablePager() { super(SimplePager.TextLocation.CENTER); setStyleName("pager"); } /* * A custom version of createText to display the correct number of data row when empty rows have been added to * the CellTable. This method is needed because the row count change event handler * (AbstractPager.handleRowCountChange()) doesn't use the row count that is sent along with the * RowCountChangeEvent. * * @see com.google.gwt.user.cellview.client.AbstractPager#handleRowCountChange(int, boolean) * * @see com.google.gwt.user.cellview.client.SimplePager#createText() */ @Override protected String createText() { final int numNonEmptyRows = getNumNonEmptyRows(); if (numNonEmptyRows < getMinNumVisibleRows()) { final NumberFormat formatter = NumberFormat.getFormat("#,###"); return formatter.format(1) + "-" + formatter.format(numNonEmptyRows) + " of " + formatter.format(numNonEmptyRows); } else { return super.createText(); } } } final private FlowPanel mainPanel = new FlowPanel(); final private ListTablePager pager = new ListTablePager(); protected String documentID; protected String tableName; protected String quickFind; protected CellTable<DataItem[]> cellTable; protected EventBus eventBus; Column<DataItem[], String> navigationButtonColumn; private int cellTableBodyHeight = 0; abstract protected AbstractDataProvider<DataItem[]> getDataProvider(); @SuppressWarnings("unused") private ListTable() { // disable default constructor } public ListTable(final String documentID) { this.documentID = documentID; } protected void createCellTable(final LayoutGroup layoutGroup, final String tableName, final int numVisibleRows, final String navigationButtonLabel, final NavigationButtonCell navigationButtonCell) { this.tableName = tableName; final List<LayoutItem> layoutItems = layoutGroup.getItems(); ProvidesKey<DataItem[]> keyProvider = null; final int primaryKeyIndex = layoutGroup.getPrimaryKeyIndex(); if ((primaryKeyIndex < 0) || (primaryKeyIndex >= layoutItems.size())) { GWT.log("createCellTable(): primaryKeyIndex is out of range: " + primaryKeyIndex); } else { final LayoutItem primaryKeyItem = layoutItems.get(primaryKeyIndex); if (!(primaryKeyItem instanceof LayoutItemField)) { GWT.log("createCellTable(): primaryKeyItem is not a LayoutItemField."); } else { final LayoutItemField primaryKeyLayoutItem = (LayoutItemField) primaryKeyItem; final GlomFieldType primaryKeyFieldType = primaryKeyLayoutItem.getGlomType(); keyProvider = new ProvidesKey<DataItem[]>() { @Override public Object getKey(final DataItem[] row) { if (row.length == 1 && row[0] == null) { // an empty row return null; } if ((primaryKeyIndex < 0) || (primaryKeyIndex >= row.length)) { GWT.log("createCellTable() keyProvider.getKey(): primaryKeyIndex is out of range: " + primaryKeyIndex + ", row.length=" + row.length); return null; } return Utils.getTypedDataItem(primaryKeyFieldType, row[primaryKeyIndex]); } }; } } // create the CellTable with the requested number of rows and the key provider cellTable = new CellTable<DataItem[]>(numVisibleRows, keyProvider); // set some style cellTable.setStyleName("data-list"); cellTable.getElement().getStyle().setProperty("whiteSpace", "nowrap"); // this prevents the header and row text // from wrapping // add columns to the CellTable and deal with the case of the hidden primary key final int numItems = layoutGroup.hasHiddenPrimaryKey() ? layoutItems.size() - 1 : layoutItems.size(); for (int i = 0; i < numItems; i++) { final LayoutItem layoutItem = layoutItems.get(i); // only add columns for LayoutItemField types if (layoutItem instanceof LayoutItemField) { addColumn((LayoutItemField) layoutItem); } else { GWT.log("createCellTable(): Ignoring non-LayoutItemField layout item."); } } // add the navigation buttons as the last column addNavigationButtonColumn(navigationButtonLabel, navigationButtonCell); // create and set the data provider final AbstractDataProvider<DataItem[]> dataProvider = getDataProvider(); dataProvider.addDataDisplay(cellTable); // add an AsyncHandler to activate sorting for the data provider cellTable.addColumnSortHandler(new AsyncHandler(cellTable)); // pack the widgets into the container pager.setDisplay(cellTable); mainPanel.add(cellTable); mainPanel.add(pager); /* * Update the height of the loading indicator widget to match the body of the CellTable so that the pager widget * doesn't bounce up and down while paging. This code also ensures that loading indicator GIF is in the centre * of the table. * * TODO: Make this work with related lists in Notebooks. These related list tables will have the original bouncy * behaviour because CellTable.getBodyHeight() of a related list table in an unselected notebook tab returns 0. * * TODO: Fix the bounce when paging to the first or last page that doesn't fall on a natural page boundary. This * happens in the first and last page when dataSize % pageSize != 0. */ cellTable.addLoadingStateChangeHandler(new LoadingStateChangeEvent.Handler() { @Override public void onLoadingStateChanged(final LoadingStateChangeEvent event) { // LoadingState.LOADED means the data has been received but not necessarily rendered. if (event.getLoadingState() == LoadingState.LOADED) { new Timer() { @Override public void run() { if (cellTable.isAttached()) { final int bodyHeight = cellTable.getBodyHeight(); /* * Modify the indicator widget only if body height is bigger than the body height that * has already been set. This is just a safety check for the case where the timer isn't * long enough and the body height is calculated to be smaller than its full size. In * practice this is not expected to happen. * * Since cellTableBodyHeight is initialised to 0, the indicator widget will not be * modified when the body height cannot be calculated (e.g. when a related list table is * in an unselected notebook tab). */ if (bodyHeight > cellTableBodyHeight) { final Widget loadingIndicator = cellTable.getLoadingIndicator(); // Set the margin of the parent div to zero. final Element parent = loadingIndicator.getElement().getParentElement(); parent.getStyle().setMargin(0, Unit.PX); // Set the height of the table cell that holds the loading indicator GIF. final Element cell = parent.getParentElement().getParentElement() .getParentElement(); cell.getStyle().setPadding(0, Unit.PX); cell.getStyle().setHeight(bodyHeight, Unit.PX); // save the new body height cellTableBodyHeight = bodyHeight; } } } }.schedule(200); // 200 ms should be enough } } }); // initialize composite widget initWidget(mainPanel); } private void addColumn(final LayoutItemField layoutItemField) { // Setup the default alignment of the column. HorizontalAlignmentConstant columnAlignment; Formatting formatting = layoutItemField.getFormatting(); if (formatting == null) { GWT.log("addColumn(): Formatting is null for field=" + layoutItemField.getLayoutDisplayName()); formatting = new Formatting(); // Just to avoid null dereferencing later. } switch (formatting.getHorizontalAlignment()) { case HORIZONTAL_ALIGNMENT_LEFT: columnAlignment = HasHorizontalAlignment.ALIGN_LEFT; break; case HORIZONTAL_ALIGNMENT_RIGHT: columnAlignment = HasHorizontalAlignment.ALIGN_RIGHT; break; case HORIZONTAL_ALIGNMENT_AUTO: default: columnAlignment = HasHorizontalAlignment.ALIGN_DEFAULT; break; } // create a new column Column<DataItem[], ?> column = null; final int j = cellTable.getColumnCount(); switch (layoutItemField.getGlomType()) { case TYPE_BOOLEAN: column = new Column<DataItem[], Boolean>(new BooleanCell()) { @Override public Boolean getValue(final DataItem[] row) { if (row.length == 1 && row[0] == null) { // an empty row return null; } if (j >= row.length) { GWT.log("addColumn(): j=" + j + " is out of range. length=" + row.length); return null; } else { return row[j].getBoolean(); } } }; // override the configured horizontal alignment columnAlignment = HasHorizontalAlignment.ALIGN_CENTER; break; case TYPE_NUMERIC: // create a GWT NumberFormat for the column final NumericFormat numericFormat = formatting.getNumericFormat(); final NumberFormat gwtNumberFormat = Utils.getNumberFormat(numericFormat); // create the actual column column = new Column<DataItem[], Double>(new NumericCell( formatting.getTextFormatColorForegroundAsHTMLColor(), formatting.getTextFormatColorBackgroundAsHTMLColor(), gwtNumberFormat, numericFormat.getUseAltForegroundColorForNegatives(), numericFormat.getCurrencySymbol())) { @Override public Double getValue(final DataItem[] row) { if (row.length == 1 && row[0] == null) { // an empty row return null; } if (j >= row.length) { GWT.log("addColumn(): j=" + j + " is out of range. length=" + row.length); return null; } else { return row[j].getNumber(); } } }; break; case TYPE_IMAGE: column = new Column<DataItem[], String>(new ImageCell()) { @Override public String getValue(final DataItem[] row) { if (row.length == 1 && row[0] == null) { // an empty row return null; } if (j >= row.length) { GWT.log("addColumn(): j=" + j + " is out of range. length=" + row.length); return null; } else { return row[j].getImageDataUrl(); } } }; // override the configured horizontal alignment columnAlignment = HasHorizontalAlignment.ALIGN_CENTER; break; default: // use a text rendering cell for types we don't know about but log an error // TODO log error here case TYPE_DATE: case TYPE_INVALID: case TYPE_TIME: case TYPE_TEXT: column = new Column<DataItem[], String>( new TextCell(formatting.getTextFormatColorForegroundAsHTMLColor(), formatting.getTextFormatColorBackgroundAsHTMLColor())) { @Override public String getValue(final DataItem[] row) { if (row.length == 1 && row[0] == null) { // an empty row return null; } if (j >= row.length) { GWT.log("addColumn(): j=" + j + " is out of range. length=" + row.length); return null; } else { return row[j].getText(); } } }; break; } // set column properties and add to cell cellTable column.setHorizontalAlignment(columnAlignment); column.setSortable(true); cellTable.addColumn(column, new SafeHtmlHeader(SafeHtmlUtils.fromString(layoutItemField.getTitle()))); } private void addNavigationButtonColumn(final String navigationButtonLabel, final NavigationButtonCell navigationButtonCell) { navigationButtonColumn = new Column<DataItem[], String>(navigationButtonCell) { @Override public String getValue(final DataItem[] row) { if (row.length == 1 && row[0] == null) { // an empty row return null; } return navigationButtonLabel; } }; // Firefox, Chrome, and Safari only support the span and width attributes of the col element so we need to set // the alignment with code navigationButtonColumn.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_RIGHT); cellTable.addColumn(navigationButtonColumn, ""); // the style name for the details column is set on the col element cellTable.addColumnStyleName(cellTable.getColumnCount() - 1, "details"); } /** * Sets the row count for the pager. */ public void setRowCount(final int rowCount) { cellTable.setRowCount(rowCount); } public void hideNavigationButtons() { if (navigationButtonColumn != null) { cellTable.setColumnWidth(navigationButtonColumn, 0, Unit.PX); } } /** * Gets the minimum number of rows the should be displayed. Empty rows will be added when the query returns fewer * rows than this minimum. * * @return The minimum number of rows that should be displayed. */ public abstract int getMinNumVisibleRows(); public abstract int getNumNonEmptyRows(); /* * (non-Javadoc) * * @see com.google.gwt.user.client.ui.Widget#onLoad() */ @Override protected void onLoad() { /* * Set the width of the navigation button column to be as small as possible. */ // The navigationButtonColumn width will be null if it hasn't been set. This indicates that the column width // hasn't been disabled with the hideNavigationButtons() method or been set with this method. The width of the // navigation button column shouldn't be changed once it's set. if (navigationButtonColumn != null && cellTable.getColumnWidth(navigationButtonColumn) == null) { // Use the NavigationButtonCell to get the button HTML and find the width. I'm doing this because the // CellTable widget is highly dynamic and there's no way to guarantee that we can access the navigation // button HTML by using the actual CellTable. final String buttonLabel = navigationButtonColumn.getValue(new DataItem[2]); // a hack to get the button // label final SafeHtmlBuilder buttonBuilder = new SafeHtmlBuilder(); navigationButtonColumn.getCell().render(null, buttonLabel, buttonBuilder); Element navigationButton = new HTML(buttonBuilder.toSafeHtml()).getElement().getFirstChildElement(); // Calculate the width similar to Utils.getWidgetHeight(). final Document doc = Document.get(); navigationButton.getStyle().setVisibility(Visibility.HIDDEN); doc.getBody().appendChild(navigationButton); final int buttonWidth = navigationButton.getOffsetWidth(); // remove the div from the from the document doc.getBody().removeChild(navigationButton); navigationButton = null; // set the width if (buttonWidth > 0) { cellTable.setColumnWidth(navigationButtonColumn, buttonWidth + 6, Unit.PX); navigationButtonColumn.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_CENTER); } } } }