diff options
63 files changed, 14181 insertions, 0 deletions
diff --git a/WebContent/VAADIN/themes/base/base.scss b/WebContent/VAADIN/themes/base/base.scss index fd3c5d067d..58274edacb 100644 --- a/WebContent/VAADIN/themes/base/base.scss +++ b/WebContent/VAADIN/themes/base/base.scss @@ -15,8 +15,10 @@ @import "inlinedatefield/inlinedatefield.scss"; @import "dragwrapper/dragwrapper.scss"; @import "embedded/embedded.scss"; +@import "escalator/escalator.scss"; @import "fonts/fonts.scss"; @import "formlayout/formlayout.scss"; +@import "grid/grid.scss"; @import "gridlayout/gridlayout.scss"; @import "label/label.scss"; @import "link/link.scss"; @@ -90,7 +92,9 @@ $line-height: normal; @include base-inline-datefield; @include base-dragwrapper; @include base-embedded; + @include base-escalator; @include base-formlayout; + @include base-grid; @include base-gridlayout; @include base-label; @include base-link; diff --git a/WebContent/VAADIN/themes/base/escalator/escalator.scss b/WebContent/VAADIN/themes/base/escalator/escalator.scss new file mode 100644 index 0000000000..21424bd456 --- /dev/null +++ b/WebContent/VAADIN/themes/base/escalator/escalator.scss @@ -0,0 +1,110 @@ +@mixin base-escalator($primaryStyleName : v-escalator) { + +$background-color: white; +$border-color: #aaa; + +.#{$primaryStyleName} { + position: relative; + background-color: $background-color; +} + +.#{$primaryStyleName}-scroller { + position: absolute; + overflow: auto; + z-index: 20; +} + +.#{$primaryStyleName}-scroller-horizontal { + left: 0; /* Left position adjusted to align with frozen columns */ + right: 0; + bottom: 0; + overflow-y: hidden; + -ms-overflow-y: hidden; +} + +.#{$primaryStyleName}-scroller-vertical { + right: 0; + top: 0; /* this will be overridden by code, but it's a good default behavior */ + bottom: 0; /* this will be overridden by code, but it's a good default behavior */ + overflow-x: hidden; + -ms-overflow-x: hidden; +} + +.#{$primaryStyleName}-tablewrapper { + position: absolute; + overflow: hidden; +} + +.#{$primaryStyleName}-tablewrapper > table { + border-spacing: 0; + table-layout: fixed; + width: inherit; /* a decent default fallback */ +} + +.#{$primaryStyleName}-header, +.#{$primaryStyleName}-body, +.#{$primaryStyleName}-footer { + position: absolute; + left: 0; + width: inherit; + z-index: 10; +} + +.#{$primaryStyleName}-header { top: 0; } +.#{$primaryStyleName}-footer { bottom: 0; } + +.#{$primaryStyleName}-body { + z-index: 0; + top: 0; + + .#{$primaryStyleName}-row { + position: absolute; + top: 0; + left: 0; + } +} + +.#{$primaryStyleName}-row { + display: block; + + .v-ie8 & { + /* IE8 doesn't let table rows be longer than body only with display block. Moar hax. */ + float: left; + clear: left; + + /* + * The inline style of margin-top from the <tbody> to offset the header's dimension is, + * for some strange reason, inherited into each contained <tr>. + * We need to cancel it: + */ + margin-top: 0; + } + + > td, > th { + /* IE8 likes the bgcolor here instead of on the row */ + background-color: $background-color; + } +} + + +.#{$primaryStyleName}-row { + width: inherit; +} + +.#{$primaryStyleName}-cell { + display: block; + float: left; + border: 1px solid $border-color; + padding: 2px; + white-space: nowrap; + -moz-box-sizing: border-box; + box-sizing: border-box; + overflow:hidden; +} + +.#{$primaryStyleName}-cell.frozen { + position: relative; + z-index: 0; +} + +}
\ No newline at end of file diff --git a/WebContent/VAADIN/themes/base/grid/grid.scss b/WebContent/VAADIN/themes/base/grid/grid.scss new file mode 100644 index 0000000000..9f7a2d8664 --- /dev/null +++ b/WebContent/VAADIN/themes/base/grid/grid.scss @@ -0,0 +1,3 @@ +@mixin base-grid($primaryStyleName : v-grid) { + @include base-escalator($primaryStyleName); +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java new file mode 100644 index 0000000000..127eb80696 --- /dev/null +++ b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java @@ -0,0 +1,323 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.data; + +import java.util.HashMap; +import java.util.List; + +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.vaadin.client.Profiler; +import com.vaadin.shared.ui.grid.Range; + +/** + * Base implementation for data sources that fetch data from a remote system. + * This class takes care of caching data and communicating with the data source + * user. An implementation of this class should override + * {@link #requestRows(int, int)} to trigger asynchronously loading of data. + * When data is received from the server, new row data should be passed to + * {@link #setRowData(int, List)}. {@link #setEstimatedSize(int)} should be used + * based on estimations of how many rows are available. + * + * @since 7.2 + * @author Vaadin Ltd + * @param <T> + * the row type + */ +public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { + + private boolean requestPending = false; + + private boolean coverageCheckPending = false; + + private Range requestedAvailability = Range.between(0, 0); + + private Range cached = Range.between(0, 0); + + private final HashMap<Integer, T> rowCache = new HashMap<Integer, T>(); + + private DataChangeHandler dataChangeHandler; + + private int estimatedSize; + + private final ScheduledCommand coverageChecker = new ScheduledCommand() { + @Override + public void execute() { + coverageCheckPending = false; + checkCacheCoverage(); + } + }; + + /** + * Sets the estimated number of rows in the data source. + * + * @param estimatedSize + * the estimated number of available rows + */ + protected void setEstimatedSize(int estimatedSize) { + // TODO update dataChangeHandler if size changes + this.estimatedSize = estimatedSize; + } + + private void ensureCoverageCheck() { + if (!coverageCheckPending) { + coverageCheckPending = true; + Scheduler.get().scheduleDeferred(coverageChecker); + } + } + + @Override + public void ensureAvailability(int firstRowIndex, int numberOfRows) { + requestedAvailability = Range.withLength(firstRowIndex, numberOfRows); + + /* + * Don't request any data right away since the data might be included in + * a message that has been received but not yet fully processed. + */ + ensureCoverageCheck(); + } + + private void checkCacheCoverage() { + if (requestPending) { + // Anyone clearing requestPending should run this method again + return; + } + + Profiler.enter("AbstractRemoteDataSource.checkCacheCoverage"); + + if (!requestedAvailability.intersects(cached) || cached.isEmpty()) { + /* + * Simple case: no overlap between cached data and needed data. + * Clear the cache and request new data + */ + rowCache.clear(); + cached = Range.between(0, 0); + + handleMissingRows(requestedAvailability); + } else { + discardStaleCacheEntries(); + + // Might need more rows -> request them + Range[] availabilityPartition = requestedAvailability + .partitionWith(cached); + handleMissingRows(availabilityPartition[0]); + handleMissingRows(availabilityPartition[2]); + } + + Profiler.leave("AbstractRemoteDataSource.checkCacheCoverage"); + } + + private void discardStaleCacheEntries() { + Range[] cacheParition = cached.partitionWith(requestedAvailability); + dropFromCache(cacheParition[0]); + cached = cacheParition[1]; + dropFromCache(cacheParition[2]); + } + + private void dropFromCache(Range range) { + for (int i = range.getStart(); i < range.getEnd(); i++) { + rowCache.remove(Integer.valueOf(i)); + } + } + + private void handleMissingRows(Range range) { + if (range.isEmpty()) { + return; + } + requestPending = true; + requestRows(range.getStart(), range.length()); + } + + /** + * Triggers fetching rows from the remote data source. + * {@link #setRowData(int, List)} should be invoked with data for the + * requested rows when they have been received. + * + * @param firstRowIndex + * the index of the first row to fetch + * @param numberOfRows + * the number of rows to fetch + */ + protected abstract void requestRows(int firstRowIndex, int numberOfRows); + + @Override + public int getEstimatedSize() { + return estimatedSize; + } + + @Override + public T getRow(int rowIndex) { + return rowCache.get(Integer.valueOf(rowIndex)); + } + + @Override + public void setDataChangeHandler(DataChangeHandler dataChangeHandler) { + this.dataChangeHandler = dataChangeHandler; + + if (dataChangeHandler != null && !cached.isEmpty()) { + // Push currently cached data to the implementation + dataChangeHandler.dataUpdated(cached.getStart(), cached.length()); + } + } + + /** + * Informs this data source that updated data has been sent from the server. + * + * @param firstRowIndex + * the index of the first received row + * @param rowData + * a list of rows, starting from <code>firstRowIndex</code> + */ + protected void setRowData(int firstRowIndex, List<T> rowData) { + requestPending = false; + + Profiler.enter("AbstractRemoteDataSource.setRowData"); + + Range received = Range.withLength(firstRowIndex, rowData.size()); + + Range[] partition = received.partitionWith(requestedAvailability); + + Range newUsefulData = partition[1]; + if (!newUsefulData.isEmpty()) { + // Update the parts that are actually inside + for (int i = newUsefulData.getStart(); i < newUsefulData.getEnd(); i++) { + rowCache.put(Integer.valueOf(i), rowData.get(i - firstRowIndex)); + } + + if (dataChangeHandler != null) { + Profiler.enter("AbstractRemoteDataSource.setRowData notify dataChangeHandler"); + dataChangeHandler.dataUpdated(newUsefulData.getStart(), + newUsefulData.length()); + Profiler.leave("AbstractRemoteDataSource.setRowData notify dataChangeHandler"); + } + + // Potentially extend the range + if (cached.isEmpty()) { + cached = newUsefulData; + } else { + discardStaleCacheEntries(); + + /* + * everything might've become stale so we need to re-check for + * emptiness. + */ + if (!cached.isEmpty()) { + cached = cached.combineWith(newUsefulData); + } else { + cached = newUsefulData; + } + } + } + + if (!partition[0].isEmpty() || !partition[2].isEmpty()) { + /* + * FIXME + * + * Got data that we might need in a moment if the container is + * updated before the widget settings. Support for this will be + * implemented later on. + */ + } + + // Eventually check whether all needed rows are now available + ensureCoverageCheck(); + + Profiler.leave("AbstractRemoteDataSource.setRowData"); + } + + /** + * Informs this data source that the server has removed data. + * + * @param firstRowIndex + * the index of the first removed row + * @param count + * the number of removed rows, starting from + * <code>firstRowIndex</code> + */ + protected void removeRowData(int firstRowIndex, int count) { + Profiler.enter("AbstractRemoteDataSource.removeRowData"); + + // pack the cached data + for (int i = 0; i < count; i++) { + Integer oldIndex = Integer.valueOf(firstRowIndex + count + i); + if (rowCache.containsKey(oldIndex)) { + Integer newIndex = Integer.valueOf(firstRowIndex + i); + rowCache.put(newIndex, rowCache.remove(oldIndex)); + } + } + + Range removedRange = Range.withLength(firstRowIndex, count); + if (removedRange.intersects(cached)) { + Range[] partitions = cached.partitionWith(removedRange); + Range remainsBefore = partitions[0]; + Range transposedRemainsAfter = partitions[2].offsetBy(-removedRange + .length()); + cached = remainsBefore.combineWith(transposedRemainsAfter); + } + estimatedSize -= count; + dataChangeHandler.dataRemoved(firstRowIndex, count); + checkCacheCoverage(); + + Profiler.leave("AbstractRemoteDataSource.removeRowData"); + } + + /** + * Informs this data source that new data has been inserted from the server. + * + * @param firstRowIndex + * the destination index of the new row data + * @param count + * the number of rows inserted + */ + protected void insertRowData(int firstRowIndex, int count) { + Profiler.enter("AbstractRemoteDataSource.insertRowData"); + + if (cached.contains(firstRowIndex)) { + int oldCacheEnd = cached.getEnd(); + /* + * We need to invalidate the cache from the inserted row onwards, + * since the cache wants to be a contiguous range. It doesn't + * support holes. + * + * If holes were supported, we could shift the higher part of + * "cached" and leave a hole the size of "count" in the middle. + */ + cached = cached.splitAt(firstRowIndex)[0]; + + for (int i = firstRowIndex; i < oldCacheEnd; i++) { + rowCache.remove(Integer.valueOf(i)); + } + } + + else if (firstRowIndex < cached.getStart()) { + Range oldCached = cached; + cached = cached.offsetBy(count); + + for (int i = 0; i < rowCache.size(); i++) { + Integer oldIndex = Integer.valueOf(oldCached.getEnd() - i); + Integer newIndex = Integer.valueOf(cached.getEnd() - i); + rowCache.put(newIndex, rowCache.remove(oldIndex)); + } + } + + estimatedSize += count; + dataChangeHandler.dataAdded(firstRowIndex, count); + checkCacheCoverage(); + + Profiler.leave("AbstractRemoteDataSource.insertRowData"); + } +} diff --git a/client/src/com/vaadin/client/data/DataChangeHandler.java b/client/src/com/vaadin/client/data/DataChangeHandler.java new file mode 100644 index 0000000000..4c4cc7656d --- /dev/null +++ b/client/src/com/vaadin/client/data/DataChangeHandler.java @@ -0,0 +1,59 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.data; + +/** + * Callback interface used by {@link DataSource} to inform its user about + * updates to the data. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface DataChangeHandler { + /** + * Called when the contents of the data source has changed. If the number of + * rows has changed or if rows have been moved around, + * {@link #dataAdded(int, int)} or {@link #dataRemoved(int, int)} should + * ideally be used instead. + * + * @param firstRowIndex + * the index of the first changed row + * @param numberOfRows + * the number of changed rows + */ + public void dataUpdated(int firstRowIndex, int numberOfRows); + + /** + * Called when rows have been removed from the data source. + * + * @param firstRowIndex + * the index that the first removed row had prior to removal + * @param numberOfRows + * the number of removed rows + */ + public void dataRemoved(int firstRowIndex, int numberOfRows); + + /** + * Called when the new rows have been added to the container. + * + * @param firstRowIndex + * the index of the first added row + * @param numberOfRows + * the number of added rows + */ + public void dataAdded(int firstRowIndex, int numberOfRows); +} diff --git a/client/src/com/vaadin/client/data/DataSource.java b/client/src/com/vaadin/client/data/DataSource.java new file mode 100644 index 0000000000..9179b6d03d --- /dev/null +++ b/client/src/com/vaadin/client/data/DataSource.java @@ -0,0 +1,76 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.data; + +/** + * Source of data for widgets showing lazily loaded data based on indexable + * items (e.g. rows) of a specified type. The data source is a lazy view into a + * larger data set. + * + * @since 7.2 + * @author Vaadin Ltd + * @param <T> + * the row type + */ +public interface DataSource<T> { + /** + * Informs the data source that data for the given range is needed. A data + * source only has one active region at a time, so calling this method + * discards the previously set range. + * <p> + * This method triggers lazy loading of data if necessary. The change + * handler registered using {@link #setDataChangeHandler(DataChangeHandler)} + * is informed when new data has been loaded. + * + * @param firstRowIndex + * the index of the first needed row + * @param numberOfRows + * the number of needed rows + */ + public void ensureAvailability(int firstRowIndex, int numberOfRows); + + /** + * Retrieves the data for the row at the given index. If the row data is not + * available, returns <code>null</code>. + * <p> + * This method does not trigger loading of unavailable data. + * {@link #ensureAvailability(int, int)} should be used to signal what data + * will be needed. + * + * @param rowIndex + * the index of the row to retrieve data for + * @return data for the row; or <code>null</code> if no data is available + */ + public T getRow(int rowIndex); + + /** + * Returns the current best guess for the number of rows in the container. + * + * @return the current estimation of the container size + */ + public int getEstimatedSize(); + + /** + * Sets a data change handler to inform when data is updated, added or + * removed. + * + * @param dataChangeHandler + * the data change handler + */ + public void setDataChangeHandler(DataChangeHandler dataChangeHandler); + +} diff --git a/client/src/com/vaadin/client/data/RpcDataSourceConnector.java b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java new file mode 100644 index 0000000000..4d22c10197 --- /dev/null +++ b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java @@ -0,0 +1,81 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.data; + +import java.util.List; + +import com.vaadin.client.ServerConnector; +import com.vaadin.client.extensions.AbstractExtensionConnector; +import com.vaadin.client.ui.grid.GridConnector; +import com.vaadin.shared.data.DataProviderRpc; +import com.vaadin.shared.data.DataProviderState; +import com.vaadin.shared.data.DataRequestRpc; +import com.vaadin.shared.ui.Connect; + +/** + * Connects a Vaadin server-side container data source to a Grid. This is + * currently implemented as an Extension hardcoded to support a specific + * connector type. This will be changed once framework support for something + * more flexible has been implemented. + * + * @since 7.2 + * @author Vaadin Ltd + */ +@Connect(com.vaadin.data.RpcDataProviderExtension.class) +public class RpcDataSourceConnector extends AbstractExtensionConnector { + + private final AbstractRemoteDataSource<String[]> dataSource = new AbstractRemoteDataSource<String[]>() { + @Override + protected void requestRows(int firstRowIndex, int numberOfRows) { + getRpcProxy(DataRequestRpc.class).requestRows(firstRowIndex, + numberOfRows); + } + }; + + @Override + protected void extend(ServerConnector target) { + dataSource.setEstimatedSize(getState().containerSize); + ((GridConnector) target).getWidget().setDataSource(dataSource); + + registerRpc(DataProviderRpc.class, new DataProviderRpc() { + @Override + public void setRowData(int firstRow, List<String[]> rows) { + dataSource.setRowData(firstRow, rows); + } + + @Override + public void removeRowData(int firstRow, int count) { + dataSource.removeRowData(firstRow, count); + } + + @Override + public void insertRowData(int firstRow, int count) { + dataSource.insertRowData(firstRow, count); + } + }); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.client.ui.AbstractConnector#getState() + */ + @Override + public DataProviderState getState() { + return (DataProviderState) super.getState(); + } +} diff --git a/client/src/com/vaadin/client/ui/grid/Cell.java b/client/src/com/vaadin/client/ui/grid/Cell.java new file mode 100644 index 0000000000..3d42f082a6 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/Cell.java @@ -0,0 +1,75 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.ui.HasOneWidget; + +/** + * A representation of a single cell. + * <p> + * A Cell instance will be provided to the {@link EscalatorUpdater} responsible + * for rendering the cells in a certain {@link RowContainer}. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface Cell extends HasOneWidget { + + /** + * Gets the index of the row this cell is in. + * + * @return the index of the row this cell is in + */ + public int getRow(); + + /** + * Gets the index of the column this cell is in. + * + * @return the index of the column this cell is in + */ + public int getColumn(); + + /** + * Gets the root element for this cell. The {@link EscalatorUpdater} may + * update the class names of the element, add inline styles and freely + * modify the contents. + * <p> + * Avoid modifying the dimensions, positioning or colspan of the cell + * element. + * + * @return The root element for this cell. Never <code>null</code>. + */ + public Element getElement(); + + /** + * Sets the column span of the cell. + * <p> + * This will overwrite any possible "colspan" attribute in the current + * element (i.e. the object returned by {@link #getElement()}). This will + * also handle internal bookkeeping, skip the rendering of any affected + * adjacent cells, and make sure that the current cell's dimensions are + * handled correctly. + * + * @param numberOfCells + * the number of cells to span to the right, or <code>1</code> to + * unset any column spans + * @throws IllegalArgumentException + * if <code>numberOfCells < 1</code> + */ + public void setColSpan(int numberOfCells) throws IllegalArgumentException; +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java b/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java new file mode 100644 index 0000000000..64104164cd --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java @@ -0,0 +1,146 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +/** + * A representation of the columns in an instance of {@link Escalator}. + * + * @since 7.2 + * @author Vaadin Ltd + * @see Escalator#getColumnConfiguration() + */ +public interface ColumnConfiguration { + + /** + * Removes columns at a certain index. + * <p> + * If any of the removed columns were frozen, the number of frozen columns + * will be reduced by the number of the removed columns that were frozen. + * + * @param index + * the index of the first column to be removed + * @param numberOfColumns + * the number of rows to remove, starting from the index + * @throws IndexOutOfBoundsException + * if any integer in the range + * <code>[index..(index+numberOfColumns)]</code> is not an + * existing column index. + * @throws IllegalArgumentException + * if <code>numberOfColumns</code> is less than 1. + */ + public void removeColumns(int index, int numberOfColumns) + throws IndexOutOfBoundsException, IllegalArgumentException; + + /** + * Adds columns at a certain index. + * <p> + * The new columns will be inserted between the column at the index, and the + * column before (an index of 0 means that the columns are inserted at the + * beginning). Therefore, the columns at the index and afterwards will be + * moved to the right. + * <p> + * The contents of the inserted columns will be queried from the respective + * cell renderers in the header, body and footer. + * <p> + * If there are frozen columns and the first added column is to the left of + * the last frozen column, the number of frozen columns will be increased by + * the number of inserted columns. + * <p> + * <em>Note:</em> Only the contents of the inserted columns will be + * rendered. If inserting new columns affects the contents of existing + * columns, {@link RowContainer#refreshRows(int, int)} needs to be called as + * appropriate. + * + * @param index + * the index of the column before which new columns are inserted, + * or {@link #getColumnCount()} to add new columns at the end + * @param numberOfColumns + * the number of columns to insert after the <code>index</code> + * @throws IndexOutOfBoundsException + * if <code>index</code> is not an integer in the range + * <code>[0..{@link #getColumnCount()}]</code> + * @throws IllegalArgumentException + * if {@code numberOfColumns} is less than 1. + */ + public void insertColumns(int index, int numberOfColumns) + throws IndexOutOfBoundsException, IllegalArgumentException; + + /** + * Returns the number of columns in the escalator. + * + * @return the number of columns in the escalator + */ + public int getColumnCount(); + + /** + * Sets the number of leftmost columns that are not affected by horizontal + * scrolling. + * + * @param count + * the number of columns to freeze + * + * @throws IllegalArgumentException + * if the column count is < 0 or > the number of columns + * + */ + public void setFrozenColumnCount(int count) throws IllegalArgumentException; + + /** + * Get the number of leftmost columns that are not affected by horizontal + * scrolling. + * + * @return the number of frozen columns + */ + public int getFrozenColumnCount(); + + /** + * Sets (or unsets) an explicit width for a column. + * + * @param index + * the index of the column for which to set a width + * @param px + * the number of pixels the indicated column should be, or a + * negative number to let the escalator decide + * @throws IllegalArgumentException + * if <code>index</code> is not a valid column index + */ + public void setColumnWidth(int index, int px) + throws IllegalArgumentException; + + /** + * Returns the user-defined width of a column. + * + * @param index + * the index of the column for which to retrieve the width + * @return the column's width in pixels, or a negative number if the width + * is implicitly decided by the escalator + * @throws IllegalArgumentException + * if <code>index</code> is not a valid column index + */ + public int getColumnWidth(int index) throws IllegalArgumentException; + + /** + * Returns the actual width of a column. + * + * @param index + * the index of the column for which to retrieve the width + * @return the column's actual width in pixels + * @throws IllegalArgumentException + * if <code>index</code> is not a valid column index + */ + public int getColumnWidthActual(int index) throws IllegalArgumentException; +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/grid/ColumnGroup.java b/client/src/com/vaadin/client/ui/grid/ColumnGroup.java new file mode 100644 index 0000000000..e48656bc6b --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/ColumnGroup.java @@ -0,0 +1,184 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import com.vaadin.client.ui.grid.renderers.TextRenderer; + +/** + * Column groups are used to group columns together for adding common auxiliary + * headers and footers. Columns groups are added to {@link ColumnGroupRow + * ColumnGroupRows}. + * + * @param <T> + * The row type of the grid. The row type is the POJO type from where + * the data is retrieved into the column cells. + * @since 7.2 + * @author Vaadin Ltd + */ +public class ColumnGroup<T> { + + /** + * The text shown in the header + */ + private String header; + + /** + * The text shown in the footer + */ + private String footer; + + /** + * Renders the header cells for the column group + */ + private Renderer<String> headerRenderer = new TextRenderer(); + + /** + * Renders the footer cells for the column group + */ + private Renderer<String> footerRenderer = new TextRenderer(); + + /** + * The columns included in the group when also accounting for subgroup + * columns + */ + private final List<GridColumn<?, T>> columns; + + /** + * The grid associated with the column group + */ + private final Grid<T> grid; + + /** + * Constructs a new column group + */ + ColumnGroup(Grid<T> grid, Collection<GridColumn<?, T>> columns) { + if (columns == null) { + throw new IllegalArgumentException( + "columns cannot be null. Pass an empty list instead."); + } + this.grid = grid; + this.columns = Collections + .unmodifiableList(new ArrayList<GridColumn<?, T>>(columns)); + } + + /** + * Gets the header text. + * + * @return the header text + */ + public String getHeaderCaption() { + return header; + } + + /** + * Sets the text shown in the header. + * + * @param header + * the header to set + */ + public void setHeaderCaption(String header) { + this.header = header; + grid.refreshHeader(); + } + + /** + * Gets the text shown in the footer. + * + * @return the text in the footer + */ + public String getFooterCaption() { + return footer; + } + + /** + * Sets the text displayed in the footer. + * + * @param footer + * the footer to set + */ + public void setFooterCaption(String footer) { + this.footer = footer; + grid.refreshFooter(); + } + + /** + * Returns all column in this group. It includes the subgroups columns as + * well. + * + * @return unmodifiable list of columns + */ + public List<GridColumn<?, T>> getColumns() { + return columns; + } + + /** + * Returns the renderer used for rendering the header cells + * + * @return a renderer that renders header cells + */ + public Renderer<String> getHeaderRenderer() { + return headerRenderer; + } + + /** + * Sets the renderer that renders header cells. + * + * @param renderer + * The renderer to use for rendering header cells. Must not be + * null. + * @throws IllegalArgumentException + * thrown when renderer is null + */ + public void setHeaderRenderer(Renderer<String> renderer) { + if (renderer == null) { + throw new IllegalArgumentException("Renderer cannot be null."); + } + this.headerRenderer = renderer; + grid.refreshHeader(); + } + + /** + * Returns the renderer used for rendering the footer cells + * + * @return a renderer that renders footer cells + */ + public Renderer<String> getFooterRenderer() { + return footerRenderer; + } + + /** + * Sets the renderer that renders footer cells. + * + * @param renderer + * The renderer to use for rendering footer cells. Must not be + * null. + * @throws IllegalArgumentException + * thrown when renderer is null + */ + public void setFooterRenderer(Renderer<String> renderer) { + if (renderer == null) { + throw new IllegalArgumentException("Renderer cannot be null."); + } + this.footerRenderer = renderer; + grid.refreshFooter(); + } +} diff --git a/client/src/com/vaadin/client/ui/grid/ColumnGroupRow.java b/client/src/com/vaadin/client/ui/grid/ColumnGroupRow.java new file mode 100644 index 0000000000..ebe4db508c --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/ColumnGroupRow.java @@ -0,0 +1,243 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A column group row represents an auxiliary header or footer row added to the + * grid. A column group row includes column groups that group columns together. + * + * @param <T> + * Row type + * @since 7.2 + * @author Vaadin Ltd + */ +public class ColumnGroupRow<T> { + + /** + * The column groups in this row + */ + private List<ColumnGroup<T>> groups = new ArrayList<ColumnGroup<T>>(); + + /** + * The grid associated with the column row + */ + private final Grid<T> grid; + + /** + * Is the header shown + */ + private boolean headerVisible = true; + + /** + * Is the footer shown + */ + private boolean footerVisible = false; + + /** + * Constructs a new column group row + * + * @param grid + * Grid associated with this column + * + */ + ColumnGroupRow(Grid<T> grid) { + this.grid = grid; + } + + /** + * Add a new group to the row by using column instances. + * + * @param columns + * The columns that should belong to the group + * @return a column group representing the collection of columns added to + * the group. + */ + public ColumnGroup<T> addGroup(GridColumn<?, T>... columns) + throws IllegalArgumentException { + + for (GridColumn<?, T> column : columns) { + if (isColumnGrouped(column)) { + throw new IllegalArgumentException("Column " + + String.valueOf(column.getHeaderCaption()) + + " already belongs to another group."); + } + } + + validateNewGroupProperties(Arrays.asList(columns)); + + ColumnGroup<T> group = new ColumnGroup<T>(grid, Arrays.asList(columns)); + groups.add(group); + grid.refreshHeader(); + grid.refreshFooter(); + return group; + } + + private void validateNewGroupProperties(Collection<GridColumn<?, T>> columns) { + + int rowIndex = grid.getColumnGroupRows().indexOf(this); + int parentRowIndex = rowIndex - 1; + + // Get the parent row of this row. + ColumnGroupRow<T> parentRow = null; + if (parentRowIndex > -1) { + parentRow = grid.getColumnGroupRows().get(parentRowIndex); + } + + if (parentRow == null) { + // A parentless row is always valid and is usually the first row + // added to the grid + return; + } + + for (GridColumn<?, T> column : columns) { + if (parentRow.hasColumnBeenGrouped(column)) { + /* + * If a property has been grouped in the parent row then all of + * the properties in the parent group also needs to be included + * in the child group for the groups to be valid + */ + ColumnGroup parentGroup = parentRow.getGroupForColumn(column); + if (!columns.containsAll(parentGroup.getColumns())) { + throw new IllegalArgumentException( + "Grouped properties overlaps previous grouping bounderies"); + } + } + } + } + + private boolean hasColumnBeenGrouped(GridColumn<?, T> column) { + return getGroupForColumn(column) != null; + } + + private ColumnGroup<T> getGroupForColumn(GridColumn<?, T> column) { + for (ColumnGroup<T> group : groups) { + if (group.getColumns().contains(column)) { + return group; + } + } + return null; + } + + /** + * Add a new group to the row by using other already greated groups + * + * @param groups + * The subgroups of the group. + * @return a column group representing the collection of columns added to + * the group. + * + */ + public ColumnGroup<T> addGroup(ColumnGroup<T>... groups) + throws IllegalArgumentException { + assert groups != null : "groups cannot be null"; + + Set<GridColumn<?, T>> columns = new HashSet<GridColumn<?, T>>(); + for (ColumnGroup<T> group : groups) { + columns.addAll(group.getColumns()); + } + + validateNewGroupProperties(columns); + + ColumnGroup<T> group = new ColumnGroup<T>(grid, columns); + this.groups.add(group); + grid.refreshHeader(); + grid.refreshFooter(); + return group; + } + + /** + * Removes a group from the row. + * + * @param group + * The group to remove + */ + public void removeGroup(ColumnGroup<T> group) { + groups.remove(group); + grid.refreshHeader(); + grid.refreshFooter(); + } + + /** + * Get the groups in the row + * + * @return unmodifiable list of groups in this row + */ + public List<ColumnGroup<T>> getGroups() { + return Collections.unmodifiableList(groups); + } + + /** + * Is the header visible for the row. + * + * @return <code>true</code> if header is visible + */ + public boolean isHeaderVisible() { + return headerVisible; + } + + /** + * Sets the header visible for the row. + * + * @param visible + * should the header be shown + */ + public void setHeaderVisible(boolean visible) { + headerVisible = visible; + grid.refreshHeader(); + } + + /** + * Is the footer visible for the row. + * + * @return <code>true</code> if footer is visible + */ + public boolean isFooterVisible() { + return footerVisible; + } + + /** + * Sets the footer visible for the row. + * + * @param visible + * should the footer be shown + */ + public void setFooterVisible(boolean visible) { + footerVisible = visible; + grid.refreshFooter(); + } + + /** + * Iterates all the column groups and checks if the columns alread has been + * added to a group. + */ + private boolean isColumnGrouped(GridColumn<?, T> column) { + for (ColumnGroup<T> group : groups) { + if (group.getColumns().contains(column)) { + return true; + } + } + return false; + } +} diff --git a/client/src/com/vaadin/client/ui/grid/Escalator.java b/client/src/com/vaadin/client/ui/grid/Escalator.java new file mode 100644 index 0000000000..77a8c2dbd9 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/Escalator.java @@ -0,0 +1,3660 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.logging.Logger; + +import com.google.gwt.animation.client.AnimationScheduler; +import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback; +import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle; +import com.google.gwt.core.client.Duration; +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; +import com.google.gwt.dom.client.Node; +import com.google.gwt.dom.client.NodeList; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.UIObject; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.Profiler; +import com.vaadin.client.Util; +import com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle; +import com.vaadin.client.ui.grid.PositionFunction.AbsolutePosition; +import com.vaadin.client.ui.grid.PositionFunction.Translate3DPosition; +import com.vaadin.client.ui.grid.PositionFunction.TranslatePosition; +import com.vaadin.client.ui.grid.PositionFunction.WebkitTranslate3DPosition; +import com.vaadin.client.ui.grid.ScrollbarBundle.HorizontalScrollbarBundle; +import com.vaadin.client.ui.grid.ScrollbarBundle.VerticalScrollbarBundle; +import com.vaadin.shared.ui.grid.Range; +import com.vaadin.shared.ui.grid.ScrollDestination; +import com.vaadin.shared.util.SharedUtil; + +/*- + + Maintenance Notes! Reading these might save your day. + + + == Row Container Structure + + AbstractRowContainer + |-- AbstractStaticRowContainer + | |-- HeaderRowContainer + | `-- FooterContainer + `-- BodyRowContainer + + AbstractRowContainer is intended to contain all common logic + between RowContainers. It manages the bookkeeping of row + count, makes sure that all individual cells are rendered + the same way, and so on. + + AbstractStaticRowContainer has some special logic that is + required by all RowContainers that don't scroll (hence the + word "static"). HeaderRowContainer and FooterRowContainer + are pretty thin special cases of a StaticRowContainer + (mostly relating to positioning of the root element). + + BodyRowContainer could also be split into an additional + "AbstractScrollingRowContainer", but I felt that no more + inner classes were needed. So it contains both logic + required for making things scroll about, and equivalent + special cases for layouting, as are found in + Header/FooterRowContainers. + + + == The Three Indices + + Each RowContainer can be thought to have three levels of + indices for any given displayed row (but the distinction + matters primarily for the BodyRowContainer, because of the + way it scrolls through data): + + - Logical index + - Physical (or DOM) index + - Visual index + + LOGICAL INDEX is the index that is linked to the data + source. If you want your data source to represent a SQL + database with 10 000 rows, the 7 000:th row in the SQL has a + logical index of 6 999, since the index is 0-based (unless + that data source does some funky logic). + + PHYSICAL INDEX is the index for a row that you see in a + browser's DOM inspector. If your row is the second <tr> + element within a <tbody> tag, it has a physical index of 1 + (because of 0-based indices). In Header and + FooterRowContainers, you are safe to assume that the logical + index is the same as the physical index. But because the + BodyRowContainer never displays large data sources entirely + in the DOM, a physical index usually has no apparent direct + relationship with its logical index. + + VISUAL INDEX is the index relating to the order that you + see a row in, in the browser, as it is rendered. The + topmost row is 0, the second is 1, and so on. The visual + index is similar to the physical index in the sense that + Header and FooterRowContainers can assume a 1:1 + relationship between visual index and logical index. And + again, BodyRowContainer has no such relationship. The + body's visual index has additionally no apparent + relationship with its physical index. Because the <tr> tags + are reused in the body and visually repositioned with CSS + as the user scrolls, the relationship between physical + index and visual index is quickly broken. You can get an + element's visual index via the field + BodyRowContainer.visualRowOrder. + + */ + +/** + * A workaround-class for GWT and JSNI. + * <p> + * GWT is unable to handle some method calls to Java methods in inner-classes + * from within JSNI blocks. Having that inner class implement a non-inner-class + * (or interface), makes it possible for JSNI to indirectly refer to the inner + * class, by invoking methods and fields in the non-inner-class. + * + * @see Escalator.Scroller + */ +abstract class JsniWorkaround { + /** + * A JavaScript function that handles the scroll DOM event, and passes it on + * to Java code. + * + * @see #createScrollListenerFunction(Escalator) + * @see Escalator#onScroll(double,double) + * @see Escalator.Scroller#onScroll(double, double) + */ + protected final JavaScriptObject scrollListenerFunction; + + /** + * A JavaScript function that handles the mousewheel DOM event, and passes + * it on to Java code. + * + * @see #createMousewheelListenerFunction(Escalator) + * @see Escalator#onScroll(double,double) + * @see Escalator.Scroller#onScroll(double, double) + */ + protected final JavaScriptObject mousewheelListenerFunction; + + /** + * A JavaScript function that handles the touch start DOM event, and passes + * it on to Java code. + * + * @see TouchHandlerBundle#touchStart(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent) + */ + protected JavaScriptObject touchStartFunction; + + /** + * A JavaScript function that handles the touch move DOM event, and passes + * it on to Java code. + * + * @see TouchHandlerBundle#touchMove(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent) + */ + protected JavaScriptObject touchMoveFunction; + + /** + * A JavaScript function that handles the touch end and cancel DOM events, + * and passes them on to Java code. + * + * @see TouchHandlerBundle#touchEnd(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent) + */ + protected JavaScriptObject touchEndFunction; + + protected JsniWorkaround(final Escalator escalator) { + scrollListenerFunction = createScrollListenerFunction(escalator); + mousewheelListenerFunction = createMousewheelListenerFunction(escalator); + + final TouchHandlerBundle bundle = new TouchHandlerBundle(escalator); + touchStartFunction = bundle.getTouchStartHandler(); + touchMoveFunction = bundle.getTouchMoveHandler(); + touchEndFunction = bundle.getTouchEndHandler(); + } + + /** + * A method that constructs the JavaScript function that will be stored into + * {@link #scrollListenerFunction}. + * + * @param esc + * a reference to the current instance of {@link Escalator} + * @see Escalator#onScroll(double,double) + */ + protected abstract JavaScriptObject createScrollListenerFunction( + Escalator esc); + + /** + * A method that constructs the JavaScript function that will be stored into + * {@link #mousewheelListenerFunction}. + * + * @param esc + * a reference to the current instance of {@link Escalator} + * @see Escalator#onScroll(double,double) + */ + protected abstract JavaScriptObject createMousewheelListenerFunction( + Escalator esc); +} + +/** + * A low-level table-like widget that features a scrolling virtual viewport and + * lazily generated rows. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class Escalator extends Widget { + + // todo comments legend + /* + * [[optimize]]: There's an opportunity to rewrite the code in such a way + * that it _might_ perform better (rememeber to measure, implement, + * re-measure) + */ + /* + * [[rowheight]]: This code will require alterations that are relevant for + * being able to support variable row heights. NOTE: these bits can most + * often also be identified by searching for code reading the ROW_HEIGHT_PX + * constant. + */ + /* + * [[API]]: Implementing this suggestion would require a change in the + * public API. These suggestions usually don't come lightly. + */ + /* + * [[mpixscroll]]: This code will require alterations that are relevant for + * supporting the scrolling through more pixels than some browsers normally + * would support. (i.e. when we support more than "a million" pixels in the + * escalator DOM). NOTE: these bits can most often also be identified by + * searching for code that call scrollElem.getScrollTop();. + */ + + /** + * A utility class that contains utility methods that are usually called + * from JSNI. + * <p> + * The methods are moved in this class to minimize the amount of JSNI code + * as much as feasible. + */ + static class JsniUtil { + public static class TouchHandlerBundle { + + /** + * A <a href= + * "http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsOverlay.html" + * >JavaScriptObject overlay</a> for the <a + * href="http://www.w3.org/TR/touch-events/">JavaScript + * TouchEvent</a> object. + * <p> + * This needs to be used in the touch event handlers, since GWT's + * {@link com.google.gwt.event.dom.client.TouchEvent TouchEvent} + * can't be cast from the JSNI call, and the + * {@link com.google.gwt.dom.client.NativeEvent NativeEvent} isn't + * properly populated with the correct values. + */ + private final static class CustomTouchEvent extends + JavaScriptObject { + protected CustomTouchEvent() { + } + + public native NativeEvent getNativeEvent() + /*-{ + return this; + }-*/; + + public native int getPageX() + /*-{ + return this.targetTouches[0].pageX; + }-*/; + + public native int getPageY() + /*-{ + return this.targetTouches[0].pageY; + }-*/; + } + + private double touches = 0; + private int lastX = 0; + private int lastY = 0; + private double lastTime = 0; + private boolean snappedScrollEnabled = true; + private double deltaX = 0; + private double deltaY = 0; + + private final Escalator escalator; + private CustomTouchEvent latestTouchMoveEvent; + private AnimationCallback mover = new AnimationCallback() { + @Override + public void execute(double timestamp) { + if (touches != 1) { + return; + } + + final int x = latestTouchMoveEvent.getPageX(); + final int y = latestTouchMoveEvent.getPageY(); + deltaX = x - lastX; + deltaY = y - lastY; + lastX = x; + lastY = y; + lastTime = timestamp; + + // snap the scroll to the major axes, at first. + if (snappedScrollEnabled) { + final double oldDeltaX = deltaX; + final double oldDeltaY = deltaY; + + /* + * Scrolling snaps to 40 degrees vs. flick scroll's 30 + * degrees, since slow movements have poor resolution - + * it's easy to interpret a slight angle as a steep + * angle, since the sample rate is "unnecessarily" high. + * 40 simply felt better than 30. + */ + final double[] snapped = Escalator.snapDeltas(deltaX, + deltaY, RATIO_OF_40_DEGREES); + deltaX = snapped[0]; + deltaY = snapped[1]; + + /* + * if the snap failed once, let's follow the pointer + * from now on. + */ + if (oldDeltaX != 0 && deltaX == oldDeltaX + && oldDeltaY != 0 && deltaY == oldDeltaY) { + snappedScrollEnabled = false; + } + } + + moveScrollFromEvent(escalator, -deltaX, -deltaY, + latestTouchMoveEvent.getNativeEvent()); + } + }; + private AnimationHandle animationHandle; + + public TouchHandlerBundle(final Escalator escalator) { + this.escalator = escalator; + } + + public native JavaScriptObject getTouchStartHandler() + /*-{ + // we need to store "this", since it won't be preserved on call. + var self = this; + return $entry(function (e) { + self.@com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle::touchStart(*)(e); + }); + }-*/; + + public native JavaScriptObject getTouchMoveHandler() + /*-{ + // we need to store "this", since it won't be preserved on call. + var self = this; + return $entry(function (e) { + self.@com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle::touchMove(*)(e); + }); + }-*/; + + public native JavaScriptObject getTouchEndHandler() + /*-{ + // we need to store "this", since it won't be preserved on call. + var self = this; + return $entry(function (e) { + self.@com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle::touchEnd(*)(e); + }); + }-*/; + + public void touchStart(final CustomTouchEvent event) { + touches++; + if (touches != 1) { + return; + } + + escalator.scroller.cancelFlickScroll(); + + lastX = event.getPageX(); + lastY = event.getPageY(); + + snappedScrollEnabled = true; + } + + public void touchMove(final CustomTouchEvent event) { + /* + * since we only use the getPageX/Y, and calculate the diff + * within the handler, we don't need to calculate any + * intermediate deltas. + */ + latestTouchMoveEvent = event; + + if (animationHandle != null) { + animationHandle.cancel(); + } + animationHandle = AnimationScheduler.get() + .requestAnimationFrame(mover, escalator.bodyElem); + event.getNativeEvent().preventDefault(); + mover.execute(Duration.currentTimeMillis()); + } + + public void touchEnd(@SuppressWarnings("unused") + final CustomTouchEvent event) { + touches--; + + if (touches == 0) { + escalator.scroller.handleFlickScroll(deltaX, deltaY, + lastTime); + } + } + } + + public static void moveScrollFromEvent(final Escalator escalator, + final double deltaX, final double deltaY, + final NativeEvent event) { + + if (!Double.isNaN(deltaX)) { + escalator.horizontalScrollbar.setScrollPosByDelta((int) deltaX); + } + + if (!Double.isNaN(deltaY)) { + escalator.verticalScrollbar.setScrollPosByDelta((int) deltaY); + } + + /* + * TODO: only prevent if not scrolled to end/bottom. Or no? UX team + * needs to decide. + */ + final boolean warrantedYScroll = deltaY != 0 + && escalator.verticalScrollbar.showsScrollHandle(); + final boolean warrantedXScroll = deltaX != 0 + && escalator.horizontalScrollbar.showsScrollHandle(); + if (warrantedYScroll || warrantedXScroll) { + event.preventDefault(); + } + } + } + + /** + * The animation callback that handles the animation of a touch-scrolling + * flick with inertia. + */ + private class FlickScrollAnimator implements AnimationCallback { + private static final double MIN_MAGNITUDE = 0.005; + private static final double MAX_SPEED = 7; + + private double velX; + private double velY; + private double prevTime = 0; + private int millisLeft; + private double xFric; + private double yFric; + + private boolean cancelled = false; + + /** + * Creates a new animation callback to handle touch-scrolling flick with + * inertia. + * + * @param deltaX + * the last scrolling delta in the x-axis in a touchmove + * @param deltaY + * the last scrolling delta in the y-axis in a touchmove + * @param lastTime + * the timestamp of the last touchmove + */ + public FlickScrollAnimator(final double deltaX, final double deltaY, + final double lastTime) { + final double currentTimeMillis = Duration.currentTimeMillis(); + velX = Math.max(Math.min(deltaX / (currentTimeMillis - lastTime), + MAX_SPEED), -MAX_SPEED); + velY = Math.max(Math.min(deltaY / (currentTimeMillis - lastTime), + MAX_SPEED), -MAX_SPEED); + prevTime = lastTime; + + /* + * If we're scrolling mainly in one of the four major directions, + * and only a teeny bit to any other side, snap the scroll to that + * major direction instead. + */ + final double[] snapDeltas = Escalator.snapDeltas(velX, velY, + RATIO_OF_30_DEGREES); + velX = snapDeltas[0]; + velY = snapDeltas[1]; + + if (velX * velX + velY * velY > MIN_MAGNITUDE) { + millisLeft = 1500; + xFric = velX / millisLeft; + yFric = velY / millisLeft; + } else { + millisLeft = 0; + } + + } + + @Override + public void execute(final double timestamp) { + if (millisLeft <= 0 || cancelled) { + scroller.currentFlickScroller = null; + return; + } + + final int lastLeft = tBodyScrollLeft; + final int lastTop = tBodyScrollTop; + + final double timeDiff = timestamp - prevTime; + setScrollLeft((int) (tBodyScrollLeft - velX * timeDiff)); + velX -= xFric * timeDiff; + + setScrollTop(tBodyScrollTop - velY * timeDiff); + velY -= yFric * timeDiff; + + cancelBecauseOfEdgeOrCornerMaybe(lastLeft, lastTop); + + prevTime = timestamp; + millisLeft -= timeDiff; + AnimationScheduler.get().requestAnimationFrame(this); + } + + private void cancelBecauseOfEdgeOrCornerMaybe(final int lastLeft, + final int lastTop) { + if (lastLeft == horizontalScrollbar.getScrollPos() + && lastTop == verticalScrollbar.getScrollPos()) { + cancel(); + } + } + + public void cancel() { + cancelled = true; + } + } + + private static final int ROW_HEIGHT_PX = 20; + + /** + * ScrollDestination case-specific handling logic. + */ + private static double getScrollPos(final ScrollDestination destination, + final double targetStartPx, final double targetEndPx, + final double viewportStartPx, final double viewportEndPx, + final int padding) { + + final double viewportLength = viewportEndPx - viewportStartPx; + + switch (destination) { + + /* + * Scroll as little as possible to show the target element. If the + * element fits into view, this works as START or END depending on the + * current scroll position. If the element does not fit into view, this + * works as START. + */ + case ANY: { + final double startScrollPos = targetStartPx - padding; + final double endScrollPos = targetEndPx + padding - viewportLength; + + if (startScrollPos < viewportStartPx) { + return startScrollPos; + } else if (targetEndPx + padding > viewportEndPx) { + return endScrollPos; + } else { + // NOOP, it's already visible + return viewportStartPx; + } + } + + /* + * Scrolls so that the element is shown at the end of the viewport. The + * viewport will, however, not scroll before its first element. + */ + case END: { + return targetEndPx + padding - viewportLength; + } + + /* + * Scrolls so that the element is shown in the middle of the viewport. + * The viewport will, however, not scroll beyond its contents, given + * more elements than what the viewport is able to show at once. Under + * no circumstances will the viewport scroll before its first element. + */ + case MIDDLE: { + final double targetMiddle = targetStartPx + + (targetEndPx - targetStartPx) / 2; + return targetMiddle - viewportLength / 2; + } + + /* + * Scrolls so that the element is shown at the start of the viewport. + * The viewport will, however, not scroll beyond its contents. + */ + case START: { + return targetStartPx - padding; + } + + /* + * Throw an error if we're here. This can only mean that + * ScrollDestination has been carelessly amended.. + */ + default: { + throw new IllegalArgumentException( + "Internal: ScrollDestination has been modified, " + + "but Escalator.getScrollPos has not been updated " + + "to match new values."); + } + } + + } + + /** An inner class that handles all logic related to scrolling. */ + private class Scroller extends JsniWorkaround { + private double lastScrollTop = 0; + private double lastScrollLeft = 0; + /** + * The current flick scroll animator. This is <code>null</code> if the + * view isn't animating a flick scroll at the moment. + */ + private FlickScrollAnimator currentFlickScroller; + + public Scroller() { + super(Escalator.this); + } + + @Override + protected native JavaScriptObject createScrollListenerFunction( + Escalator esc) + /*-{ + var vScroll = esc.@com.vaadin.client.ui.grid.Escalator::verticalScrollbar; + var vScrollElem = vScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::getElement()(); + + var hScroll = esc.@com.vaadin.client.ui.grid.Escalator::horizontalScrollbar; + var hScrollElem = hScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::getElement()(); + + return $entry(function(e) { + var target = e.target || e.srcElement; // IE8 uses e.scrElement + + // in case the scroll event was native (i.e. scrollbars were dragged, or + // the scrollTop/Left was manually modified), the bundles have old cache + // values. We need to make sure that the caches are kept up to date. + if (target === vScrollElem) { + vScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::updateScrollPosFromDom()(); + } else if (target === hScrollElem) { + hScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::updateScrollPosFromDom()(); + } else { + $wnd.console.error("unexpected scroll target: "+target); + } + + esc.@com.vaadin.client.ui.grid.Escalator::onScroll()(); + }); + }-*/; + + @Override + protected native JavaScriptObject createMousewheelListenerFunction( + Escalator esc) + /*-{ + return $entry(function(e) { + var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX; + var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY; + + // IE8 has only delta y + if (isNaN(deltaY)) { + deltaY = -0.5*e.wheelDelta; + } + + @com.vaadin.client.ui.grid.Escalator.JsniUtil::moveScrollFromEvent(*)(esc, deltaX, deltaY, e); + }); + }-*/; + + /** + * Recalculates the virtual viewport represented by the scrollbars, so + * that the sizes of the scroll handles appear correct in the browser + */ + public void recalculateScrollbarsForVirtualViewport() { + int scrollContentHeight = ROW_HEIGHT_PX * body.getRowCount(); + int scrollContentWidth = columnConfiguration.calculateRowWidth(); + + double tableWrapperHeight = height; + double tableWrapperWidth = width; + + boolean verticalScrollNeeded = scrollContentHeight > tableWrapperHeight + - header.height - footer.height; + boolean horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth; + + // One dimension got scrollbars, but not the other. Recheck time! + if (verticalScrollNeeded != horizontalScrollNeeded) { + if (!verticalScrollNeeded && horizontalScrollNeeded) { + verticalScrollNeeded = scrollContentHeight > tableWrapperHeight + - header.height + - footer.height + - horizontalScrollbar.getScrollbarThickness(); + } else { + horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth + - verticalScrollbar.getScrollbarThickness(); + } + } + + // let's fix the table wrapper size, since it's now stable. + if (verticalScrollNeeded) { + tableWrapperWidth -= verticalScrollbar.getScrollbarThickness(); + } + if (horizontalScrollNeeded) { + tableWrapperHeight -= horizontalScrollbar + .getScrollbarThickness(); + } + tableWrapper.getStyle().setHeight(tableWrapperHeight, Unit.PX); + tableWrapper.getStyle().setWidth(tableWrapperWidth, Unit.PX); + + verticalScrollbar.setOffsetSize((int) (tableWrapperHeight + - footer.height - header.height)); + verticalScrollbar.setScrollSize(scrollContentHeight); + + /* + * If decreasing the amount of frozen columns, and scrolled to the + * right, the scroll position might reset. So we need to remember + * the scroll position, and re-apply it once the scrollbar size has + * been adjusted. + */ + int prevScrollPos = horizontalScrollbar.getScrollPos(); + + int unfrozenPixels = columnConfiguration + .getCalculatedColumnsWidth(Range.between( + columnConfiguration.getFrozenColumnCount(), + columnConfiguration.getColumnCount())); + int frozenPixels = scrollContentWidth - unfrozenPixels; + double hScrollOffsetWidth = tableWrapperWidth - frozenPixels; + horizontalScrollbar.setOffsetSize((int) hScrollOffsetWidth); + horizontalScrollbar.setScrollSize(unfrozenPixels); + horizontalScrollbar.getElement().getStyle() + .setLeft(frozenPixels, Unit.PX); + horizontalScrollbar.setScrollPos(prevScrollPos); + } + + /** + * Logical scrolling event handler for the entire widget. + * + * @param scrollLeft + * the current number of pixels that the user has scrolled + * from left + * @param scrollTop + * the current number of pixels that the user has scrolled + * from the top + */ + public void onScroll() { + if (internalScrollEventCalls > 0) { + internalScrollEventCalls--; + return; + } + + final int scrollLeft = horizontalScrollbar.getScrollPos(); + final int scrollTop = verticalScrollbar.getScrollPos(); + + if (lastScrollLeft != scrollLeft) { + for (int i = 0; i < columnConfiguration.frozenColumns; i++) { + header.updateFreezePosition(i, scrollLeft); + body.updateFreezePosition(i, scrollLeft); + footer.updateFreezePosition(i, scrollLeft); + } + + position.set(headElem, -scrollLeft, 0); + + /* + * TODO [[optimize]]: cache this value in case the instanceof + * check has undesirable overhead. This could also be a + * candidate for some deferred binding magic so that e.g. + * AbsolutePosition is not even considered in permutations that + * we know support something better. That would let the compiler + * completely remove the entire condition since it knows that + * the if will never be true. + */ + if (position instanceof AbsolutePosition) { + /* + * we don't want to put "top: 0" on the footer, since it'll + * render wrong, as we already have + * "bottom: $footer-height". + */ + footElem.getStyle().setLeft(-scrollLeft, Unit.PX); + } else { + position.set(footElem, -scrollLeft, 0); + } + + lastScrollLeft = scrollLeft; + } + + body.setBodyScrollPosition(scrollLeft, scrollTop); + + lastScrollTop = scrollTop; + body.updateEscalatorRowsOnScroll(); + /* + * TODO [[optimize]]: Might avoid a reflow by first calculating new + * scrolltop and scrolleft, then doing the escalator magic based on + * those numbers and only updating the positions after that. + */ + } + + public native void attachScrollListener(Element element) + /* + * Attaching events with JSNI instead of the GWT event mechanism because + * GWT didn't provide enough details in events, or triggering the event + * handlers with GWT bindings was unsuccessful. Maybe, with more time + * and skill, it could be done with better success. JavaScript overlay + * types might work. This might also get rid of the JsniWorkaround + * class. + */ + /*-{ + if (element.addEventListener) { + element.addEventListener("scroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction); + } else { + element.attachEvent("onscroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction); + } + }-*/; + + public native void detachScrollListener(Element element) + /* + * Attaching events with JSNI instead of the GWT event mechanism because + * GWT didn't provide enough details in events, or triggering the event + * handlers with GWT bindings was unsuccessful. Maybe, with more time + * and skill, it could be done with better success. JavaScript overlay + * types might work. This might also get rid of the JsniWorkaround + * class. + */ + /*-{ + if (element.addEventListener) { + element.removeEventListener("scroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction); + } else { + element.detachEvent("onscroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction); + } + }-*/; + + public native void attachMousewheelListener(Element element) + /* + * Attaching events with JSNI instead of the GWT event mechanism because + * GWT didn't provide enough details in events, or triggering the event + * handlers with GWT bindings was unsuccessful. Maybe, with more time + * and skill, it could be done with better success. JavaScript overlay + * types might work. This might also get rid of the JsniWorkaround + * class. + */ + /*-{ + if (element.addEventListener) { + // firefox likes "wheel", while others use "mousewheel" + var eventName = element.onwheel===undefined?"mousewheel":"wheel"; + element.addEventListener(eventName, this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction); + } else { + // IE8 + element.attachEvent("onmousewheel", this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction); + } + }-*/; + + public native void detachMousewheelListener(Element element) + /* + * Detaching events with JSNI instead of the GWT event mechanism because + * GWT didn't provide enough details in events, or triggering the event + * handlers with GWT bindings was unsuccessful. Maybe, with more time + * and skill, it could be done with better success. JavaScript overlay + * types might work. This might also get rid of the JsniWorkaround + * class. + */ + /*-{ + if (element.addEventListener) { + // firefox likes "wheel", while others use "mousewheel" + var eventName = element.onwheel===undefined?"mousewheel":"wheel"; + element.removeEventListener(eventName, this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction); + } else { + // IE8 + element.detachEvent("onmousewheel", this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction); + } + }-*/; + + public native void attachTouchListeners(Element element) + /* + * Detaching events with JSNI instead of the GWT event mechanism because + * GWT didn't provide enough details in events, or triggering the event + * handlers with GWT bindings was unsuccessful. Maybe, with more time + * and skill, it could be done with better success. JavaScript overlay + * types might work. This might also get rid of the JsniWorkaround + * class. + */ + /*-{ + if (element.addEventListener) { + element.addEventListener("touchstart", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchStartFunction); + element.addEventListener("touchmove", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchMoveFunction); + element.addEventListener("touchend", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction); + element.addEventListener("touchcancel", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction); + } else { + // this would be IE8, but we don't support it with touch + } + }-*/; + + public native void detachTouchListeners(Element element) + /* + * Detaching events with JSNI instead of the GWT event mechanism because + * GWT didn't provide enough details in events, or triggering the event + * handlers with GWT bindings was unsuccessful. Maybe, with more time + * and skill, it could be done with better success. JavaScript overlay + * types might work. This might also get rid of the JsniWorkaround + * class. + */ + /*-{ + if (element.removeEventListener) { + element.removeEventListener("touchstart", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchStartFunction); + element.removeEventListener("touchmove", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchMoveFunction); + element.removeEventListener("touchend", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction); + element.removeEventListener("touchcancel", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction); + } else { + // this would be IE8, but we don't support it with touch + } + }-*/; + + private void cancelFlickScroll() { + if (currentFlickScroller != null) { + currentFlickScroller.cancel(); + } + } + + /** + * Handles a touch-based flick scroll. + * + * @param deltaX + * the last scrolling delta in the x-axis in a touchmove + * @param deltaY + * the last scrolling delta in the y-axis in a touchmove + * @param lastTime + * the timestamp of the last touchmove + */ + public void handleFlickScroll(double deltaX, double deltaY, + double lastTime) { + currentFlickScroller = new FlickScrollAnimator(deltaX, deltaY, + lastTime); + AnimationScheduler.get() + .requestAnimationFrame(currentFlickScroller); + } + + public void scrollToColumn(final int columnIndex, + final ScrollDestination destination, final int padding) { + assert columnIndex >= columnConfiguration.frozenColumns : "Can't scroll to a frozen column"; + + /* + * To cope with frozen columns, we just pretend those columns are + * not there at all when calculating the position of the target + * column and the boundaries of the viewport. The resulting + * scrollLeft will be correct without compensation since the DOM + * structure effectively means that scrollLeft also ignores the + * frozen columns. + */ + final int frozenPixels = columnConfiguration + .getCalculatedColumnsWidth(Range.withLength(0, + columnConfiguration.frozenColumns)); + + final int targetStartPx = columnConfiguration + .getCalculatedColumnsWidth(Range.withLength(0, columnIndex)) + - frozenPixels; + final int targetEndPx = targetStartPx + + columnConfiguration.getColumnWidthActual(columnIndex); + + final int viewportStartPx = getScrollLeft(); + int viewportEndPx = viewportStartPx + getElement().getOffsetWidth() + - frozenPixels; + if (verticalScrollbar.showsScrollHandle()) { + viewportEndPx -= Util.getNativeScrollbarSize(); + } + + final double scrollLeft = getScrollPos(destination, targetStartPx, + targetEndPx, viewportStartPx, viewportEndPx, padding); + + /* + * note that it doesn't matter if the scroll would go beyond the + * content, since the browser will adjust for that, and everything + * fall into line accordingly. + */ + setScrollLeft((int) scrollLeft); + } + + public void scrollToRow(final int rowIndex, + final ScrollDestination destination, final int padding) { + // TODO [[rowheight]] + final int targetStartPx = ROW_HEIGHT_PX * rowIndex; + final int targetEndPx = targetStartPx + ROW_HEIGHT_PX; + + final double viewportStartPx = getScrollTop(); + final double viewportEndPx = viewportStartPx + + body.calculateHeight(); + + final double scrollTop = getScrollPos(destination, targetStartPx, + targetEndPx, viewportStartPx, viewportEndPx, padding); + + /* + * note that it doesn't matter if the scroll would go beyond the + * content, since the browser will adjust for that, and everything + * falls into line accordingly. + */ + setScrollTop(scrollTop); + } + } + + private abstract class AbstractRowContainer implements RowContainer { + private EscalatorUpdater updater = EscalatorUpdater.NULL; + + private int rows; + + /** + * The table section element ({@code <thead>}, {@code <tbody>} or + * {@code <tfoot>}) the rows (i.e. {@code <tr>} tags) are contained in. + */ + protected final Element root; + + /** The height of the combined rows in the DOM. */ + protected double height = -1; + + /** + * The primary style name of the escalator. Most commonly provided by + * Escalator as "v-escalator". + */ + private String primaryStyleName = null; + + public AbstractRowContainer(final Element rowContainerElement) { + root = rowContainerElement; + } + + /** + * Informs the row container that the height of its respective table + * section has changed. + * <p> + * These calculations might affect some layouting logic, such as the + * body is being offset by the footer, the footer needs to be readjusted + * according to its height, and so on. + * <p> + * A table section is either header, body or footer. + * + */ + protected void sectionHeightCalculated() { + // Does nothing by default. Override to catch the "event". + } + + /** + * Gets the tag name of an element to represent a cell in a row. + * <p> + * Usually {@code "th"} or {@code "td"}. + * <p> + * <em>Note:</em> To actually <em>create</em> such an element, use + * {@link #createCellElement()} instead. + * + * @return the tag name for the element to represent cells as + * @see #createCellElement() + */ + protected abstract String getCellElementTagName(); + + @Override + public EscalatorUpdater getEscalatorUpdater() { + return updater; + } + + /** + * {@inheritDoc} + * <p> + * <em>Implementation detail:</em> This method does no DOM modifications + * (i.e. is very cheap to call) if there is no data for rows or columns + * when this method is called. + * + * @see #hasColumnAndRowData() + */ + @Override + public void setEscalatorUpdater(final EscalatorUpdater escalatorUpdater) { + if (escalatorUpdater == null) { + throw new IllegalArgumentException( + "escalator updater cannot be null"); + } + + updater = escalatorUpdater; + + if (hasColumnAndRowData() && getRowCount() > 0) { + refreshRows(0, getRowCount()); + } + } + + /** + * {@inheritDoc} + * <p> + * <em>Implementation detail:</em> This method does no DOM modifications + * (i.e. is very cheap to call) if there are no rows in the DOM when + * this method is called. + * + * @see #hasSomethingInDom() + */ + @Override + public void removeRows(final int index, final int numberOfRows) { + assertArgumentsAreValidAndWithinRange(index, numberOfRows); + + rows -= numberOfRows; + + if (!isAttached()) { + return; + } + + if (hasSomethingInDom()) { + paintRemoveRows(index, numberOfRows); + } + } + + protected abstract void paintRemoveRows(final int index, + final int numberOfRows); + + private void assertArgumentsAreValidAndWithinRange(final int index, + final int numberOfRows) throws IllegalArgumentException, + IndexOutOfBoundsException { + if (numberOfRows < 1) { + throw new IllegalArgumentException( + "Number of rows must be 1 or greater (was " + + numberOfRows + ")"); + } + + if (index < 0 || index + numberOfRows > getRowCount()) { + throw new IndexOutOfBoundsException("The given " + + "row range (" + index + ".." + (index + numberOfRows) + + ") was outside of the current number of rows (" + + getRowCount() + ")"); + } + } + + @Override + public int getRowCount() { + return rows; + } + + /** + * {@inheritDoc} + * <p> + * <em>Implementation detail:</em> This method does no DOM modifications + * (i.e. is very cheap to call) if there is no data for columns when + * this method is called. + * + * @see #hasColumnAndRowData() + */ + @Override + public void insertRows(final int index, final int numberOfRows) { + if (index < 0 || index > getRowCount()) { + throw new IndexOutOfBoundsException("The given index (" + index + + ") was outside of the current number of rows (0.." + + getRowCount() + ")"); + } + + if (numberOfRows < 1) { + throw new IllegalArgumentException( + "Number of rows must be 1 or greater (was " + + numberOfRows + ")"); + } + + rows += numberOfRows; + + /* + * only add items in the DOM if the widget itself is attached to the + * DOM. We can't calculate sizes otherwise. + */ + if (isAttached()) { + paintInsertRows(index, numberOfRows); + } + } + + /** + * Actually add rows into the DOM, now that everything can be + * calculated. + * + * @param visualIndex + * the DOM index to add rows into + * @param numberOfRows + * the number of rows to insert + * @return a list of the added row elements + */ + protected List<Element> paintInsertRows(final int visualIndex, + final int numberOfRows) { + assert isAttached() : "Can't paint rows if Escalator is not attached"; + + final List<Element> addedRows = new ArrayList<Element>(); + + if (numberOfRows < 1) { + return addedRows; + } + + Node referenceRow; + if (root.getChildCount() != 0 && visualIndex != 0) { + // get the row node we're inserting stuff after + referenceRow = root.getChild(visualIndex - 1); + } else { + // index is 0, so just prepend. + referenceRow = null; + } + + for (int row = visualIndex; row < visualIndex + numberOfRows; row++) { + final int rowHeight = ROW_HEIGHT_PX; + final Element tr = DOM.createTR(); + addedRows.add(tr); + tr.addClassName(getStylePrimaryName() + "-row"); + referenceRow = insertAfterReferenceAndUpdateIt(root, tr, + referenceRow); + + for (int col = 0; col < columnConfiguration.getColumnCount(); col++) { + final int colWidth = columnConfiguration + .getColumnWidthActual(col); + final Element cellElem = createCellElement(rowHeight, + colWidth); + tr.appendChild(cellElem); + + // Set stylename and position if new cell is frozen + if (col < columnConfiguration.frozenColumns) { + cellElem.addClassName("frozen"); + position.set(cellElem, scroller.lastScrollLeft, 0); + } + } + + refreshRow(tr, row); + } + reapplyRowWidths(); + + recalculateSectionHeight(); + + return addedRows; + } + + private Node insertAfterReferenceAndUpdateIt(final Element parent, + final Element elem, final Node referenceNode) { + if (referenceNode != null) { + parent.insertAfter(elem, referenceNode); + } else { + /* + * referencenode being null means we have offset 0, i.e. make it + * the first row + */ + /* + * TODO [[optimize]]: Is insertFirst or append faster for an + * empty root? + */ + parent.insertFirst(elem); + } + return elem; + } + + protected void recalculateSectionHeight() { + Profiler.enter("Escalator.AbstractRowContainer.recalculateSectionHeight"); + final double newHeight = root.getChildCount() * ROW_HEIGHT_PX; + if (newHeight != height) { + height = newHeight; + sectionHeightCalculated(); + } + Profiler.leave("Escalator.AbstractRowContainer.recalculateSectionHeight"); + } + + /** + * {@inheritDoc} + * <p> + * <em>Implementation detail:</em> This method does no DOM modifications + * (i.e. is very cheap to call) if there is no data for columns when + * this method is called. + * + * @see #hasColumnAndRowData() + */ + @Override + public void refreshRows(final int index, final int numberOfRows) { + Profiler.enter("Escalator.AbstractRowContainer.refreshRows"); + + assertArgumentsAreValidAndWithinRange(index, numberOfRows); + + if (!isAttached()) { + return; + } + + /* + * TODO [[rowheight]]: even if no rows are evaluated in the current + * viewport, the heights of some unrendered rows might change in a + * refresh. This would cause the scrollbar to be adjusted (in + * scrollHeight and/or scrollTop). Do we want to take this into + * account? + */ + if (hasColumnAndRowData()) { + /* + * TODO [[rowheight]]: nudge rows down with + * refreshRowPositions() as needed + */ + for (int row = index; row < index + numberOfRows; row++) { + final Node tr = getTrByVisualIndex(row); + refreshRow(tr, row); + } + } + + Profiler.leave("Escalator.AbstractRowContainer.refreshRows"); + } + + void refreshRow(final Node tr, final int logicalRowIndex) { + flyweightRow.setup((Element) tr, logicalRowIndex, + columnConfiguration.getCalculatedColumnWidths()); + updater.updateCells(flyweightRow, flyweightRow.getCells()); + + /* + * the "assert" guarantees that this code is run only during + * development/debugging. + */ + assert flyweightRow.teardown(); + } + + /** + * Create and setup an empty cell element. + * + * @param width + * the width of the cell, in pixels + * @param height + * the height of the cell, in pixels + * + * @return a set-up empty cell element + */ + @SuppressWarnings("hiding") + public Element createCellElement(final int height, final int width) { + final Element cellElem = DOM.createElement(getCellElementTagName()); + cellElem.getStyle().setHeight(height, Unit.PX); + cellElem.getStyle().setWidth(width, Unit.PX); + cellElem.addClassName(getStylePrimaryName() + "-cell"); + return cellElem; + } + + /** + * Gets the child element that is visually at a certain index + * + * @param index + * the index of the element to retrieve + * @return the element at position {@code index} + * @throws IndexOutOfBoundsException + * if {@code index} is not valid within {@link #root} + */ + abstract protected Element getTrByVisualIndex(int index) + throws IndexOutOfBoundsException; + + abstract protected int getTopVisualRowLogicalIndex(); + + protected void paintRemoveColumns(final int offset, + final int numberOfColumns, + final List<ColumnConfigurationImpl.Column> removedColumns) { + final NodeList<Node> childNodes = root.getChildNodes(); + for (int visualRowIndex = 0; visualRowIndex < childNodes + .getLength(); visualRowIndex++) { + final Node tr = childNodes.getItem(visualRowIndex); + + for (int column = 0; column < numberOfColumns; column++) { + Element cellElement = tr.getChild(offset).cast(); + detachPossibleWidgetFromCell(cellElement); + cellElement.removeFromParent(); + } + } + reapplyRowWidths(); + + final int firstRemovedColumnLeft = columnConfiguration + .getCalculatedColumnsWidth(Range.withLength(0, offset)); + final boolean columnsWereRemovedFromLeftOfTheViewport = scroller.lastScrollLeft > firstRemovedColumnLeft; + + if (columnsWereRemovedFromLeftOfTheViewport) { + int removedColumnsPxAmount = 0; + for (ColumnConfigurationImpl.Column removedColumn : removedColumns) { + removedColumnsPxAmount += removedColumn + .getCalculatedWidth(); + } + final int leftByDiff = (int) (scroller.lastScrollLeft - removedColumnsPxAmount); + final int newScrollLeft = Math.max(firstRemovedColumnLeft, + leftByDiff); + horizontalScrollbar.setScrollPos(newScrollLeft); + } + + // this needs to be after the scroll position adjustment above. + scroller.recalculateScrollbarsForVirtualViewport(); + + /* + * Because we might remove columns where affected by colspans, it's + * easiest to simply redraw everything when columns are modified. + * + * Yes, this is a TODO [[optimize]]. + */ + if (getRowCount() > 0 + && getColumnConfiguration().getColumnCount() > 0) { + refreshRows(0, getRowCount()); + } + } + + void detachPossibleWidgetFromCell(Node cellNode) { + // Detach possible widget + Widget widget = getWidgetFromCell(cellNode); + if (widget != null) { + // Orphan. + setParent(widget, null); + + // Physical detach. + cellNode.removeChild(widget.getElement()); + } + } + + protected void paintInsertColumns(final int offset, + final int numberOfColumns, boolean frozen) { + final NodeList<Node> childNodes = root.getChildNodes(); + + for (int row = 0; row < childNodes.getLength(); row++) { + final int rowHeight = ROW_HEIGHT_PX; + final Element tr = getTrByVisualIndex(row); + + Node referenceCell; + if (offset != 0) { + referenceCell = tr.getChild(offset - 1); + } else { + referenceCell = null; + } + + for (int col = offset; col < offset + numberOfColumns; col++) { + final int colWidth = columnConfiguration + .getColumnWidthActual(col); + final Element cellElem = createCellElement(rowHeight, + colWidth); + referenceCell = insertAfterReferenceAndUpdateIt(tr, + cellElem, referenceCell); + } + } + reapplyRowWidths(); + + if (frozen) { + for (int col = offset; col < offset + numberOfColumns; col++) { + setColumnFrozen(col, true); + } + } + + // this needs to be before the scrollbar adjustment. + scroller.recalculateScrollbarsForVirtualViewport(); + + int pixelsToInsertedColumn = columnConfiguration + .getCalculatedColumnsWidth(Range.withLength(0, offset)); + final boolean columnsWereAddedToTheLeftOfViewport = scroller.lastScrollLeft > pixelsToInsertedColumn; + + if (columnsWereAddedToTheLeftOfViewport) { + int insertedColumnsWidth = columnConfiguration + .getCalculatedColumnsWidth(Range.withLength(offset, + numberOfColumns)); + horizontalScrollbar + .setScrollPos((int) (scroller.lastScrollLeft + insertedColumnsWidth)); + } + + /* + * Because we might insert columns where affected by colspans, it's + * easiest to simply redraw everything when columns are modified. + * + * Yes, this is a TODO [[optimize]]. + */ + if (getRowCount() > 0 + && getColumnConfiguration().getColumnCount() > 1) { + refreshRows(0, getRowCount()); + } + } + + public void setColumnFrozen(int column, boolean frozen) { + final NodeList<Node> childNodes = root.getChildNodes(); + + for (int row = 0; row < childNodes.getLength(); row++) { + final Element tr = childNodes.getItem(row).cast(); + + Element cell = (Element) tr.getChild(column); + if (frozen) { + cell.addClassName("frozen"); + } else { + cell.removeClassName("frozen"); + position.reset(cell); + } + } + + if (frozen) { + updateFreezePosition(column, scroller.lastScrollLeft); + } + } + + public void updateFreezePosition(int column, double scrollLeft) { + final NodeList<Node> childNodes = root.getChildNodes(); + + for (int row = 0; row < childNodes.getLength(); row++) { + final Element tr = childNodes.getItem(row).cast(); + + Element cell = (Element) tr.getChild(column); + position.set(cell, scrollLeft, 0); + } + } + + /** + * Iterates through all the cells in a column and returns the width of + * the widest element in this RowContainer. + * + * @param index + * the index of the column to inspect + * @return the pixel width of the widest element in the indicated column + */ + public int calculateMaxColWidth(int index) { + Element row = root.getFirstChildElement(); + int maxWidth = 0; + while (row != null) { + final Element cell = (Element) row.getChild(index); + final boolean isVisible = !cell.getStyle().getDisplay() + .equals(Display.NONE.getCssName()); + if (isVisible) { + maxWidth = Math.max(maxWidth, cell.getScrollWidth()); + } + row = row.getNextSiblingElement(); + } + return maxWidth; + } + + /** + * Reapplies all the cells' widths according to the calculated widths in + * the column configuration. + */ + public void reapplyColumnWidths() { + Element row = root.getFirstChildElement(); + while (row != null) { + Element cell = row.getFirstChildElement(); + int columnIndex = 0; + while (cell != null) { + @SuppressWarnings("hiding") + final int width = getCalculatedColumnWidthWithColspan(cell, + columnIndex); + + /* + * TODO Should Escalator implement ProvidesResize at some + * point, this is where we need to do that. + */ + cell.getStyle().setWidth(width, Unit.PX); + + cell = cell.getNextSiblingElement(); + columnIndex++; + } + row = row.getNextSiblingElement(); + } + + reapplyRowWidths(); + } + + private int getCalculatedColumnWidthWithColspan(final Element cell, + final int columnIndex) { + final int colspan = cell.getPropertyInt(FlyweightCell.COLSPAN_ATTR); + Range spannedColumns = Range.withLength(columnIndex, colspan); + + /* + * Since browsers don't explode with overflowing colspans, escalator + * shouldn't either. + */ + if (spannedColumns.getEnd() > columnConfiguration.getColumnCount()) { + spannedColumns = Range.between(columnIndex, + columnConfiguration.getColumnCount()); + } + return columnConfiguration + .getCalculatedColumnsWidth(spannedColumns); + } + + /** + * Applies the total length of the columns to each row element. + * <p> + * <em>Note:</em> In contrast to {@link #reapplyColumnWidths()}, this + * method only modifies the width of the {@code <tr>} element, not the + * cells within. + */ + protected void reapplyRowWidths() { + int rowWidth = columnConfiguration.calculateRowWidth(); + + com.google.gwt.dom.client.Element row = root.getFirstChildElement(); + while (row != null) { + row.getStyle().setWidth(rowWidth, Unit.PX); + row = row.getNextSiblingElement(); + } + } + + /** + * The primary style name for the container. + * + * @param primaryStyleName + * the style name to use as prefix for all row and cell style + * names. + */ + protected void setStylePrimaryName(String primaryStyleName) { + String oldStyle = getStylePrimaryName(); + if (SharedUtil.equals(oldStyle, primaryStyleName)) { + return; + } + + this.primaryStyleName = primaryStyleName; + + // Update already rendered rows and cells + Node row = root.getFirstChild(); + while (row != null) { + Element rowElement = row.cast(); + UIObject.setStylePrimaryName(rowElement, primaryStyleName + + "-row"); + Node cell = row.getFirstChild(); + while (cell != null) { + Element cellElement = cell.cast(); + UIObject.setStylePrimaryName(cellElement, primaryStyleName + + "-cell"); + cell = cell.getNextSibling(); + } + row = row.getNextSibling(); + } + } + + /** + * Returns the primary style name of the container. + * + * @return The primary style name or <code>null</code> if not set. + */ + protected String getStylePrimaryName() { + return primaryStyleName; + } + } + + private abstract class AbstractStaticRowContainer extends + AbstractRowContainer { + public AbstractStaticRowContainer(final Element headElement) { + super(headElement); + } + + @Override + protected void paintRemoveRows(final int index, final int numberOfRows) { + for (int i = index; i < index + numberOfRows; i++) { + final Element tr = (Element) root.getChild(index); + for (int c = 0; c < tr.getChildCount(); c++) { + detachPossibleWidgetFromCell((Element) tr.getChild(c) + .cast()); + } + tr.removeFromParent(); + } + recalculateSectionHeight(); + } + + @Override + protected Element getTrByVisualIndex(final int index) + throws IndexOutOfBoundsException { + if (index >= 0 && index < root.getChildCount()) { + return (Element) root.getChild(index); + } else { + throw new IndexOutOfBoundsException("No such visual index: " + + index); + } + } + + @Override + protected int getTopVisualRowLogicalIndex() { + return 0; + } + + @Override + public void insertRows(int index, int numberOfRows) { + super.insertRows(index, numberOfRows); + recalculateElementSizes(); + } + + @Override + public void removeRows(int index, int numberOfRows) { + super.removeRows(index, numberOfRows); + recalculateElementSizes(); + } + } + + private class HeaderRowContainer extends AbstractStaticRowContainer { + public HeaderRowContainer(final Element headElement) { + super(headElement); + } + + @Override + protected void sectionHeightCalculated() { + bodyElem.getStyle().setMarginTop(height, Unit.PX); + verticalScrollbar.getElement().getStyle().setTop(height, Unit.PX); + } + + @Override + protected String getCellElementTagName() { + return "th"; + } + + @Override + public void setStylePrimaryName(String primaryStyleName) { + super.setStylePrimaryName(primaryStyleName); + UIObject.setStylePrimaryName(root, primaryStyleName + "-header"); + } + } + + private class FooterRowContainer extends AbstractStaticRowContainer { + public FooterRowContainer(final Element footElement) { + super(footElement); + } + + @Override + public void setStylePrimaryName(String primaryStyleName) { + super.setStylePrimaryName(primaryStyleName); + UIObject.setStylePrimaryName(root, primaryStyleName + "-footer"); + } + + @Override + protected String getCellElementTagName() { + return "td"; + } + } + + private class BodyRowContainer extends AbstractRowContainer { + /* + * TODO [[optimize]]: check whether a native JsArray might be faster + * than LinkedList + */ + /** + * The order in which row elements are rendered visually in the browser, + * with the help of CSS tricks. Usually has nothing to do with the DOM + * order. + */ + private final LinkedList<Element> visualRowOrder = new LinkedList<Element>(); + + /** + * Don't use this field directly, because it will not take proper care + * of all the bookkeeping required. + * + * @deprecated Use {@link #setRowPosition(Element, int, int)} and + * {@link #getRowTop(Element)} instead. + */ + @Deprecated + private final Map<Element, Integer> rowTopPosMap = new HashMap<Element, Integer>(); + + public BodyRowContainer(final Element bodyElement) { + super(bodyElement); + } + + @Override + public void setStylePrimaryName(String primaryStyleName) { + super.setStylePrimaryName(primaryStyleName); + UIObject.setStylePrimaryName(root, primaryStyleName + "-body"); + } + + public void updateEscalatorRowsOnScroll() { + if (visualRowOrder.isEmpty()) { + return; + } + + boolean rowsWereMoved = false; + + final int topRowPos = getRowTop(visualRowOrder.getFirst()); + // TODO [[mpixscroll]] + final int scrollTop = tBodyScrollTop; + final int viewportOffset = topRowPos - scrollTop; + + /* + * TODO [[optimize]] this if-else can most probably be refactored + * into a neater block of code + */ + + if (viewportOffset > 0) { + // there's empty room on top + + int rowsToMove = (int) Math.ceil((double) viewportOffset + / (double) ROW_HEIGHT_PX); + rowsToMove = Math.min(rowsToMove, root.getChildCount()); + + final int end = root.getChildCount(); + final int start = end - rowsToMove; + final int logicalRowIndex = scrollTop / ROW_HEIGHT_PX; + moveAndUpdateEscalatorRows(Range.between(start, end), 0, + logicalRowIndex); + + rowsWereMoved = (rowsToMove != 0); + } + + else if (viewportOffset + ROW_HEIGHT_PX <= 0) { + /* + * the viewport has been scrolled more than the topmost visual + * row. + */ + + /* + * Using the fact that integer division has implicit + * floor-function to our advantage here. + */ + int rowsToMove = Math.abs(viewportOffset / ROW_HEIGHT_PX); + rowsToMove = Math.min(rowsToMove, root.getChildCount()); + + int logicalRowIndex; + if (rowsToMove < root.getChildCount()) { + /* + * We scroll so little that we can just keep adding the rows + * below the current escalator + */ + logicalRowIndex = getLogicalRowIndex(visualRowOrder + .getLast()) + 1; + } else { + /* + * Since we're moving all escalator rows, we need to + * calculate the first logical row index from the scroll + * position. + */ + logicalRowIndex = scrollTop / ROW_HEIGHT_PX; + } + + /* + * Since we're moving the viewport downwards, the visual index + * is always at the bottom. Note: Due to how + * moveAndUpdateEscalatorRows works, this will work out even if + * we move all the rows, and try to place them "at the end". + */ + final int targetVisualIndex = root.getChildCount(); + + // make sure that we don't move rows over the data boundary + boolean aRowWasLeftBehind = false; + if (logicalRowIndex + rowsToMove > getRowCount()) { + /* + * TODO [[rowheight]]: with constant row heights, there's + * always exactly one row that will be moved beyond the data + * source, when viewport is scrolled to the end. This, + * however, isn't guaranteed anymore once row heights start + * varying. + */ + rowsToMove--; + aRowWasLeftBehind = true; + } + + moveAndUpdateEscalatorRows(Range.between(0, rowsToMove), + targetVisualIndex, logicalRowIndex); + + if (aRowWasLeftBehind) { + /* + * To keep visualRowOrder as a spatially contiguous block of + * rows, let's make sure that the one row we didn't move + * visually still stays with the pack. + */ + final Range strayRow = Range.withOnly(0); + final int topLogicalIndex = getLogicalRowIndex(visualRowOrder + .get(1)) - 1; + moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex); + } + + rowsWereMoved = (rowsToMove != 0); + } + + if (rowsWereMoved) { + fireRowVisibilityChangeEvent(); + } + } + + @Override + protected List<Element> paintInsertRows(final int index, + final int numberOfRows) { + if (numberOfRows == 0) { + return Collections.emptyList(); + } + + /* + * TODO: this method should probably only add physical rows, and not + * populate them - let everything be populated as appropriate by the + * logic that follows. + * + * This also would lead to the fact that paintInsertRows wouldn't + * need to return anything. + */ + final List<Element> addedRows = fillAndPopulateEscalatorRowsIfNeeded( + index, numberOfRows); + + /* + * insertRows will always change the number of rows - update the + * scrollbar sizes. + */ + scroller.recalculateScrollbarsForVirtualViewport(); + + final boolean addedRowsAboveCurrentViewport = index * ROW_HEIGHT_PX < getScrollTop(); + final boolean addedRowsBelowCurrentViewport = index * ROW_HEIGHT_PX > getScrollTop() + + calculateHeight(); + + if (addedRowsAboveCurrentViewport) { + /* + * We need to tweak the virtual viewport (scroll handle + * positions, table "scroll position" and row locations), but + * without re-evaluating any rows. + */ + + final int yDelta = numberOfRows * ROW_HEIGHT_PX; + adjustScrollPosIgnoreEvents(yDelta); + } + + else if (addedRowsBelowCurrentViewport) { + // NOOP, we already recalculated scrollbars. + } + + else { // some rows were added inside the current viewport + + final int unupdatedLogicalStart = index + addedRows.size(); + final int visualOffset = getLogicalRowIndex(visualRowOrder + .getFirst()); + + /* + * At this point, we have added new escalator rows, if so + * needed. + * + * If more rows were added than the new escalator rows can + * account for, we need to start to spin the escalator to update + * the remaining rows aswell. + */ + final int rowsStillNeeded = numberOfRows - addedRows.size(); + final Range unupdatedVisual = convertToVisual(Range.withLength( + unupdatedLogicalStart, rowsStillNeeded)); + final int end = root.getChildCount(); + final int start = end - unupdatedVisual.length(); + final int visualTargetIndex = unupdatedLogicalStart + - visualOffset; + moveAndUpdateEscalatorRows(Range.between(start, end), + visualTargetIndex, unupdatedLogicalStart); + + // move the surrounding rows to their correct places. + int rowTop = (unupdatedLogicalStart + (end - start)) + * ROW_HEIGHT_PX; + final ListIterator<Element> i = visualRowOrder + .listIterator(visualTargetIndex + (end - start)); + while (i.hasNext()) { + final Element tr = i.next(); + setRowPosition(tr, 0, rowTop); + rowTop += ROW_HEIGHT_PX; + } + + fireRowVisibilityChangeEvent(); + } + return addedRows; + } + + /** + * Move escalator rows around, and make sure everything gets + * appropriately repositioned and repainted. + * + * @param visualSourceRange + * the range of rows to move to a new place + * @param visualTargetIndex + * the visual index where the rows will be placed to + * @param logicalTargetIndex + * the logical index to be assigned to the first moved row + * @throws IllegalArgumentException + * if any of <code>visualSourceRange.getStart()</code>, + * <code>visualTargetIndex</code> or + * <code>logicalTargetIndex</code> is a negative number; or + * if <code>visualTargetInfo</code> is greater than the + * number of escalator rows. + */ + private void moveAndUpdateEscalatorRows(final Range visualSourceRange, + final int visualTargetIndex, final int logicalTargetIndex) + throws IllegalArgumentException { + + if (visualSourceRange.isEmpty()) { + return; + } + + if (visualSourceRange.getStart() < 0) { + throw new IllegalArgumentException( + "Logical source start must be 0 or greater (was " + + visualSourceRange.getStart() + ")"); + } else if (logicalTargetIndex < 0) { + throw new IllegalArgumentException( + "Logical target must be 0 or greater"); + } else if (visualTargetIndex < 0) { + throw new IllegalArgumentException( + "Visual target must be 0 or greater"); + } else if (visualTargetIndex > root.getChildCount()) { + throw new IllegalArgumentException( + "Visual target must not be greater than the number of escalator rows"); + } else if (logicalTargetIndex + visualSourceRange.length() > getRowCount()) { + final int logicalEndIndex = logicalTargetIndex + + visualSourceRange.length() - 1; + throw new IllegalArgumentException( + "Logical target leads to rows outside of the data range (" + + logicalTargetIndex + ".." + logicalEndIndex + + ")"); + } + + /* + * Since we move a range into another range, the indices might move + * about. Having 10 rows, if we move 0..1 to index 10 (to the end of + * the collection), the target range will end up being 8..9, instead + * of 10..11. + * + * This applies only if we move elements forward in the collection, + * not backward. + */ + final int adjustedVisualTargetIndex; + if (visualSourceRange.getStart() < visualTargetIndex) { + adjustedVisualTargetIndex = visualTargetIndex + - visualSourceRange.length(); + } else { + adjustedVisualTargetIndex = visualTargetIndex; + } + + if (visualSourceRange.getStart() != adjustedVisualTargetIndex) { + + /* + * Reorder the rows to their correct places within + * visualRowOrder (unless rows are moved back to their original + * places) + */ + + /* + * TODO [[optimize]]: move whichever set is smaller: the ones + * explicitly moved, or the others. So, with 10 escalator rows, + * if we are asked to move idx[0..8] to the end of the list, + * it's faster to just move idx[9] to the beginning. + */ + + final List<Element> removedRows = new ArrayList<Element>( + visualSourceRange.length()); + for (int i = 0; i < visualSourceRange.length(); i++) { + final Element tr = visualRowOrder.remove(visualSourceRange + .getStart()); + removedRows.add(tr); + } + visualRowOrder.addAll(adjustedVisualTargetIndex, removedRows); + } + + { // Refresh the contents of the affected rows + final ListIterator<Element> iter = visualRowOrder + .listIterator(adjustedVisualTargetIndex); + for (int logicalIndex = logicalTargetIndex; logicalIndex < logicalTargetIndex + + visualSourceRange.length(); logicalIndex++) { + final Element tr = iter.next(); + refreshRow(tr, logicalIndex); + } + } + + { // Reposition the rows that were moved + int newRowTop = logicalTargetIndex * ROW_HEIGHT_PX; + + final ListIterator<Element> iter = visualRowOrder + .listIterator(adjustedVisualTargetIndex); + for (int i = 0; i < visualSourceRange.length(); i++) { + final Element tr = iter.next(); + setRowPosition(tr, 0, newRowTop); + newRowTop += ROW_HEIGHT_PX; + } + } + } + + /** + * Adjust the scroll position without having the scroll handler have any + * side-effects. + * <p> + * <em>Note:</em> {@link Scroller#onScroll(double, double)} + * <em>will</em> be triggered, but it not do anything, with the help of + * {@link Escalator#internalScrollEventCalls}. + * + * @param yDelta + * the delta of pixels to scrolls. A positive value moves the + * viewport downwards, while a negative value moves the + * viewport upwards + */ + public void adjustScrollPosIgnoreEvents(final int yDelta) { + if (yDelta == 0) { + return; + } + + internalScrollEventCalls++; + verticalScrollbar.setScrollPosByDelta(yDelta); + + final int snappedYDelta = yDelta - yDelta % ROW_HEIGHT_PX; + for (final Element tr : visualRowOrder) { + setRowPosition(tr, 0, getRowTop(tr) + snappedYDelta); + } + setBodyScrollPosition(tBodyScrollLeft, tBodyScrollTop + yDelta); + } + + private void setRowPosition(final Element tr, final int x, final int y) { + position.set(tr, x, y); + rowTopPosMap.put(tr, y); + } + + private int getRowTop(final Element tr) { + return rowTopPosMap.get(tr); + } + + /** + * Adds new physical escalator rows to the DOM at the given index if + * there's still a need for more escalator rows. + * <p> + * If Escalator already is at (or beyond) max capacity, this method does + * nothing to the DOM. + * + * @param index + * the index at which to add new escalator rows. + * <em>Note:</em>It is assumed that the index is both the + * visual index and the logical index. + * @param numberOfRows + * the number of rows to add at <code>index</code> + * @return a list of the added rows + */ + private List<Element> fillAndPopulateEscalatorRowsIfNeeded( + final int index, final int numberOfRows) { + + final int escalatorRowsStillFit = getMaxEscalatorRowCapacity() + - root.getChildCount(); + final int escalatorRowsNeeded = Math.min(numberOfRows, + escalatorRowsStillFit); + + if (escalatorRowsNeeded > 0) { + + final List<Element> addedRows = super.paintInsertRows(index, + escalatorRowsNeeded); + visualRowOrder.addAll(index, addedRows); + + /* + * We need to figure out the top positions for the rows we just + * added. + */ + for (int i = 0; i < addedRows.size(); i++) { + setRowPosition(addedRows.get(i), 0, (index + i) + * ROW_HEIGHT_PX); + } + + /* Move the other rows away from above the added escalator rows */ + for (int i = index + addedRows.size(); i < visualRowOrder + .size(); i++) { + final Element tr = visualRowOrder.get(i); + setRowPosition(tr, 0, i * ROW_HEIGHT_PX); + } + + return addedRows; + } else { + return new ArrayList<Element>(); + } + } + + private int getMaxEscalatorRowCapacity() { + final int maxEscalatorRowCapacity = (int) Math + .ceil(calculateHeight() / ROW_HEIGHT_PX) + 1; + /* + * maxEscalatorRowCapacity can become negative if the headers and + * footers start to overlap. This is a crazy situation, but Vaadin + * blinks the components a lot, so it's feasible. + */ + return Math.max(0, maxEscalatorRowCapacity); + } + + @Override + protected void paintRemoveRows(final int index, final int numberOfRows) { + + final Range viewportRange = Range.withLength( + getLogicalRowIndex(visualRowOrder.getFirst()), + visualRowOrder.size()); + + final Range removedRowsRange = Range + .withLength(index, numberOfRows); + + final Range[] partitions = removedRowsRange + .partitionWith(viewportRange); + final Range removedAbove = partitions[0]; + final Range removedLogicalInside = partitions[1]; + final Range removedVisualInside = convertToVisual(removedLogicalInside); + + /* + * TODO: extract the following if-block to a separate method. I'll + * leave this be inlined for now, to make linediff-based code + * reviewing easier. Probably will be moved in the following patch + * set. + */ + + /* + * Adjust scroll position in one of two scenarios: + * + * 1) Rows were removed above. Then we just need to adjust the + * scrollbar by the height of the removed rows. + * + * 2) There are no logical rows above, and at least the first (if + * not more) visual row is removed. Then we need to snap the scroll + * position to the first visible row (i.e. reset scroll position to + * absolute 0) + * + * The logic is optimized in such a way that the + * adjustScrollPosIgnoreEvents is called only once, to avoid extra + * reflows, and thus the code might seem a bit obscure. + */ + final boolean firstVisualRowIsRemoved = !removedVisualInside + .isEmpty() && removedVisualInside.getStart() == 0; + + if (!removedAbove.isEmpty() || firstVisualRowIsRemoved) { + // TODO [[rowheight]] + final int yDelta = removedAbove.length() * ROW_HEIGHT_PX; + final int firstLogicalRowHeight = ROW_HEIGHT_PX; + final boolean removalScrollsToShowFirstLogicalRow = verticalScrollbar + .getScrollPos() - yDelta < firstLogicalRowHeight; + + if (removedVisualInside.isEmpty() + && (!removalScrollsToShowFirstLogicalRow || !firstVisualRowIsRemoved)) { + /* + * rows were removed from above the viewport, so all we need + * to do is to adjust the scroll position to account for the + * removed rows + */ + adjustScrollPosIgnoreEvents(-yDelta); + } else if (removalScrollsToShowFirstLogicalRow) { + /* + * It seems like we've removed all rows from above, and also + * into the current viewport. This means we'll need to even + * out the scroll position to exactly 0 (i.e. adjust by the + * current negative scrolltop, presto!), so that it isn't + * aligned funnily + */ + adjustScrollPosIgnoreEvents(-verticalScrollbar + .getScrollPos()); + } + } + + // ranges evaluated, let's do things. + if (!removedVisualInside.isEmpty()) { + int escalatorRowCount = bodyElem.getChildCount(); + + /* + * If we're left with less rows than the number of escalators, + * remove the unused ones. + */ + final int escalatorRowsToRemove = escalatorRowCount + - getRowCount(); + if (escalatorRowsToRemove > 0) { + for (int i = 0; i < escalatorRowsToRemove; i++) { + final Element tr = visualRowOrder + .remove(removedVisualInside.getStart()); + for (int c = 0; c < tr.getChildCount(); c++) { + detachPossibleWidgetFromCell((Element) tr.getChild( + c).cast()); + } + tr.removeFromParent(); + rowTopPosMap.remove(tr); + } + escalatorRowCount -= escalatorRowsToRemove; + + /* + * Because we're removing escalator rows, we don't have + * anything to scroll by. Let's make sure the viewport is + * scrolled to top, to render any rows possibly left above. + */ + body.setBodyScrollPosition(tBodyScrollLeft, 0); + + /* + * We might have removed some rows from the middle, so let's + * make sure we're not left with any holes. Also remember: + * visualIndex == logicalIndex applies now. + */ + final int dirtyRowsStart = removedLogicalInside.getStart(); + for (int i = dirtyRowsStart; i < escalatorRowCount; i++) { + final Element tr = visualRowOrder.get(i); + setRowPosition(tr, 0, i * ROW_HEIGHT_PX); + } + + /* + * this is how many rows appeared into the viewport from + * below + */ + final int rowsToUpdateDataOn = numberOfRows + - escalatorRowsToRemove; + final int start = Math.max(0, escalatorRowCount + - rowsToUpdateDataOn); + final int end = escalatorRowCount; + for (int i = start; i < end; i++) { + final Element tr = visualRowOrder.get(i); + refreshRow(tr, i); + } + } + + else { + // No escalator rows need to be removed. + + /* + * Two things (or a combination thereof) can happen: + * + * 1) We're scrolled to the bottom, the last rows are + * removed. SOLUTION: moveAndUpdateEscalatorRows the + * bottommost rows, and place them at the top to be + * refreshed. + * + * 2) We're scrolled somewhere in the middle, arbitrary rows + * are removed. SOLUTION: moveAndUpdateEscalatorRows the + * removed rows, and place them at the bottom to be + * refreshed. + * + * Since a combination can also happen, we need to handle + * this in a smart way, all while avoiding + * double-refreshing. + */ + + final int contentBottom = getRowCount() * ROW_HEIGHT_PX; + final int viewportBottom = (int) (tBodyScrollTop + calculateHeight()); + if (viewportBottom <= contentBottom) { + /* + * We're in the middle of the row container, everything + * is added to the bottom + */ + paintRemoveRowsAtMiddle(removedLogicalInside, + removedVisualInside, 0); + } + + else if (contentBottom + (numberOfRows * ROW_HEIGHT_PX) + - viewportBottom < ROW_HEIGHT_PX) { + /* + * We're at the end of the row container, everything is + * added to the top. + */ + paintRemoveRowsAtBottom(removedLogicalInside, + removedVisualInside); + } + + else { + /* + * We're in a combination, where we need to both scroll + * up AND show new rows at the bottom. + * + * Example: Scrolled down to show the second to last + * row. Remove two. Viewport scrolls up, revealing the + * row above row. The last element collapses up and into + * view. + * + * Reminder: this use case handles only the case when + * there are enough escalator rows to still render a + * full view. I.e. all escalator rows will _always_ be + * populated + */ + /*- + * 1 1 |1| <- newly rendered + * |2| |2| |2| + * |3| ==> |*| ==> |5| <- newly rendered + * |4| |*| + * 5 5 + * + * 1 1 |1| <- newly rendered + * |2| |*| |4| + * |3| ==> |*| ==> |5| <- newly rendered + * |4| |4| + * 5 5 + */ + + /* + * STEP 1: + * + * reorganize deprecated escalator rows to bottom, but + * don't re-render anything yet + */ + /*- + * 1 1 1 + * |2| |*| |4| + * |3| ==> |*| ==> |*| + * |4| |4| |*| + * 5 5 5 + */ + int newTop = getRowTop(visualRowOrder + .get(removedVisualInside.getStart())); + for (int i = 0; i < removedVisualInside.length(); i++) { + final Element tr = visualRowOrder + .remove(removedVisualInside.getStart()); + visualRowOrder.addLast(tr); + } + + for (int i = removedVisualInside.getStart(); i < escalatorRowCount; i++) { + final Element tr = visualRowOrder.get(i); + setRowPosition(tr, 0, newTop); + newTop += ROW_HEIGHT_PX; + } + + /* + * STEP 2: + * + * manually scroll + */ + /*- + * 1 |1| <-- newly rendered (by scrolling) + * |4| |4| + * |*| ==> |*| + * |*| + * 5 5 + */ + final double newScrollTop = contentBottom + - calculateHeight(); + setScrollTop(newScrollTop); + /* + * Manually call the scroll handler, so we get immediate + * effects in the escalator. + */ + scroller.onScroll(); + internalScrollEventCalls++; + + /* + * Move the bottommost (n+1:th) escalator row to top, + * because scrolling up doesn't handle that for us + * automatically + */ + moveAndUpdateEscalatorRows( + Range.withOnly(escalatorRowCount - 1), + 0, + getLogicalRowIndex(visualRowOrder.getFirst()) - 1); + + /* + * STEP 3: + * + * update remaining escalator rows + */ + /*- + * |1| |1| + * |4| ==> |4| + * |*| |5| <-- newly rendered + * + * 5 + */ + final int rowsScrolled = (int) (Math + .ceil((viewportBottom - (double) contentBottom) + / ROW_HEIGHT_PX)); + final int start = escalatorRowCount + - (removedVisualInside.length() - rowsScrolled); + final Range visualRefreshRange = Range.between(start, + escalatorRowCount); + final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder + .getFirst()) + start; + // in-place move simply re-renders the rows. + moveAndUpdateEscalatorRows(visualRefreshRange, start, + logicalTargetIndex); + } + } + } + + /* + * this needs to be done after the escalator has been shrunk down, + * or it won't work correctly (due to setScrollTop invocation) + */ + scroller.recalculateScrollbarsForVirtualViewport(); + + fireRowVisibilityChangeEvent(); + } + + private void paintRemoveRowsAtMiddle(final Range removedLogicalInside, + final Range removedVisualInside, final int logicalOffset) { + /*- + * : : : + * |2| |2| |2| + * |3| ==> |*| ==> |4| + * |4| |4| |6| <- newly rendered + * : : : + */ + + final int escalatorRowCount = visualRowOrder.size(); + + final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder + .getLast()) + - (removedVisualInside.length() - 1) + + logicalOffset; + moveAndUpdateEscalatorRows(removedVisualInside, escalatorRowCount, + logicalTargetIndex); + + // move the surrounding rows to their correct places. + final ListIterator<Element> iterator = visualRowOrder + .listIterator(removedVisualInside.getStart()); + int rowTop = (removedLogicalInside.getStart() + logicalOffset) + * ROW_HEIGHT_PX; + for (int i = removedVisualInside.getStart(); i < escalatorRowCount + - removedVisualInside.length(); i++) { + final Element tr = iterator.next(); + setRowPosition(tr, 0, rowTop); + rowTop += ROW_HEIGHT_PX; + } + } + + private void paintRemoveRowsAtBottom(final Range removedLogicalInside, + final Range removedVisualInside) { + /*- + * : + * : : |4| <- newly rendered + * |5| |5| |5| + * |6| ==> |*| ==> |7| + * |7| |7| + */ + + final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder + .getFirst()) - removedVisualInside.length(); + moveAndUpdateEscalatorRows(removedVisualInside, 0, + logicalTargetIndex); + + // move the surrounding rows to their correct places. + final ListIterator<Element> iterator = visualRowOrder + .listIterator(removedVisualInside.getEnd()); + int rowTop = removedLogicalInside.getStart() * ROW_HEIGHT_PX; + while (iterator.hasNext()) { + final Element tr = iterator.next(); + setRowPosition(tr, 0, rowTop); + rowTop += ROW_HEIGHT_PX; + } + } + + private int getLogicalRowIndex(final Element element) { + // TODO [[rowheight]] + return getRowTop(element) / ROW_HEIGHT_PX; + } + + @Override + protected void recalculateSectionHeight() { + // disable for body, since it doesn't make any sense. + } + + /** + * Adjusts the row index and number to be relevant for the current + * virtual viewport. + * <p> + * It converts a logical range of rows index to the matching visual + * range, truncating the resulting range with the viewport. + * <p> + * <ul> + * <li>Escalator contains logical rows 0..100 + * <li>Current viewport showing logical rows 20..29 + * <li>convertToVisual([20..29]) → [0..9] + * <li>convertToVisual([15..24]) → [0..4] + * <li>convertToVisual([25..29]) → [5..9] + * <li>convertToVisual([26..39]) → [6..9] + * <li>convertToVisual([0..5]) → [0..-1] <em>(empty)</em> + * <li>convertToVisual([35..1]) → [0..-1] <em>(empty)</em> + * <li>convertToVisual([0..100]) → [0..9] + * </ul> + * + * @return a logical range converted to a visual range, truncated to the + * current viewport. The first visual row has the index 0. + */ + private Range convertToVisual(final Range logicalRange) { + if (logicalRange.isEmpty()) { + return logicalRange; + } else if (visualRowOrder.isEmpty()) { + // empty range + return Range.withLength(0, 0); + } + + /* + * TODO [[rowheight]]: these assumptions will be totally broken with + * variable row heights. + */ + final int topRowHeight = ROW_HEIGHT_PX; + final int maxEscalatorRows = (int) Math + .ceil((calculateHeight() + topRowHeight) / ROW_HEIGHT_PX); + final int currentTopRowIndex = getLogicalRowIndex(visualRowOrder + .getFirst()); + + final Range[] partitions = logicalRange.partitionWith(Range + .withLength(currentTopRowIndex, maxEscalatorRows)); + final Range insideRange = partitions[1]; + return insideRange.offsetBy(-currentTopRowIndex); + } + + @Override + protected String getCellElementTagName() { + return "td"; + } + + private double calculateHeight() { + final int tableHeight = tableWrapper.getOffsetHeight(); + final double footerHeight = footer.height; + final double headerHeight = header.height; + return tableHeight - footerHeight - headerHeight; + } + + @Override + public void refreshRows(final int index, final int numberOfRows) { + Profiler.enter("Escalator.BodyRowContainer.refreshRows"); + + final Range visualRange = convertToVisual(Range.withLength(index, + numberOfRows)); + + if (!visualRange.isEmpty()) { + final int firstLogicalRowIndex = getLogicalRowIndex(visualRowOrder + .getFirst()); + for (int rowNumber = visualRange.getStart(); rowNumber < visualRange + .getEnd(); rowNumber++) { + refreshRow(visualRowOrder.get(rowNumber), + firstLogicalRowIndex + rowNumber); + } + } + + Profiler.leave("Escalator.BodyRowContainer.refreshRows"); + } + + @Override + protected Element getTrByVisualIndex(final int index) + throws IndexOutOfBoundsException { + if (index >= 0 && index < visualRowOrder.size()) { + return visualRowOrder.get(index); + } else { + throw new IndexOutOfBoundsException("No such visual index: " + + index); + } + } + + private void setBodyScrollPosition(final int scrollLeft, + final int scrollTop) { + tBodyScrollLeft = scrollLeft; + tBodyScrollTop = scrollTop; + position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop); + } + + @Override + protected int getTopVisualRowLogicalIndex() { + if (!visualRowOrder.isEmpty()) { + return getLogicalRowIndex(visualRowOrder.getFirst()); + } else { + return 0; + } + } + + /** + * Make sure that there is a correct amount of escalator rows: Add more + * if needed, or remove any superfluous ones. + * <p> + * This method should be called when e.g. the height of the Escalator + * changes. + */ + public void verifyEscalatorCount() { + /* + * This method indeed has a smell very similar to paintRemoveRows + * and paintInsertRows. + * + * Unfortunately, those the code can't trivially be shared, since + * there are some slight differences in the respective + * responsibilities. The "paint" methods fake the addition and + * removal of rows, and make sure to either push existing data out + * of view, or draw new data into view. Only in some special cases + * will the DOM element count change. + * + * This method, however, has the explicit responsibility to verify + * that when "something" happens, we still have the correct amount + * of escalator rows in the DOM, and if not, we make sure to modify + * that count. Only in some special cases do we need to take into + * account other things than simply modifying the DOM element count. + */ + + Profiler.enter("Escalator.BodyRowContainer.verifyEscalatorCount"); + + if (!isAttached()) { + return; + } + + final int maxEscalatorRows = getMaxEscalatorRowCapacity(); + final int neededEscalatorRows = Math.min(maxEscalatorRows, + body.getRowCount()); + final int neededEscalatorRowsDiff = neededEscalatorRows + - visualRowOrder.size(); + + if (neededEscalatorRowsDiff > 0) { + // needs more + + /* + * This is a workaround for the issue where we might be scrolled + * to the bottom, and the widget expands beyond the content + * range + */ + + final int index = visualRowOrder.size(); + final int nextLastLogicalIndex; + if (!visualRowOrder.isEmpty()) { + nextLastLogicalIndex = getLogicalRowIndex(visualRowOrder + .getLast()) + 1; + } else { + nextLastLogicalIndex = 0; + } + + final boolean contentWillFit = nextLastLogicalIndex < getRowCount() + - neededEscalatorRowsDiff; + if (contentWillFit) { + final List<Element> addedRows = fillAndPopulateEscalatorRowsIfNeeded( + index, neededEscalatorRowsDiff); + + /* + * Since fillAndPopulateEscalatorRowsIfNeeded operates on + * the assumption that index == visual index == logical + * index, we thank for the added escalator rows, but since + * they're painted in the wrong CSS position, we need to + * move them to their actual locations. + * + * Note: this is the second (see body.paintInsertRows) + * occasion where fillAndPopulateEscalatorRowsIfNeeded would + * behave "more correctly" if it only would add escalator + * rows to the DOM and appropriate bookkeping, and not + * actually populate them :/ + */ + moveAndUpdateEscalatorRows( + Range.withLength(index, addedRows.size()), index, + nextLastLogicalIndex); + } else { + /* + * TODO [[optimize]] + * + * We're scrolled so far down that all rows can't be simply + * appended at the end, since we might start displaying + * escalator rows that don't exist. To avoid the mess that + * is body.paintRemoveRows, this is a dirty hack that dumbs + * the problem down to a more basic and already-solved + * problem: + * + * 1) scroll all the way up 2) add the missing escalator + * rows 3) scroll back to the original position. + * + * Letting the browser scroll back to our original position + * will automatically solve any possible overflow problems, + * since the browser will not allow us to scroll beyond the + * actual content. + */ + + final double oldScrollTop = getScrollTop(); + setScrollTop(0); + scroller.onScroll(); + fillAndPopulateEscalatorRowsIfNeeded(index, + neededEscalatorRowsDiff); + setScrollTop(oldScrollTop); + scroller.onScroll(); + internalScrollEventCalls++; + } + } + + else if (neededEscalatorRowsDiff < 0) { + // needs less + + final ListIterator<Element> iter = visualRowOrder + .listIterator(visualRowOrder.size()); + for (int i = 0; i < -neededEscalatorRowsDiff; i++) { + final Element last = iter.previous(); + for (int c = 0; c < last.getChildCount(); c++) { + detachPossibleWidgetFromCell((Element) last.getChild(c) + .cast()); + } + last.removeFromParent(); + iter.remove(); + } + + /* + * If we were scrolled to the bottom so that we didn't have an + * extra escalator row at the bottom, we'll probably end up with + * blank space at the bottom of the escalator, and one extra row + * above the header. + * + * Experimentation idea #1: calculate "scrollbottom" vs content + * bottom and remove one row from top, rest from bottom. This + * FAILED, since setHeight has already happened, thus we never + * will detect ourselves having been scrolled all the way to the + * bottom. + */ + + if (!visualRowOrder.isEmpty()) { + final int firstRowTop = getRowTop(visualRowOrder.getFirst()); + final double firstRowMinTop = tBodyScrollTop + - ROW_HEIGHT_PX; + if (firstRowTop < firstRowMinTop) { + final int newLogicalIndex = getLogicalRowIndex(visualRowOrder + .getLast()) + 1; + moveAndUpdateEscalatorRows(Range.withOnly(0), + visualRowOrder.size(), newLogicalIndex); + } + } + } + + if (neededEscalatorRowsDiff != 0) { + fireRowVisibilityChangeEvent(); + } + + Profiler.leave("Escalator.BodyRowContainer.verifyEscalatorCount"); + } + } + + private class ColumnConfigurationImpl implements ColumnConfiguration { + public class Column { + private static final int DEFAULT_COLUMN_WIDTH_PX = 100; + + private int definedWidth = -1; + private int calculatedWidth = DEFAULT_COLUMN_WIDTH_PX; + + public void setWidth(int px) { + definedWidth = px; + calculatedWidth = (px >= 0) ? px : DEFAULT_COLUMN_WIDTH_PX; + } + + public int getDefinedWidth() { + return definedWidth; + } + + public int getCalculatedWidth() { + return calculatedWidth; + } + } + + private final List<Column> columns = new ArrayList<Column>(); + private int frozenColumns = 0; + + /** + * A cached array of all the calculated column widths. + * + * @see #getCalculatedColumnWidths() + */ + private int[] widthsArray = null; + + /** + * {@inheritDoc} + * <p> + * <em>Implementation detail:</em> This method does no DOM modifications + * (i.e. is very cheap to call) if there are no rows in the DOM when + * this method is called. + * + * @see #hasSomethingInDom() + */ + @Override + public void removeColumns(final int index, final int numberOfColumns) { + assertArgumentsAreValidAndWithinRange(index, numberOfColumns); + + flyweightRow.removeCells(index, numberOfColumns); + + // Cope with removing frozen columns + if (index < frozenColumns) { + if (index + numberOfColumns < frozenColumns) { + /* + * Last removed column was frozen, meaning that all removed + * columns were frozen. Just decrement the number of frozen + * columns accordingly. + */ + frozenColumns -= numberOfColumns; + } else { + /* + * If last removed column was not frozen, we have removed + * columns beyond the frozen range, so all remaining frozen + * columns are to the left of the removed columns. + */ + frozenColumns = index; + } + } + + List<Column> removedColumns = new ArrayList<Column>(); + for (int i = 0; i < numberOfColumns; i++) { + removedColumns.add(columns.remove(index)); + } + + if (hasSomethingInDom()) { + for (final AbstractRowContainer rowContainer : rowContainers) { + rowContainer.paintRemoveColumns(index, numberOfColumns, + removedColumns); + } + } + } + + /** + * Calculate the width of a row, as the sum of columns' widths. + * + * @return the width of a row, in pixels + */ + public int calculateRowWidth() { + return getCalculatedColumnsWidth(Range.between(0, getColumnCount())); + } + + private void assertArgumentsAreValidAndWithinRange(final int index, + final int numberOfColumns) { + if (numberOfColumns < 1) { + throw new IllegalArgumentException( + "Number of columns can't be less than 1 (was " + + numberOfColumns + ")"); + } + + if (index < 0 || index + numberOfColumns > getColumnCount()) { + throw new IndexOutOfBoundsException("The given " + + "column range (" + index + ".." + + (index + numberOfColumns) + + ") was outside of the current " + + "number of columns (" + getColumnCount() + ")"); + } + } + + /** + * {@inheritDoc} + * <p> + * <em>Implementation detail:</em> This method does no DOM modifications + * (i.e. is very cheap to call) if there is no data for rows when this + * method is called. + * + * @see #hasColumnAndRowData() + */ + @Override + public void insertColumns(final int index, final int numberOfColumns) { + if (index < 0 || index > getColumnCount()) { + throw new IndexOutOfBoundsException("The given index(" + index + + ") was outside of the current number of columns (0.." + + getColumnCount() + ")"); + } + + if (numberOfColumns < 1) { + throw new IllegalArgumentException( + "Number of columns must be 1 or greater (was " + + numberOfColumns); + } + + flyweightRow.addCells(index, numberOfColumns); + + for (int i = 0; i < numberOfColumns; i++) { + columns.add(index, new Column()); + } + + // Either all or none of the new columns are frozen + boolean frozen = index < frozenColumns; + if (frozen) { + frozenColumns += numberOfColumns; + } + + if (hasColumnAndRowData()) { + for (final AbstractRowContainer rowContainer : rowContainers) { + rowContainer.paintInsertColumns(index, numberOfColumns, + frozen); + } + } + } + + @Override + public int getColumnCount() { + return columns.size(); + } + + @Override + public void setFrozenColumnCount(int count) + throws IllegalArgumentException { + if (count < 0 || count > getColumnCount()) { + throw new IllegalArgumentException( + "count must be between 0 and the current number of columns (" + + columns + ")"); + } + int oldCount = frozenColumns; + if (count == oldCount) { + return; + } + + frozenColumns = count; + + if (hasSomethingInDom()) { + // Are we freezing or unfreezing? + boolean frozen = count > oldCount; + + int firstAffectedCol; + int firstUnaffectedCol; + + if (frozen) { + firstAffectedCol = oldCount; + firstUnaffectedCol = count; + } else { + firstAffectedCol = count; + firstUnaffectedCol = oldCount; + } + + for (int col = firstAffectedCol; col < firstUnaffectedCol; col++) { + header.setColumnFrozen(col, frozen); + body.setColumnFrozen(col, frozen); + footer.setColumnFrozen(col, frozen); + } + } + + scroller.recalculateScrollbarsForVirtualViewport(); + } + + @Override + public int getFrozenColumnCount() { + return frozenColumns; + } + + @Override + public void setColumnWidth(int index, int px) + throws IllegalArgumentException { + checkValidColumnIndex(index); + + columns.get(index).setWidth(px); + widthsArray = null; + + /* + * TODO [[optimize]]: only modify the elements that are actually + * modified. + */ + header.reapplyColumnWidths(); + body.reapplyColumnWidths(); + footer.reapplyColumnWidths(); + recalculateElementSizes(); + } + + private void checkValidColumnIndex(int index) + throws IllegalArgumentException { + if (!Range.withLength(0, getColumnCount()).contains(index)) { + throw new IllegalArgumentException("The given column index (" + + index + ") does not exist"); + } + } + + @Override + public int getColumnWidth(int index) throws IllegalArgumentException { + checkValidColumnIndex(index); + return columns.get(index).getDefinedWidth(); + } + + @Override + public int getColumnWidthActual(int index) { + return columns.get(index).getCalculatedWidth(); + } + + /** + * Calculates the width of the columns in a given range. + * + * @param columns + * the columns to calculate + * @return the total width of the columns in the given + * <code>columns</code> + */ + int getCalculatedColumnsWidth(@SuppressWarnings("hiding") + final Range columns) { + /* + * This is an assert instead of an exception, since this is an + * internal method. + */ + assert columns.isSubsetOf(Range.between(0, getColumnCount())) : "Range " + + "was outside of current column range (i.e.: " + + Range.between(0, getColumnCount()) + + ", but was given :" + + columns; + + int sum = 0; + for (int i = columns.getStart(); i < columns.getEnd(); i++) { + sum += getColumnWidthActual(i); + } + return sum; + } + + void setCalculatedColumnWidth(int index, int width) { + columns.get(index).calculatedWidth = width; + widthsArray = null; + } + + int[] getCalculatedColumnWidths() { + if (widthsArray == null || widthsArray.length != getColumnCount()) { + widthsArray = new int[getColumnCount()]; + for (int i = 0; i < columns.size(); i++) { + widthsArray[i] = columns.get(i).getCalculatedWidth(); + } + } + return widthsArray; + } + } + + // abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y + /** + * The solution to + * <code>|tan<sup>-1</sup>(<i>x</i>)|×(180/π) = 30</code> + * . + * <p> + * This constant is placed in the Escalator class, instead of an inner + * class, since even mathematical expressions aren't allowed in non-static + * inner classes for constants. + */ + private static final double RATIO_OF_30_DEGREES = 1 / Math.sqrt(3); + /** + * The solution to + * <code>|tan<sup>-1</sup>(<i>x</i>)|×(180/π) = 40</code> + * . + * <p> + * This constant is placed in the Escalator class, instead of an inner + * class, since even mathematical expressions aren't allowed in non-static + * inner classes for constants. + */ + private static final double RATIO_OF_40_DEGREES = Math.tan(2 * Math.PI / 9); + + private static final String DEFAULT_WIDTH = "400.0px"; + private static final String DEFAULT_HEIGHT = "400.0px"; + + private FlyweightRow flyweightRow = new FlyweightRow(this); + + /** The {@code <thead/>} tag. */ + private final Element headElem = DOM.createTHead(); + /** The {@code <tbody/>} tag. */ + private final Element bodyElem = DOM.createTBody(); + /** The {@code <tfoot/>} tag. */ + private final Element footElem = DOM.createTFoot(); + + /** + * TODO: investigate whether this field is now unnecessary, as + * {@link ScrollbarBundle} now caches its values. + * + * @deprecated maybe... + */ + @Deprecated + private int tBodyScrollTop = 0; + + /** + * TODO: investigate whether this field is now unnecessary, as + * {@link ScrollbarBundle} now caches its values. + * + * @deprecated maybe... + */ + @Deprecated + private int tBodyScrollLeft = 0; + + private final VerticalScrollbarBundle verticalScrollbar = new VerticalScrollbarBundle(); + private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle(); + + private final HeaderRowContainer header = new HeaderRowContainer(headElem); + private final BodyRowContainer body = new BodyRowContainer(bodyElem); + private final FooterRowContainer footer = new FooterRowContainer(footElem); + + private final Scroller scroller = new Scroller(); + + private final AbstractRowContainer[] rowContainers = new AbstractRowContainer[] { + header, body, footer }; + + private final ColumnConfigurationImpl columnConfiguration = new ColumnConfigurationImpl(); + private final Element tableWrapper; + + private PositionFunction position; + + private int internalScrollEventCalls = 0; + + /** The cached width of the escalator, in pixels. */ + private double width; + /** The cached height of the escalator, in pixels. */ + private double height; + + private static native double getPreciseWidth(Element element) + /*-{ + if (element.getBoundingClientRect) { + var rect = element.getBoundingClientRect(); + return rect.right - rect.left; + } else { + return element.offsetWidth; + } + }-*/; + + private static native double getPreciseHeight(Element element) + /*-{ + if (element.getBoundingClientRect) { + var rect = element.getBoundingClientRect(); + return rect.bottom - rect.top; + } else { + return element.offsetHeight; + } + }-*/; + + /** + * Creates a new Escalator widget instance. + */ + public Escalator() { + + detectAndApplyPositionFunction(); + getLogger().info( + "Using " + position.getClass().getSimpleName() + + " for position"); + + final Element root = DOM.createDiv(); + setElement(root); + + root.appendChild(verticalScrollbar.getElement()); + root.appendChild(horizontalScrollbar.getElement()); + verticalScrollbar.setScrollbarThickness(Util.getNativeScrollbarSize()); + horizontalScrollbar + .setScrollbarThickness(Util.getNativeScrollbarSize()); + + tableWrapper = DOM.createDiv(); + + root.appendChild(tableWrapper); + + final Element table = DOM.createTable(); + tableWrapper.appendChild(table); + + table.appendChild(headElem); + table.appendChild(bodyElem); + table.appendChild(footElem); + + setStylePrimaryName("v-escalator"); + + // init default dimensions + setHeight(null); + setWidth(null); + } + + @Override + protected void onLoad() { + super.onLoad(); + + header.paintInsertRows(0, header.getRowCount()); + footer.paintInsertRows(0, footer.getRowCount()); + recalculateElementSizes(); + /* + * Note: There's no need to explicitly insert rows into the body. + * + * recalculateElementSizes will recalculate the height of the body. This + * has the side-effect that as the body's size grows bigger (i.e. from 0 + * to its actual height), more escalator rows are populated. Those + * escalator rows are then immediately rendered. This, in effect, is the + * same thing as inserting those rows. + * + * In fact, having an extra paintInsertRows here would lead to duplicate + * rows. + */ + + scroller.attachScrollListener(verticalScrollbar.getElement()); + scroller.attachScrollListener(horizontalScrollbar.getElement()); + scroller.attachMousewheelListener(getElement()); + scroller.attachTouchListeners(getElement()); + } + + @Override + protected void onUnload() { + + scroller.detachScrollListener(verticalScrollbar.getElement()); + scroller.detachScrollListener(horizontalScrollbar.getElement()); + scroller.detachMousewheelListener(getElement()); + scroller.detachTouchListeners(getElement()); + + header.paintRemoveRows(0, header.getRowCount()); + footer.paintRemoveRows(0, footer.getRowCount()); + body.paintRemoveRows(0, body.getRowCount()); + + super.onUnload(); + } + + private void detectAndApplyPositionFunction() { + /* + * firefox has a bug in its translate operation, showing white space + * when adjusting the scrollbar in BodyRowContainer.paintInsertRows + */ + if (Window.Navigator.getUserAgent().contains("Firefox")) { + position = new AbsolutePosition(); + return; + } + + final Style docStyle = Document.get().getBody().getStyle(); + if (hasProperty(docStyle, "transform")) { + if (hasProperty(docStyle, "transformStyle")) { + position = new Translate3DPosition(); + } else { + position = new TranslatePosition(); + } + } else if (hasProperty(docStyle, "webkitTransform")) { + position = new WebkitTranslate3DPosition(); + } else { + position = new AbsolutePosition(); + } + } + + private Logger getLogger() { + return Logger.getLogger(getClass().getName()); + } + + private static native boolean hasProperty(Style style, String name) + /*-{ + return style[name] !== undefined; + }-*/; + + /** + * Check whether there are both columns and any row data (for either + * headers, body or footer). + * + * @return <code>true</code> iff header, body or footer has rows && there + * are columns + */ + private boolean hasColumnAndRowData() { + return (header.getRowCount() > 0 || body.getRowCount() > 0 || footer + .getRowCount() > 0) && columnConfiguration.getColumnCount() > 0; + } + + /** + * Check whether there are any cells in the DOM. + * + * @return <code>true</code> iff header, body or footer has any child + * elements + */ + private boolean hasSomethingInDom() { + return headElem.hasChildNodes() || bodyElem.hasChildNodes() + || footElem.hasChildNodes(); + } + + /** + * Returns the representation of this Escalator header. + * + * @return the header. Never <code>null</code> + */ + public RowContainer getHeader() { + return header; + } + + /** + * Returns the representation of this Escalator body. + * + * @return the body. Never <code>null</code> + */ + public RowContainer getBody() { + return body; + } + + /** + * Returns the representation of this Escalator footer. + * + * @return the footer. Never <code>null</code> + */ + public RowContainer getFooter() { + return footer; + } + + /** + * Returns the configuration object for the columns in this Escalator. + * + * @return the configuration object for the columns in this Escalator. Never + * <code>null</code> + */ + public ColumnConfiguration getColumnConfiguration() { + return columnConfiguration; + } + + /* + * TODO remove method once RequiresResize and the Vaadin layoutmanager + * listening mechanisms are implemented (https://trello.com/c/r3Kh0Kfy) + */ + @Override + public void setWidth(final String width) { + super.setWidth(width != null && !width.isEmpty() ? width + : DEFAULT_WIDTH); + recalculateElementSizes(); + } + + /* + * TODO remove method once RequiresResize and the Vaadin layoutmanager + * listening mechanisms are implemented (https://trello.com/c/r3Kh0Kfy) + */ + @Override + public void setHeight(final String height) { + final int escalatorRowsBefore = body.visualRowOrder.size(); + + super.setHeight(height != null && !height.isEmpty() ? height + : DEFAULT_HEIGHT); + recalculateElementSizes(); + + if (escalatorRowsBefore != body.visualRowOrder.size()) { + fireRowVisibilityChangeEvent(); + } + } + + /** + * Returns the vertical scroll offset. Note that this is not necessarily the + * same as the scroll top in the DOM + * + * @return the logical vertical scroll offset + */ + public double getScrollTop() { + return verticalScrollbar.getScrollPos(); + } + + /** + * Sets the vertical scroll offset. Note that this is not necessarily the + * same as the scroll top in the DOM + * + * @param scrollTop + * the number of pixels to scroll vertically + */ + public void setScrollTop(final double scrollTop) { + verticalScrollbar.setScrollPos((int) scrollTop); + } + + /** + * Returns the logical horizontal scroll offset. Note that this is not + * necessarily the same as the scroll left in the DOM. + * + * @return the logical horizontal scroll offset + */ + public int getScrollLeft() { + return horizontalScrollbar.getScrollPos(); + } + + /** + * Sets the logical horizontal scroll offset. Note that this is not + * necessarily the same as the scroll left in the DOM. + * + * @param scrollLeft + * the number of pixels to scroll horizontally + */ + public void setScrollLeft(final int scrollLeft) { + horizontalScrollbar.setScrollPos(scrollLeft); + } + + /** + * Scrolls the body horizontally so that the column at the given index is + * visible and there is at least {@code padding} pixels to the given scroll + * destination. + * + * @param columnIndex + * the index of the column to scroll to + * @param destination + * where the column should be aligned visually after scrolling + * @param padding + * the number pixels to place between the scrolled-to column and + * the viewport edge. + * @throws IndexOutOfBoundsException + * if {@code columnIndex} is not a valid index for an existing + * column + * @throws IllegalArgumentException + * if {@code destination} is {@link ScrollDestination#MIDDLE} + * and padding is nonzero, because having a padding on a + * centered column is undefined behavior, or if the column is + * frozen + */ + public void scrollToColumn(final int columnIndex, + final ScrollDestination destination, final int padding) + throws IndexOutOfBoundsException, IllegalArgumentException { + if (destination == ScrollDestination.MIDDLE && padding != 0) { + throw new IllegalArgumentException( + "You cannot have a padding with a MIDDLE destination"); + } + verifyValidColumnIndex(columnIndex); + + if (columnIndex < columnConfiguration.frozenColumns) { + throw new IllegalArgumentException("The given column index " + + columnIndex + " is frozen."); + } + + scroller.scrollToColumn(columnIndex, destination, padding); + } + + private void verifyValidColumnIndex(final int columnIndex) + throws IndexOutOfBoundsException { + if (columnIndex < 0 + || columnIndex >= columnConfiguration.getColumnCount()) { + throw new IndexOutOfBoundsException("The given column index " + + columnIndex + " does not exist."); + } + } + + /** + * Scrolls the body vertically so that the row at the given index is visible + * and there is at least {@literal padding} pixels to the given scroll + * destination. + * + * @param rowIndex + * the index of the logical row to scroll to + * @param destination + * where the row should be aligned visually after scrolling + * @param padding + * the number pixels to place between the scrolled-to row and the + * viewport edge. + * @throws IndexOutOfBoundsException + * if {@code rowIndex} is not a valid index for an existing row + * @throws IllegalArgumentException + * if {@code destination} is {@link ScrollDestination#MIDDLE} + * and padding is nonzero, because having a padding on a + * centered row is undefined behavior + */ + public void scrollToRow(final int rowIndex, + final ScrollDestination destination, final int padding) + throws IndexOutOfBoundsException, IllegalArgumentException { + if (destination == ScrollDestination.MIDDLE && padding != 0) { + throw new IllegalArgumentException( + "You cannot have a padding with a MIDDLE destination"); + } + verifyValidRowIndex(rowIndex); + + scroller.scrollToRow(rowIndex, destination, padding); + } + + private void verifyValidRowIndex(final int rowIndex) { + if (rowIndex < 0 || rowIndex >= body.getRowCount()) { + throw new IndexOutOfBoundsException("The given row index " + + rowIndex + " does not exist."); + } + } + + /** + * Recalculates the dimensions for all elements that require manual + * calculations. Also updates the dimension caches. + * <p> + * <em>Note:</em> This method has the <strong>side-effect</strong> + * automatically makes sure that an appropriate amount of escalator rows are + * present. So, if the body area grows, more <strong>escalator rows might be + * inserted</strong>. Conversely, if the body area shrinks, + * <strong>escalator rows might be removed</strong>. + */ + private void recalculateElementSizes() { + if (!isAttached()) { + return; + } + + Profiler.enter("Escalator.recalculateElementSizes"); + width = getPreciseWidth(getElement()); + height = getPreciseHeight(getElement()); + for (final AbstractRowContainer rowContainer : rowContainers) { + rowContainer.recalculateSectionHeight(); + } + + scroller.recalculateScrollbarsForVirtualViewport(); + body.verifyEscalatorCount(); + Profiler.leave("Escalator.recalculateElementSizes"); + } + + /** + * A routing method for {@link Scroller#onScroll(double, double)}. + * <p> + * This is a workaround for GWT and JSNI unable to properly handle inner + * classes, so instead we call the outer class' method, which calls the + * inner class' respective method. + * <p> + * Ideally, this method would not exist, and + * {@link Scroller#onScroll(double, double)} would be called directly. + */ + private void onScroll() { + scroller.onScroll(); + } + + /** + * Snap deltas of x and y to the major four axes (up, down, left, right) + * with a threshold of a number of degrees from those axes. + * + * @param deltaX + * the delta in the x axis + * @param deltaY + * the delta in the y axis + * @param thresholdRatio + * the threshold in ratio (0..1) between x and y for when to snap + * @return a two-element array: <code>[snappedX, snappedY]</code> + */ + private static double[] snapDeltas(final double deltaX, + final double deltaY, final double thresholdRatio) { + + final double[] array = new double[2]; + if (deltaX != 0 && deltaY != 0) { + final double aDeltaX = Math.abs(deltaX); + final double aDeltaY = Math.abs(deltaY); + final double yRatio = aDeltaY / aDeltaX; + final double xRatio = aDeltaX / aDeltaY; + + array[0] = (xRatio < thresholdRatio) ? 0 : deltaX; + array[1] = (yRatio < thresholdRatio) ? 0 : deltaY; + } else { + array[0] = deltaX; + array[1] = deltaY; + } + + return array; + } + + /** + * Adds an event handler that gets notified when the range of visible rows + * changes e.g. because of scrolling. + * + * @param rowVisibilityChangeHandler + * the event handler + * @return a handler registration for the added handler + */ + public HandlerRegistration addRowVisibilityChangeHandler( + RowVisibilityChangeHandler rowVisibilityChangeHandler) { + return addHandler(rowVisibilityChangeHandler, + RowVisibilityChangeEvent.TYPE); + } + + private void fireRowVisibilityChangeEvent() { + if (!body.visualRowOrder.isEmpty()) { + int visibleRangeStart = body.getLogicalRowIndex(body.visualRowOrder + .getFirst()); + int visibleRangeEnd = body.getLogicalRowIndex(body.visualRowOrder + .getLast()) + 1; + + int visibleRowCount = visibleRangeEnd - visibleRangeStart; + + fireEvent(new RowVisibilityChangeEvent(visibleRangeStart, + visibleRowCount)); + } else { + fireEvent(new RowVisibilityChangeEvent(0, 0)); + } + } + + /** + * Accesses the package private method Widget#setParent() + * + * @param widget + * The widget to access + * @param parent + * The parent to set + */ + static native final void setParent(Widget widget, Widget parent) + /*-{ + widget.@com.google.gwt.user.client.ui.Widget::setParent(Lcom/google/gwt/user/client/ui/Widget;)(parent); + }-*/; + + /** + * Returns the widget from a cell node or <code>null</code> if there is no + * widget in the cell + * + * @param cellNode + * The cell node + */ + static Widget getWidgetFromCell(Node cellNode) { + Node possibleWidgetNode = cellNode.getFirstChild(); + if (possibleWidgetNode != null + && possibleWidgetNode.getNodeType() == Node.ELEMENT_NODE) { + @SuppressWarnings("deprecation") + com.google.gwt.user.client.Element castElement = (com.google.gwt.user.client.Element) possibleWidgetNode + .cast(); + return Util.findWidget(castElement, null); + } + return null; + } + + /** + * Forces the escalator to recalculate the widths of its columns. + * <p> + * All columns that haven't been assigned an explicit width will be resized + * to fit all currently visible contents. + * + * @see ColumnConfiguration#setColumnWidth(int, int) + */ + public void calculateColumnWidths() { + boolean widthsHaveChanged = false; + for (int colIndex = 0; colIndex < columnConfiguration.getColumnCount(); colIndex++) { + if (columnConfiguration.getColumnWidth(colIndex) >= 0) { + continue; + } + + final int oldColumnWidth = columnConfiguration + .getColumnWidthActual(colIndex); + + int maxColumnWidth = 0; + maxColumnWidth = Math.max(maxColumnWidth, + header.calculateMaxColWidth(colIndex)); + maxColumnWidth = Math.max(maxColumnWidth, + body.calculateMaxColWidth(colIndex)); + maxColumnWidth = Math.max(maxColumnWidth, + footer.calculateMaxColWidth(colIndex)); + + Logger.getLogger("Escalator.calculateColumnWidths").info( + "#" + colIndex + ": " + maxColumnWidth + "px"); + + if (oldColumnWidth != maxColumnWidth) { + columnConfiguration.setCalculatedColumnWidth(colIndex, + maxColumnWidth); + widthsHaveChanged = true; + } + } + + if (widthsHaveChanged) { + header.reapplyColumnWidths(); + body.reapplyColumnWidths(); + footer.reapplyColumnWidths(); + recalculateElementSizes(); + } + } + + @Override + public void setStylePrimaryName(String style) { + super.setStylePrimaryName(style); + + verticalScrollbar.setStylePrimaryName(style); + horizontalScrollbar.setStylePrimaryName(style); + + UIObject.setStylePrimaryName(tableWrapper, style + "-tablewrapper"); + + header.setStylePrimaryName(style); + body.setStylePrimaryName(style); + footer.setStylePrimaryName(style); + } +} diff --git a/client/src/com/vaadin/client/ui/grid/EscalatorUpdater.java b/client/src/com/vaadin/client/ui/grid/EscalatorUpdater.java new file mode 100644 index 0000000000..283517fcc4 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/EscalatorUpdater.java @@ -0,0 +1,66 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + + +/** + * A functional interface that allows client code to define how a certain row in + * Escalator will be displayed. The contents of an escalator's header, body and + * footer are rendered by their respective updaters. + * <p> + * The updater is responsible for internally handling all remote communication, + * should the displayed data need to be fetched remotely. + * + * @since 7.2 + * @author Vaadin Ltd + * @see RowContainer#setEscalatorUpdater(EscalatorUpdater) + * @see Escalator#getHeader() + * @see Escalator#getBody() + * @see Escalator#getFooter() + */ +public interface EscalatorUpdater { + /** An {@link EscalatorUpdater} that doesn't render anything. */ + public static final EscalatorUpdater NULL = new EscalatorUpdater() { + @Override + public void updateCells(final Row row, + final Iterable<Cell> cellsToUpdate) { + // NOOP + } + }; + + /** + * Renders a row contained in a row container. + * <p> + * <em>Note:</em> If rendering of cells is deferred (e.g. because + * asynchronous data retrieval), this method is responsible for explicitly + * displaying some placeholder data (empty content is valid). Because the + * cells (and rows) in an escalator are recycled, failing to reset a cell + * will lead to invalid data being displayed in the escalator. + * <p> + * For performance reasons, the escalator will never autonomously clear any + * data in a cell. + * + * @param row + * information about the row to update. <em>Note:</em> You should + * not store nor reuse this reference + * @param cellsToUpdate + * a collection of cells which need to be updated. <em>Note:</em> + * You should neither store nor reuse the reference to the list, + * nor to the individual cells + */ + public void updateCells(Row row, Iterable<Cell> cellsToUpdate); +} diff --git a/client/src/com/vaadin/client/ui/grid/FlyweightCell.java b/client/src/com/vaadin/client/ui/grid/FlyweightCell.java new file mode 100644 index 0000000000..296a70934b --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/FlyweightCell.java @@ -0,0 +1,205 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +import java.util.List; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.user.client.ui.IsWidget; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ui.grid.FlyweightRow.CellIterator; + +/** + * An internal implementation of the {@link Cell} interface. + * <p> + * These instances are populated into a {@link FlyweightRow} instance, and + * intended to be reused when rendering cells in an escalator. + * + * @since 7.2 + * @author Vaadin Ltd + * @see FlyweightRow#getCells() + * @see FlyweightRow#addCells(int, int) + * @see FlyweightRow#removeCells(int, int) + */ +class FlyweightCell implements Cell { + static final String COLSPAN_ATTR = "colSpan"; + + private final int column; + private final FlyweightRow row; + + private CellIterator currentIterator = null; + + private final Escalator escalator; + + public FlyweightCell(final FlyweightRow row, final int column, + Escalator escalator) { + this.row = row; + this.column = column; + this.escalator = escalator; + } + + @Override + public int getRow() { + assertSetup(); + return row.getRow(); + } + + @Override + public int getColumn() { + assertSetup(); + return column; + } + + @Override + public Element getElement() { + return (Element) row.getElement().getChild(column); + } + + void setup(final CellIterator cellIterator) { + currentIterator = cellIterator; + + final Element e = getElement(); + e.setPropertyInt(COLSPAN_ATTR, 1); + e.getStyle().setWidth(row.getColumnWidth(column), Unit.PX); + e.getStyle().clearDisplay(); + } + + /** + * Tear down the state of the Cell. + * <p> + * This is an internal check method, to prevent retrieving uninitialized + * data by calling {@link #getRow()}, {@link #getColumn()} or + * {@link #getElement()} at an improper time. + * <p> + * This should only be used with asserts (" + * <code>assert flyweightCell.teardown()</code> ") so that the code is never + * run when asserts aren't enabled. + * + * @return always <code>true</code> + * @see FlyweightRow#teardown() + */ + boolean teardown() { + currentIterator = null; + return true; + } + + /** + * Asserts that the flyweight cell has properly been set up before trying to + * access any of its data. + */ + private void assertSetup() { + assert currentIterator != null : "FlyweightCell was not properly " + + "initialized. This is either a bug in Grid/Escalator " + + "or a Cell reference has been stored and reused " + + "inappropriately."; + } + + @Override + public void setColSpan(final int numberOfCells) { + /*- + * This will default to 1 if unset, as per DOM specifications: + * http://www.w3.org/TR/html5/tabular-data.html#attributes-common-to-td-and-th-elements + */ + final int prevColSpan = getElement().getPropertyInt(COLSPAN_ATTR); + if (numberOfCells == 1 && prevColSpan == 1) { + return; + } + + getElement().setPropertyInt(COLSPAN_ATTR, numberOfCells); + adjustCellWidthForSpan(numberOfCells); + hideOrRevealAdjacentCellElements(numberOfCells, prevColSpan); + currentIterator.setSkipNext(numberOfCells - 1); + } + + private void adjustCellWidthForSpan(final int numberOfCells) { + final int cellsToTheRight = currentIterator.rawPeekNext( + numberOfCells - 1).size(); + + final int selfWidth = row.getColumnWidth(column); + int widthsOfColumnsToTheRight = 0; + for (int i = 0; i < cellsToTheRight; i++) { + widthsOfColumnsToTheRight += row.getColumnWidth(column + i + 1); + } + getElement().getStyle().setWidth(selfWidth + widthsOfColumnsToTheRight, + Unit.PX); + } + + private void hideOrRevealAdjacentCellElements(final int numberOfCells, + final int prevColSpan) { + final int affectedCellsNumber = Math.max(prevColSpan, numberOfCells); + final List<FlyweightCell> affectedCells = currentIterator + .rawPeekNext(affectedCellsNumber - 1); + if (prevColSpan < numberOfCells) { + for (int i = 0; i < affectedCells.size(); i++) { + affectedCells.get(prevColSpan + i - 1).getElement().getStyle() + .setDisplay(Display.NONE); + } + } else if (prevColSpan > numberOfCells) { + for (int i = 0; i < affectedCells.size(); i++) { + affectedCells.get(numberOfCells + i - 1).getElement() + .getStyle().clearDisplay(); + } + } + } + + @Override + public Widget getWidget() { + return Escalator.getWidgetFromCell(getElement()); + } + + @Override + public void setWidget(Widget widget) { + + Widget oldWidget = getWidget(); + + // Validate + if (oldWidget == widget) { + return; + } + + // Detach old child. + if (oldWidget != null) { + // Orphan. + Escalator.setParent(oldWidget, null); + + // Physical detach. + getElement().removeChild(oldWidget.getElement()); + } + + // Remove any previous text nodes from previous + // setInnerText/setInnerHTML + getElement().removeAllChildren(); + + // Attach new child. + if (widget != null) { + // Detach new child from old parent. + widget.removeFromParent(); + + // Physical attach. + getElement().appendChild(widget.getElement()); + + Escalator.setParent(widget, escalator); + } + } + + @Override + public void setWidget(IsWidget w) { + setWidget(Widget.asWidgetOrNull(w)); + } + +} diff --git a/client/src/com/vaadin/client/ui/grid/FlyweightRow.java b/client/src/com/vaadin/client/ui/grid/FlyweightRow.java new file mode 100644 index 0000000000..6bfd368c6b --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/FlyweightRow.java @@ -0,0 +1,217 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Node; + +/** + * An internal implementation of the {@link Row} interface. + * <p> + * There is only one instance per Escalator. This is designed to be re-used when + * rendering rows. + * + * @since 7.2 + * @author Vaadin Ltd + * @see Escalator.AbstractRowContainer#refreshRow(Node, int) + */ +class FlyweightRow implements Row { + + static class CellIterator implements Iterator<Cell> { + /** A defensive copy of the cells in the current row. */ + private final ArrayList<FlyweightCell> cells; + private int cursor = 0; + private int skipNext = 0; + + public CellIterator(final Collection<FlyweightCell> cells) { + this.cells = new ArrayList<FlyweightCell>(cells); + } + + @Override + public boolean hasNext() { + return cursor + skipNext < cells.size(); + } + + @Override + public FlyweightCell next() { + // if we needed to skip some cells since the last invocation. + for (int i = 0; i < skipNext; i++) { + cells.remove(cursor); + } + skipNext = 0; + + final FlyweightCell cell = cells.get(cursor++); + cell.setup(this); + return cell; + } + + @Override + public void remove() { + throw new UnsupportedOperationException( + "Cannot remove cells via iterator"); + } + + /** + * Sets the number of cells to skip when {@link #next()} is called the + * next time. Cell hiding is also handled eagerly in this method. + * + * @param colspan + * the number of cells to skip on next invocation of + * {@link #next()} + */ + public void setSkipNext(final int colspan) { + assert colspan > 0 : "Number of cells didn't make sense: " + + colspan; + skipNext = colspan; + } + + /** + * Gets the next <code>n</code> cells in the iterator, ignoring any + * possibly spanned cells. + * + * @param n + * the number of next cells to retrieve + * @return A list of next <code>n</code> cells, or less if there aren't + * enough cells to retrieve + */ + public List<FlyweightCell> rawPeekNext(final int n) { + final int from = Math.min(cursor, cells.size()); + final int to = Math.min(cursor + n, cells.size()); + return cells.subList(from, to); + } + } + + private static final int BLANK = Integer.MIN_VALUE; + + private int row; + private Element element; + private int[] columnWidths = null; + private final Escalator escalator; + private final List<FlyweightCell> cells = new ArrayList<FlyweightCell>(); + + public FlyweightRow(final Escalator escalator) { + this.escalator = escalator; + } + + @Override + public Escalator getEscalator() { + return escalator; + } + + void setup(final Element e, final int row, int[] columnWidths) { + element = e; + this.row = row; + this.columnWidths = columnWidths; + } + + /** + * Tear down the state of the Row. + * <p> + * This is an internal check method, to prevent retrieving uninitialized + * data by calling {@link #getRow()}, {@link #getElement()} or + * {@link #getCells()} at an improper time. + * <p> + * This should only be used with asserts (" + * <code>assert flyweightRow.teardown()</code> ") so that the code is never + * run when asserts aren't enabled. + * + * @return always <code>true</code> + */ + boolean teardown() { + element = null; + row = BLANK; + columnWidths = null; + for (final FlyweightCell cell : cells) { + assert cell.teardown(); + } + return true; + } + + @Override + public int getRow() { + assertSetup(); + return row; + } + + @Override + public Element getElement() { + assertSetup(); + return element; + } + + void addCells(final int index, final int numberOfColumns) { + for (int i = 0; i < numberOfColumns; i++) { + final int col = index + i; + cells.add(col, new FlyweightCell(this, col, escalator)); + } + updateRestOfCells(index + numberOfColumns); + } + + void removeCells(final int index, final int numberOfColumns) { + for (int i = 0; i < numberOfColumns; i++) { + cells.remove(index); + } + updateRestOfCells(index); + } + + private void updateRestOfCells(final int startPos) { + // update the column number for the cells to the right + for (int col = startPos; col < cells.size(); col++) { + cells.set(col, new FlyweightCell(this, col, escalator)); + } + } + + /** + * Get flyweight cells for the client code to render. + * + * @return a list of {@link FlyweightCell FlyweightCells}. They are + * generified into {@link Cell Cells}, because Java's generics + * system isn't expressive enough. + * @see #setup(Element, int) + * @see #teardown() + */ + Iterable<Cell> getCells() { + assertSetup(); + return new Iterable<Cell>() { + @Override + public Iterator<Cell> iterator() { + return new CellIterator(cells); + } + }; + } + + /** + * Asserts that the flyweight row has properly been set up before trying to + * access any of its data. + */ + private void assertSetup() { + assert element != null && row != BLANK && columnWidths != null : "Flyweight row was not " + + "properly initialized. Make sure the setup-method is " + + "called before retrieving data. This is either a bug " + + "in Escalator, or the instance of the flyweight row " + + "has been stored and accessed."; + } + + int getColumnWidth(int column) { + assertSetup(); + return columnWidths[column]; + } +} diff --git a/client/src/com/vaadin/client/ui/grid/Grid.java b/client/src/com/vaadin/client/ui/grid/Grid.java new file mode 100644 index 0000000000..02aa194655 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/Grid.java @@ -0,0 +1,1318 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +import com.google.gwt.core.shared.GWT; +import com.google.gwt.dom.client.Element; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.HasVisibility; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.data.DataChangeHandler; +import com.vaadin.client.data.DataSource; +import com.vaadin.client.ui.grid.renderers.TextRenderer; +import com.vaadin.shared.ui.grid.GridConstants; +import com.vaadin.shared.ui.grid.ScrollDestination; +import com.vaadin.shared.util.SharedUtil; + +/** + * A data grid view that supports columns and lazy loading of data rows from a + * data source. + * + * <h3>Columns</h3> + * <p> + * The {@link GridColumn} class defines the renderer used to render a cell in + * the grid. Implement {@link GridColumn#getValue(Object)} to retrieve the cell + * value from the row object and return the cell renderer to render that cell. + * </p> + * <p> + * {@link GridColumn}s contain other properties like the width of the column and + * the visiblity of the column. If you want to change a column's properties + * after it has been added to the grid you can get a column object for a + * specific column index using {@link Grid#getColumn(int)}. + * </p> + * <p> + * + * TODO Explain about headers/footers once the multiple header/footer api has + * been implemented + * + * <h3>Data sources</h3> + * <p> + * TODO Explain about what a data source is and how it should be implemented. + * </p> + * + * @param <T> + * The row type of the grid. The row type is the POJO type from where + * the data is retrieved into the column cells. + * @since 7.2 + * @author Vaadin Ltd + */ +public class Grid<T> extends Composite { + + /** + * Escalator used internally by grid to render the rows + */ + private Escalator escalator = GWT.create(Escalator.class); + + /** + * List of columns in the grid. Order defines the visible order. + */ + private final List<GridColumn<?, T>> columns = new ArrayList<GridColumn<?, T>>(); + + /** + * The datasource currently in use. <em>Note:</em> it is <code>null</code> + * on initialization, but not after that. + */ + private DataSource<T> dataSource; + + /** + * The column groups rows added to the grid + */ + private final List<ColumnGroupRow<T>> columnGroupRows = new ArrayList<ColumnGroupRow<T>>(); + + /** + * Are the headers for the columns visible + */ + private boolean columnHeadersVisible = true; + + /** + * Are the footers for the columns visible + */ + private boolean columnFootersVisible = false; + + /** + * The last column frozen counter from the left + */ + private GridColumn<?, T> lastFrozenColumn; + + /** + * Base class for grid columns internally used by the Grid. The user should + * use {@link GridColumn} when creating new columns. + * + * @param <C> + * the column type + * + * @param <T> + * the row type + */ + static abstract class AbstractGridColumn<C, T> implements HasVisibility { + + /** + * The grid the column is associated with + */ + private Grid<T> grid; + + /** + * Should the column be visible in the grid + */ + private boolean visible = true; + + /** + * The text displayed in the header of the column + */ + private String header; + + /** + * Text displayed in the column footer + */ + private String footer; + + /** + * Width of column in pixels + */ + private int width = 100; + + /** + * Renderer for rendering a value into the cell + */ + private Renderer<C> bodyRenderer = new Renderer<C>() { + + @Override + public void renderCell(Cell cell, C value) { + if (value instanceof Widget) { + cell.setWidget((Widget) value); + } else if (value instanceof String) { + cell.getElement().setInnerText(value.toString()); + } else { + throw new IllegalArgumentException( + "Cell value cannot be converted into a String. Please use a custom renderer to convert the value."); + } + } + }; + + /** + * Renderer for rendering the header cell value into the cell + */ + private Renderer<String> headerRenderer = new TextRenderer(); + + /** + * Renderer for rendering the footer cell value into the cell + */ + private Renderer<String> footerRenderer = new TextRenderer(); + + /** + * Constructs a new column. + */ + public AbstractGridColumn() { + + } + + /** + * Constructs a new column with a custom renderer. + * + * @param renderer + * The renderer to use for rendering the cells + */ + public AbstractGridColumn(Renderer<C> renderer) { + if (renderer == null) { + throw new IllegalArgumentException("Renderer cannot be null."); + } + this.bodyRenderer = renderer; + } + + /** + * Constructs a new column with custom renderers for rows, header and + * footer cells. + * + * @param bodyRenderer + * The renderer to use for rendering body cells + * @param headerRenderer + * The renderer to use for rendering header cells + * @param footerRenderer + * The renderer to use for rendering footer cells + */ + public AbstractGridColumn(Renderer<C> bodyRenderer, + Renderer<String> headerRenderer, Renderer<String> footerRenderer) { + this(bodyRenderer); + if (headerRenderer == null || footerRenderer == null) { + throw new IllegalArgumentException("Renderer cannot be null."); + } + + this.headerRenderer = headerRenderer; + this.footerRenderer = footerRenderer; + } + + /** + * Internally used by the grid to set itself + * + * @param grid + */ + private void setGrid(Grid<T> grid) { + if (this.grid != null && grid != null) { + // Trying to replace grid + throw new IllegalStateException( + "Column already is attached to grid. Remove the column first from the grid and then add it."); + } + + this.grid = grid; + + setVisible(this.visible); + setWidth(this.width); + setHeaderCaption(this.header); + setFooterCaption(this.footer); + } + + /** + * Gets text in the header of the column. By default the header caption + * is empty. + * + * @return the text displayed in the column caption + */ + public String getHeaderCaption() { + return header; + } + + /** + * Returns the renderer used for rendering the header cells + * + * @return a renderer that renders header cells + */ + public Renderer<String> getHeaderRenderer() { + return headerRenderer; + } + + /** + * Sets the renderer that renders header cells. Should not be null. + * + * @param renderer + * The renderer to use for rendering header cells. + */ + public void setHeaderRenderer(Renderer<String> renderer) { + if (renderer == null) { + throw new IllegalArgumentException("Renderer cannot be null."); + } + headerRenderer = renderer; + if (grid != null) { + grid.refreshHeader(); + } + } + + /** + * Returns the renderer used for rendering the footer cells + * + * @return a renderer that renders footer cells + */ + public Renderer<String> getFooterRenderer() { + return footerRenderer; + } + + /** + * Sets the renderer that renders footer cells. Should not be null. + * + * @param renderer + * The renderer to use for rendering footer cells. + */ + public void setFooterRenderer(Renderer<String> renderer) { + if (renderer == null) { + throw new IllegalArgumentException("Renderer cannot be null."); + } + footerRenderer = renderer; + if (grid != null) { + grid.refreshFooter(); + } + } + + /** + * Sets the text in the header of the column. + * + * @param caption + * the text displayed in the column header + */ + public void setHeaderCaption(String caption) { + if (SharedUtil.equals(caption, header)) { + return; + } + + header = caption; + + if (grid != null) { + grid.refreshHeader(); + } + } + + /** + * Gets text in the footer of the column. By default the footer caption + * is empty. + * + * @return The text displayed in the footer of the column + */ + public String getFooterCaption() { + return footer; + } + + /** + * Sets text in the footer of the column. + * + * @param caption + * the text displayed in the footer of the column + */ + public void setFooterCaption(String caption) { + if (SharedUtil.equals(caption, footer)) { + return; + } + + footer = caption; + + if (grid != null) { + grid.refreshFooter(); + } + } + + /** + * Is the column visible. By default all columns are visible. + * + * @return <code>true</code> if the column is visible + */ + @Override + public boolean isVisible() { + return visible; + } + + /** + * Sets a column as visible in the grid. + * + * @param visible + * <code>true</code> if the column should be displayed in the + * grid + */ + @Override + public void setVisible(boolean visible) { + if (this.visible == visible) { + return; + } + + this.visible = visible; + + // Remove column + if (grid != null) { + int index = findIndexOfColumn(); + ColumnConfiguration conf = grid.escalator + .getColumnConfiguration(); + + if (visible) { + conf.insertColumns(index, 1); + } else { + conf.removeColumns(index, 1); + } + } + + } + + /** + * Returns the data that should be rendered into the cell. By default + * returning Strings and Widgets are supported. If the return type is a + * String then it will be treated as preformatted text. + * <p> + * To support other types you will need to pass a custom renderer to the + * column via the column constructor. + * + * @param row + * The row object that provides the cell content. + * + * @return The cell content + */ + public abstract C getValue(T row); + + /** + * The renderer to render the cell width. By default renders the data as + * a String or adds the widget into the cell if the column type is of + * widget type. + * + * @return The renderer to render the cell content with + */ + public Renderer<C> getRenderer() { + return bodyRenderer; + } + + /** + * Finds the index of this column instance + * + */ + private int findIndexOfColumn() { + return grid.findVisibleColumnIndex((GridColumn<?, T>) this); + } + + /** + * Sets the pixel width of the column. Use a negative value for the grid + * to autosize column based on content and available space + * + * @param pixels + * the width in pixels or negative for auto sizing + */ + public void setWidth(int pixels) { + this.width = pixels; + + if (grid != null && isVisible()) { + int index = findIndexOfColumn(); + ColumnConfiguration conf = grid.escalator + .getColumnConfiguration(); + conf.setColumnWidth(index, pixels); + } + } + + /** + * Returns the pixel width of the column + * + * @return pixel width of the column + */ + public int getWidth() { + if (grid == null) { + return this.width; + } else { + int index = findIndexOfColumn(); + ColumnConfiguration conf = grid.escalator + .getColumnConfiguration(); + return conf.getColumnWidth(index); + } + } + } + + /** + * Base class for header / footer escalator updater + */ + protected abstract class HeaderFooterEscalatorUpdater implements + EscalatorUpdater { + + /** + * The row container which contains the header or footer rows + */ + private RowContainer rows; + + /** + * Should the index be counted from 0-> or 0<- + */ + private boolean inverted; + + /** + * Constructs an updater for updating a header / footer + * + * @param rows + * The row container + * @param inverted + * Should index counting be inverted + */ + public HeaderFooterEscalatorUpdater(RowContainer rows, boolean inverted) { + this.rows = rows; + this.inverted = inverted; + } + + /** + * Gets the header/footer caption value + * + * @param column + * The column to get the value for. + * + * @return The value that should be rendered for the column caption + */ + public abstract String getColumnValue(GridColumn<?, T> column); + + /** + * Gets the group caption value + * + * @param group + * The group for with the caption value should be returned + * @return The value that should be rendered for the column caption + */ + public abstract String getGroupValue(ColumnGroup<T> group); + + /** + * Is the row visible in the header/footer + * + * @param row + * the row to check + * + * @return <code>true</code> if the row should be visible + */ + public abstract boolean isRowVisible(ColumnGroupRow<T> row); + + /** + * Should the first row be visible + * + * @return <code>true</code> if the first row should be visible + */ + public abstract boolean firstRowIsVisible(); + + /** + * The renderer that renders the cell + * + * @param column + * The column for which the cell should be rendered + * + * @return renderer used for rendering + */ + public abstract Renderer<String> getRenderer(GridColumn<?, T> column); + + /** + * The renderer that renders the cell for column groups + * + * @param group + * The group that should be rendered + * @return renderer used for rendering + */ + public abstract Renderer<String> getGroupRenderer(ColumnGroup<T> group); + + @Override + public void updateCells(Row row, Iterable<Cell> cellsToUpdate) { + + int rowIndex; + if (inverted) { + rowIndex = rows.getRowCount() - row.getRow() - 1; + } else { + rowIndex = row.getRow(); + } + + if (firstRowIsVisible() && rowIndex == 0) { + // column headers + for (Cell cell : cellsToUpdate) { + GridColumn<?, T> column = getColumnFromVisibleIndex(cell + .getColumn()); + if (column != null) { + getRenderer(column).renderCell(cell, + getColumnValue(column)); + } + } + + } else if (columnGroupRows.size() > 0) { + // Adjust for headers + if (firstRowIsVisible()) { + rowIndex--; + } + + // Adjust for previous invisible header rows + ColumnGroupRow<T> groupRow = null; + for (int i = 0, realIndex = 0; i < columnGroupRows.size(); i++) { + groupRow = columnGroupRows.get(i); + if (isRowVisible(groupRow)) { + if (realIndex == rowIndex) { + rowIndex = realIndex; + break; + } + realIndex++; + } + } + + assert groupRow != null; + + for (Cell cell : cellsToUpdate) { + GridColumn<?, T> column = getColumnFromVisibleIndex(cell + .getColumn()); + ColumnGroup<T> group = getGroupForColumn(groupRow, column); + Element cellElement = cell.getElement(); + + if (group != null) { + getGroupRenderer(group).renderCell(cell, + getGroupValue(group)); + cell.setColSpan(group.getColumns().size()); + } else { + // Cells are reused + cellElement.setInnerHTML(null); + cell.setColSpan(1); + } + } + } + } + } + + /** + * Creates a new instance. + */ + public Grid() { + initWidget(escalator); + + setStylePrimaryName("v-grid"); + + escalator.getHeader().setEscalatorUpdater(createHeaderUpdater()); + escalator.getBody().setEscalatorUpdater(createBodyUpdater()); + escalator.getFooter().setEscalatorUpdater(createFooterUpdater()); + + refreshHeader(); + refreshFooter(); + + escalator + .addRowVisibilityChangeHandler(new RowVisibilityChangeHandler() { + @Override + public void onRowVisibilityChange( + RowVisibilityChangeEvent event) { + if (dataSource != null) { + dataSource.ensureAvailability( + event.getFirstVisibleRow(), + event.getVisibleRowCount()); + } + } + }); + + } + + @Override + public void setStylePrimaryName(String style) { + super.setStylePrimaryName(style); + escalator.setStylePrimaryName(style); + + } + + /** + * Creates the header updater that updates the escalator header rows from + * the column and column group rows. + * + * @return the updater that updates the data in the escalator. + */ + private EscalatorUpdater createHeaderUpdater() { + return new HeaderFooterEscalatorUpdater(escalator.getHeader(), true) { + + @Override + public boolean isRowVisible(ColumnGroupRow<T> row) { + return row.isHeaderVisible(); + } + + @Override + public String getGroupValue(ColumnGroup<T> group) { + return group.getHeaderCaption(); + } + + @Override + public String getColumnValue(GridColumn<?, T> column) { + return column.getHeaderCaption(); + } + + @Override + public boolean firstRowIsVisible() { + return isColumnHeadersVisible(); + } + + @Override + public Renderer<String> getRenderer(GridColumn<?, T> column) { + return column.getHeaderRenderer(); + } + + @Override + public Renderer<String> getGroupRenderer(ColumnGroup<T> group) { + return group.getHeaderRenderer(); + } + }; + } + + private EscalatorUpdater createBodyUpdater() { + return new EscalatorUpdater() { + + @Override + public void updateCells(Row row, Iterable<Cell> cellsToUpdate) { + int rowIndex = row.getRow(); + if (dataSource == null) { + setCellsLoading(cellsToUpdate); + return; + } + + T rowData = dataSource.getRow(rowIndex); + if (rowData == null) { + setCellsLoading(cellsToUpdate); + return; + } + + for (Cell cell : cellsToUpdate) { + GridColumn column = getColumnFromVisibleIndex(cell + .getColumn()); + if (column != null) { + Object value = column.getValue(rowData); + column.getRenderer().renderCell(cell, value); + } + } + } + + private void setCellsLoading(Iterable<Cell> cellsToUpdate) { + for (Cell cell : cellsToUpdate) { + cell.getElement().setInnerText("..."); + } + } + }; + } + + /** + * Creates the footer updater that updates the escalator footer rows from + * the column and column group rows. + * + * @return the updater that updates the data in the escalator. + */ + private EscalatorUpdater createFooterUpdater() { + return new HeaderFooterEscalatorUpdater(escalator.getFooter(), false) { + + @Override + public boolean isRowVisible(ColumnGroupRow<T> row) { + return row.isFooterVisible(); + } + + @Override + public String getGroupValue(ColumnGroup<T> group) { + return group.getFooterCaption(); + } + + @Override + public String getColumnValue(GridColumn<?, T> column) { + return column.getFooterCaption(); + } + + @Override + public boolean firstRowIsVisible() { + return isColumnFootersVisible(); + } + + @Override + public Renderer<String> getRenderer(GridColumn<?, T> column) { + return column.getFooterRenderer(); + } + + @Override + public Renderer<String> getGroupRenderer(ColumnGroup<T> group) { + return group.getFooterRenderer(); + } + }; + } + + /** + * Refreshes header or footer rows on demand + * + * @param rows + * The row container + * @param firstRowIsVisible + * is the first row visible + * @param isHeader + * <code>true</code> if we refreshing the header, else assumed + * the footer + */ + private void refreshRowContainer(RowContainer rows, + boolean firstRowIsVisible, boolean isHeader) { + + // Count needed rows + int totalRows = firstRowIsVisible ? 1 : 0; + for (ColumnGroupRow<T> row : columnGroupRows) { + if (isHeader ? row.isHeaderVisible() : row.isFooterVisible()) { + totalRows++; + } + } + + // Add or Remove rows on demand + int rowDiff = totalRows - rows.getRowCount(); + if (rowDiff > 0) { + rows.insertRows(0, rowDiff); + } else if (rowDiff < 0) { + rows.removeRows(0, -rowDiff); + } + + // Refresh all the rows + if (rows.getRowCount() > 0) { + rows.refreshRows(0, rows.getRowCount()); + } + } + + /** + * Refreshes all header rows + */ + void refreshHeader() { + refreshRowContainer(escalator.getHeader(), isColumnHeadersVisible(), + true); + } + + /** + * Refreshes all footer rows + */ + void refreshFooter() { + refreshRowContainer(escalator.getFooter(), isColumnFootersVisible(), + false); + } + + /** + * Adds a column as the last column in the grid. + * + * @param column + * the column to add + */ + public void addColumn(GridColumn<?, T> column) { + ColumnConfiguration conf = escalator.getColumnConfiguration(); + addColumn(column, conf.getColumnCount()); + } + + /** + * Inserts a column into a specific position in the grid. + * + * @param index + * the index where the column should be inserted into + * @param column + * the column to add + */ + public void addColumn(GridColumn<?, T> column, int index) { + + // Register column with grid + columns.add(index, column); + + // Insert column into escalator + if (column.isVisible()) { + int visibleIndex = findVisibleColumnIndex(column); + ColumnConfiguration conf = escalator.getColumnConfiguration(); + conf.insertColumns(visibleIndex, 1); + } + + // Register this grid instance with the column + ((AbstractGridColumn<?, T>) column).setGrid(this); + + if (lastFrozenColumn != null + && ((AbstractGridColumn<?, T>) lastFrozenColumn) + .findIndexOfColumn() < index) { + refreshFrozenColumns(); + } + } + + private int findVisibleColumnIndex(GridColumn<?, T> column) { + int idx = 0; + for (GridColumn<?, T> c : columns) { + if (c == column) { + return idx; + } else if (c.isVisible()) { + idx++; + } + } + return -1; + } + + private GridColumn<?, T> getColumnFromVisibleIndex(int index) { + int idx = -1; + for (GridColumn<?, T> c : columns) { + if (c.isVisible()) { + idx++; + } + if (index == idx) { + return c; + } + } + return null; + } + + /** + * Removes a column from the grid. + * + * @param column + * the column to remove + */ + public void removeColumn(GridColumn<?, T> column) { + + int columnIndex = columns.indexOf(column); + int visibleIndex = findVisibleColumnIndex(column); + columns.remove(columnIndex); + + // de-register column with grid + ((AbstractGridColumn<?, T>) column).setGrid(null); + + if (column.isVisible()) { + ColumnConfiguration conf = escalator.getColumnConfiguration(); + conf.removeColumns(visibleIndex, 1); + } + + if (column.equals(lastFrozenColumn)) { + setLastFrozenColumn(null); + } else { + refreshFrozenColumns(); + } + } + + /** + * Returns the amount of columns in the grid. + * + * @return The number of columns in the grid + */ + public int getColumnCount() { + return columns.size(); + } + + /** + * Returns a list of columns in the grid. + * + * @return A unmodifiable list of the columns in the grid + */ + public List<GridColumn<?, T>> getColumns() { + return Collections.unmodifiableList(new ArrayList<GridColumn<?, T>>( + columns)); + } + + /** + * Returns a column by its index in the grid. + * + * @param index + * the index of the column + * @return The column in the given index + * @throws IllegalArgumentException + * if the column index does not exist in the grid + */ + public GridColumn<?, T> getColumn(int index) + throws IllegalArgumentException { + if (index < 0 || index >= columns.size()) { + throw new IllegalStateException("Column not found."); + } + return columns.get(index); + } + + /** + * Set the column headers visible. + * + * <p> + * A column header is a single cell header on top of each column reserved + * for a specific header for that column. The column header can be set by + * {@link GridColumn#setHeaderCaption(String)} and column headers cannot be + * merged with other column headers. + * </p> + * + * <p> + * All column headers occupy the first header row of the grid. If you do not + * wish to show the column headers in the grid you should hide the row by + * setting visibility of the header row to <code>false</code>. + * </p> + * + * <p> + * If you want to merge the column headers into groups you can use + * {@link ColumnGroupRow}s to group columns together and give them a common + * header. See {@link #addColumnGroupRow()} for details. + * </p> + * + * <p> + * The header row is by default visible. + * </p> + * + * @param visible + * <code>true</code> if header rows should be visible + */ + public void setColumnHeadersVisible(boolean visible) { + if (visible == isColumnHeadersVisible()) { + return; + } + columnHeadersVisible = visible; + refreshHeader(); + } + + /** + * Are the column headers visible + * + * @return <code>true</code> if they are visible + */ + public boolean isColumnHeadersVisible() { + return columnHeadersVisible; + } + + /** + * Set the column footers visible. + * + * <p> + * A column footer is a single cell footer below of each column reserved for + * a specific footer for that column. The column footer can be set by + * {@link GridColumn#setFooterCaption(String)} and column footers cannot be + * merged with other column footers. + * </p> + * + * <p> + * All column footers occupy the first footer row of the grid. If you do not + * wish to show the column footers in the grid you should hide the row by + * setting visibility of the footer row to <code>false</code>. + * </p> + * + * <p> + * If you want to merge the column footers into groups you can use + * {@link ColumnGroupRow}s to group columns together and give them a common + * footer. See {@link #addColumnGroupRow()} for details. + * </p> + * + * <p> + * The footer row is by default hidden. + * </p> + * + * @param visible + * <code>true</code> if the footer row should be visible + */ + public void setColumnFootersVisible(boolean visible) { + if (visible == isColumnFootersVisible()) { + return; + } + this.columnFootersVisible = visible; + refreshFooter(); + } + + /** + * Are the column footers visible + * + * @return <code>true</code> if they are visible + * + */ + public boolean isColumnFootersVisible() { + return columnFootersVisible; + } + + /** + * Adds a new column group row to the grid. + * + * <p> + * Column group rows are rendered in the header and footer of the grid. + * Column group rows are made up of column groups which groups together + * columns for adding a common auxiliary header or footer for the columns. + * </p> + * + * Example usage: + * + * <pre> + * // Add a new column group row to the grid + * ColumnGroupRow row = grid.addColumnGroupRow(); + * + * // Group "Column1" and "Column2" together to form a header in the row + * ColumnGroup column12 = row.addGroup("Column1", "Column2"); + * + * // Set a common header for "Column1" and "Column2" + * column12.setHeader("Column 1&2"); + * + * // Set a common footer for "Column1" and "Column2" + * column12.setFooter("Column 1&2"); + * </pre> + * + * @return a column group row instance you can use to add column groups + */ + public ColumnGroupRow<T> addColumnGroupRow() { + ColumnGroupRow<T> row = new ColumnGroupRow<T>(this); + columnGroupRows.add(row); + refreshHeader(); + refreshFooter(); + return row; + } + + /** + * Adds a new column group row to the grid at a specific index. + * + * @see #addColumnGroupRow() {@link Grid#addColumnGroupRow()} for example + * usage + * + * @param rowIndex + * the index where the column group row should be added + * @return a column group row instance you can use to add column groups + */ + public ColumnGroupRow<T> addColumnGroupRow(int rowIndex) { + ColumnGroupRow<T> row = new ColumnGroupRow<T>(this); + columnGroupRows.add(rowIndex, row); + refreshHeader(); + refreshFooter(); + return row; + } + + /** + * Removes a column group row + * + * @param row + * The row to remove + */ + public void removeColumnGroupRow(ColumnGroupRow<T> row) { + columnGroupRows.remove(row); + refreshHeader(); + refreshFooter(); + } + + /** + * Get the column group rows + * + * @return a unmodifiable list of column group rows + * + */ + public List<ColumnGroupRow<T>> getColumnGroupRows() { + return Collections.unmodifiableList(new ArrayList<ColumnGroupRow<T>>( + columnGroupRows)); + } + + /** + * Returns the column group for a row and column + * + * @param row + * The row of the column + * @param column + * the column to get the group for + * @return A column group for the row and column or <code>null</code> if not + * found. + */ + private ColumnGroup<T> getGroupForColumn(ColumnGroupRow<T> row, + GridColumn<?, T> column) { + for (ColumnGroup<T> group : row.getGroups()) { + List<GridColumn<?, T>> columns = group.getColumns(); + if (columns.contains(column)) { + return group; + } + } + return null; + } + + @Override + public void setHeight(String height) { + escalator.setHeight(height); + } + + @Override + public void setWidth(String width) { + escalator.setWidth(width); + } + + /** + * Sets the data source used by this grid. + * + * @param dataSource + * the data source to use, not null + * @throws IllegalArgumentException + * if <code>dataSource</code> is <code>null</code> + */ + public void setDataSource(DataSource<T> dataSource) + throws IllegalArgumentException { + if (dataSource == null) { + throw new IllegalArgumentException("dataSource can't be null."); + } + + if (this.dataSource != null) { + this.dataSource.setDataChangeHandler(null); + } + + this.dataSource = dataSource; + dataSource.setDataChangeHandler(new DataChangeHandler() { + @Override + public void dataUpdated(int firstIndex, int numberOfItems) { + escalator.getBody().refreshRows(firstIndex, numberOfItems); + } + + @Override + public void dataRemoved(int firstIndex, int numberOfItems) { + escalator.getBody().removeRows(firstIndex, numberOfItems); + } + + @Override + public void dataAdded(int firstIndex, int numberOfItems) { + escalator.getBody().insertRows(firstIndex, numberOfItems); + } + }); + + int previousRowCount = escalator.getBody().getRowCount(); + if (previousRowCount != 0) { + escalator.getBody().removeRows(0, previousRowCount); + } + + int estimatedSize = dataSource.getEstimatedSize(); + if (estimatedSize > 0) { + escalator.getBody().insertRows(0, estimatedSize); + } + } + + /** + * Sets the rightmost frozen column in the grid. + * <p> + * All columns up to and including the given column will be frozen in place + * when the grid is scrolled sideways. + * + * @param lastFrozenColumn + * the rightmost column to freeze, or <code>null</code> to not + * have any columns frozen + * @throws IllegalArgumentException + * if {@code lastFrozenColumn} is not a column from this grid + */ + public void setLastFrozenColumn(GridColumn<?, T> lastFrozenColumn) { + this.lastFrozenColumn = lastFrozenColumn; + refreshFrozenColumns(); + } + + private void refreshFrozenColumns() { + final int frozenCount; + if (lastFrozenColumn != null) { + frozenCount = columns.indexOf(lastFrozenColumn) + 1; + if (frozenCount == 0) { + throw new IllegalArgumentException( + "The given column isn't attached to this grid"); + } + } else { + frozenCount = 0; + } + + escalator.getColumnConfiguration().setFrozenColumnCount(frozenCount); + } + + /** + * Gets the rightmost frozen column in the grid. + * <p> + * <em>Note:</em> Most usually, this method returns the very value set with + * {@link #setLastFrozenColumn(GridColumn)}. This value, however, can be + * reset to <code>null</code> if the column is removed from this grid. + * + * @return the rightmost frozen column in the grid, or <code>null</code> if + * no columns are frozen. + */ + public GridColumn<?, T> getLastFrozenColumn() { + return lastFrozenColumn; + } + + public HandlerRegistration addRowVisibilityChangeHandler( + RowVisibilityChangeHandler handler) { + /* + * Reusing Escalator's RowVisibilityChangeHandler, since a scroll + * concept is too abstract. e.g. the event needs to be re-sent when the + * widget is resized. + */ + return escalator.addRowVisibilityChangeHandler(handler); + } + + /** + * Scrolls to a certain row, using {@link ScrollDestination#ANY}. + * + * @param rowIndex + * zero-based index of the row to scroll to. + * @throws IllegalArgumentException + * if rowIndex is below zero, or above the maximum value + * supported by the data source. + */ + public void scrollToRow(int rowIndex) throws IllegalArgumentException { + scrollToRow(rowIndex, ScrollDestination.ANY, + GridConstants.DEFAULT_PADDING); + } + + /** + * Scrolls to a certain row, using user-specified scroll destination. + * + * @param rowIndex + * zero-based index of the row to scroll to. + * @param destination + * desired destination placement of scrolled-to-row. See + * {@link ScrollDestination} for more information. + * @throws IllegalArgumentException + * if rowIndex is below zero, or above the maximum value + * supported by the data source. + */ + public void scrollToRow(int rowIndex, ScrollDestination destination) + throws IllegalArgumentException { + scrollToRow(rowIndex, destination, + destination == ScrollDestination.MIDDLE ? 0 + : GridConstants.DEFAULT_PADDING); + } + + /** + * Scrolls to a certain row using only user-specified parameters. + * + * @param rowIndex + * zero-based index of the row to scroll to. + * @param destination + * desired destination placement of scrolled-to-row. See + * {@link ScrollDestination} for more information. + * @param paddingPx + * number of pixels to overscroll. Behavior depends on + * destination. + * @throws IllegalArgumentException + * if {@code destination} is {@link ScrollDestination#MIDDLE} + * and padding is nonzero, because having a padding on a + * centered row is undefined behavior, or if rowIndex is below + * zero or above the row count of the data source. + */ + private void scrollToRow(int rowIndex, ScrollDestination destination, + int paddingPx) throws IllegalArgumentException { + int maxsize = escalator.getBody().getRowCount() - 1; + + if (rowIndex < 0) { + throw new IllegalArgumentException("Row index (" + rowIndex + + ") is below zero!"); + } + + if (rowIndex > maxsize) { + throw new IllegalArgumentException("Row index (" + rowIndex + + ") is above maximum (" + maxsize + ")!"); + } + + escalator.scrollToRow(rowIndex, destination, paddingPx); + } + + /** + * Scrolls to the beginning of the very first row. + */ + public void scrollToStart() { + scrollToRow(0, ScrollDestination.START); + } + + /** + * Scrolls to the end of the very last row. + */ + public void scrollToEnd() { + scrollToRow(escalator.getBody().getRowCount() - 1, + ScrollDestination.END); + } + + private static final Logger getLogger() { + return Logger.getLogger(Grid.class.getName()); + } + +} diff --git a/client/src/com/vaadin/client/ui/grid/GridColumn.java b/client/src/com/vaadin/client/ui/grid/GridColumn.java new file mode 100644 index 0000000000..afd80a5f5a --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/GridColumn.java @@ -0,0 +1,54 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +/** + * Represents a column in the {@link Grid}. + * + * @param <C> + * The column type + * + * @param <T> + * The row type + * + * @since 7.2 + * @author Vaadin Ltd + */ +public abstract class GridColumn<C, T> extends Grid.AbstractGridColumn<C, T> { + + /* + * This class is a convenience class so you do not have to reference + * Grid.AbstractGridColumn in your production code. The real implementation + * should be in the abstract class. + */ + + /** + * Constructs a new column. + */ + public GridColumn() { + super(); + } + + /** + * Constructs a new column with a custom renderer. + * + * @param renderer + * The renderer to use for rendering the cells + */ + public GridColumn(Renderer<C> renderer) { + super(renderer); + } +} diff --git a/client/src/com/vaadin/client/ui/grid/GridConnector.java b/client/src/com/vaadin/client/ui/grid/GridConnector.java new file mode 100644 index 0000000000..5e0664667d --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/GridConnector.java @@ -0,0 +1,277 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.vaadin.client.communication.StateChangeEvent; +import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.grid.ColumnGroupRowState; +import com.vaadin.shared.ui.grid.ColumnGroupState; +import com.vaadin.shared.ui.grid.GridClientRpc; +import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridServerRpc; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.ScrollDestination; + +/** + * Connects the client side {@link Grid} widget with the server side + * {@link com.vaadin.ui.components.grid.Grid} component. + * + * @since 7.2 + * @author Vaadin Ltd + */ +@Connect(com.vaadin.ui.components.grid.Grid.class) +public class GridConnector extends AbstractComponentConnector { + + /** + * Custom implementation of the custom grid column using a String[]Â to + * represent the cell value and String as a column type. + */ + private class CustomGridColumn extends GridColumn<String, String[]> { + + private final int columnIndex; + + public CustomGridColumn(int columnIndex) { + this.columnIndex = columnIndex; + } + + @Override + public String getValue(String[] obj) { + return obj[columnIndex]; + } + } + + /** + * Maps a generated column id to a grid column instance + */ + private Map<String, CustomGridColumn> columnIdToColumn = new HashMap<String, CustomGridColumn>(); + + @Override + protected Grid<String[]> createWidget() { + // FIXME Shouldn't be needed after #12873 has been fixed. + return new Grid<String[]>(); + } + + @Override + @SuppressWarnings("unchecked") + public Grid<String[]> getWidget() { + return (Grid<String[]>) super.getWidget(); + } + + @Override + public GridState getState() { + return (GridState) super.getState(); + } + + @Override + protected void init() { + super.init(); + getWidget().addRowVisibilityChangeHandler( + new RowVisibilityChangeHandler() { + @Override + public void onRowVisibilityChange( + RowVisibilityChangeEvent event) { + getRpcProxy(GridServerRpc.class).setVisibleRows( + event.getFirstVisibleRow(), + event.getVisibleRowCount()); + } + }); + + registerRpc(GridClientRpc.class, new GridClientRpc() { + @Override + public void scrollToStart() { + getWidget().scrollToStart(); + } + + @Override + public void scrollToEnd() { + getWidget().scrollToEnd(); + } + + @Override + public void scrollToRow(int row, ScrollDestination destination) { + getWidget().scrollToRow(row, destination); + } + }); + } + + @Override + public void onStateChanged(StateChangeEvent stateChangeEvent) { + super.onStateChanged(stateChangeEvent); + + // Column updates + if (stateChangeEvent.hasPropertyChanged("columns")) { + + int totalColumns = getState().columns.size(); + + // Remove old columns + purgeRemovedColumns(); + + int currentColumns = getWidget().getColumnCount(); + + // Add new columns + for (int columnIndex = currentColumns; columnIndex < totalColumns; columnIndex++) { + addColumnFromStateChangeEvent(columnIndex); + } + + // Update old columns + for (int columnIndex = 0; columnIndex < currentColumns; columnIndex++) { + // FIXME Currently updating all column header / footers when a + // change in made in one column. When the framework supports + // quering a specific item in a list then it should do so here. + updateColumnFromStateChangeEvent(columnIndex); + } + } + + // Header + if (stateChangeEvent.hasPropertyChanged("columnHeadersVisible")) { + getWidget() + .setColumnHeadersVisible(getState().columnHeadersVisible); + } + + // Footer + if (stateChangeEvent.hasPropertyChanged("columnFootersVisible")) { + getWidget() + .setColumnFootersVisible(getState().columnFootersVisible); + } + + // Column row groups + if (stateChangeEvent.hasPropertyChanged("columnGroupRows")) { + updateColumnGroupsFromStateChangeEvent(); + } + + if (stateChangeEvent.hasPropertyChanged("lastFrozenColumnId")) { + String frozenColId = getState().lastFrozenColumnId; + if (frozenColId != null) { + CustomGridColumn column = columnIdToColumn.get(frozenColId); + assert column != null : "Column to be frozen could not be found (id:" + + frozenColId + ")"; + getWidget().setLastFrozenColumn(column); + } else { + getWidget().setLastFrozenColumn(null); + } + } + } + + /** + * Updates a column from a state change event. + * + * @param columnIndex + * The index of the column to update + */ + private void updateColumnFromStateChangeEvent(int columnIndex) { + GridColumn<?, String[]> column = getWidget().getColumn(columnIndex); + GridColumnState columnState = getState().columns.get(columnIndex); + updateColumnFromState(column, columnState); + } + + /** + * Adds a new column to the grid widget from a state change event + * + * @param columnIndex + * The index of the column, according to how it + */ + private void addColumnFromStateChangeEvent(int columnIndex) { + GridColumnState state = getState().columns.get(columnIndex); + CustomGridColumn column = new CustomGridColumn(columnIndex); + updateColumnFromState(column, state); + + columnIdToColumn.put(state.id, column); + + getWidget().addColumn(column, columnIndex); + } + + /** + * Updates the column values from a state + * + * @param column + * The column to update + * @param state + * The state to get the data from + */ + private static void updateColumnFromState(GridColumn<?, String[]> column, + GridColumnState state) { + column.setVisible(state.visible); + column.setHeaderCaption(state.header); + column.setFooterCaption(state.footer); + column.setWidth(state.width); + } + + /** + * Removes any orphan columns that has been removed from the state from the + * grid + */ + private void purgeRemovedColumns() { + + // Get columns still registered in the state + Set<String> columnsInState = new HashSet<String>(); + for (GridColumnState columnState : getState().columns) { + columnsInState.add(columnState.id); + } + + // Remove column no longer in state + Iterator<String> columnIdIterator = columnIdToColumn.keySet() + .iterator(); + while (columnIdIterator.hasNext()) { + String id = columnIdIterator.next(); + if (!columnsInState.contains(id)) { + CustomGridColumn column = columnIdToColumn.get(id); + columnIdIterator.remove(); + getWidget().removeColumn(column); + } + } + } + + /** + * Updates the column groups from a state change + */ + private void updateColumnGroupsFromStateChangeEvent() { + + // FIXME When something changes the header/footer rows will be + // re-created. At some point we should optimize this so partial updates + // can be made on the header/footer. + for (ColumnGroupRow<String[]> row : getWidget().getColumnGroupRows()) { + getWidget().removeColumnGroupRow(row); + } + + for (ColumnGroupRowState rowState : getState().columnGroupRows) { + ColumnGroupRow<String[]> row = getWidget().addColumnGroupRow(); + row.setFooterVisible(rowState.footerVisible); + row.setHeaderVisible(rowState.headerVisible); + + for (ColumnGroupState groupState : rowState.groups) { + List<GridColumn<String, String[]>> columns = new ArrayList<GridColumn<String, String[]>>(); + for (String columnId : groupState.columns) { + CustomGridColumn column = columnIdToColumn.get(columnId); + columns.add(column); + } + ColumnGroup<String[]> group = row.addGroup(columns + .toArray(new GridColumn[columns.size()])); + group.setFooterCaption(groupState.footer); + group.setHeaderCaption(groupState.header); + } + } + } +} diff --git a/client/src/com/vaadin/client/ui/grid/PositionFunction.java b/client/src/com/vaadin/client/ui/grid/PositionFunction.java new file mode 100644 index 0000000000..e41e533996 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/PositionFunction.java @@ -0,0 +1,118 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.Unit; + +/** + * A functional interface that can be used for positioning elements in the DOM. + * + * @since 7.2 + * @author Vaadin Ltd + */ +interface PositionFunction { + /** + * A position function using "transform: translate3d(x,y,z)" to position + * elements in the DOM. + */ + public static class Translate3DPosition implements PositionFunction { + @Override + public void set(Element e, double x, double y) { + e.getStyle().setProperty("transform", + "translate3d(" + x + "px, " + y + "px, 0)"); + } + + @Override + public void reset(Element e) { + e.getStyle().clearProperty("transform"); + } + } + + /** + * A position function using "transform: translate(x,y)" to position + * elements in the DOM. + */ + public static class TranslatePosition implements PositionFunction { + @Override + public void set(Element e, double x, double y) { + e.getStyle().setProperty("transform", + "translate(" + x + "px," + y + "px)"); + } + + @Override + public void reset(Element e) { + e.getStyle().clearProperty("transform"); + } + } + + /** + * A position function using "-webkit-transform: translate3d(x,y,z)" to + * position elements in the DOM. + */ + public static class WebkitTranslate3DPosition implements PositionFunction { + @Override + public void set(Element e, double x, double y) { + e.getStyle().setProperty("webkitTransform", + "translate3d(" + x + "px," + y + "px,0)"); + } + + @Override + public void reset(Element e) { + e.getStyle().clearProperty("webkitTransform"); + } + } + + /** + * A position function using "left: x" and "top: y" to position elements in + * the DOM. + */ + public static class AbsolutePosition implements PositionFunction { + @Override + public void set(Element e, double x, double y) { + e.getStyle().setLeft(x, Unit.PX); + e.getStyle().setTop(y, Unit.PX); + } + + @Override + public void reset(Element e) { + e.getStyle().clearLeft(); + e.getStyle().clearTop(); + } + } + + /** + * Position an element in an (x,y) coordinate system in the DOM. + * + * @param e + * the element to position. Never <code>null</code>. + * @param x + * the x coordinate, in pixels + * @param y + * the y coordinate, in pixels + */ + void set(Element e, double x, double y); + + /** + * Resets any previously applied positioning, clearing the used style + * attributes. + * + * @param e + * the element for which to reset the positioning + */ + void reset(Element e); +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/grid/Renderer.java b/client/src/com/vaadin/client/ui/grid/Renderer.java new file mode 100644 index 0000000000..3312ec87e4 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/Renderer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +/** + * Renderer for rending a value <T> into cell. + * <p> + * You can add a renderer to any column by overring the + * {@link GridColumn#getRenderer()} method and returning your own renderer. You + * can retrieve the cell element using {@link Cell#getElement()}. + * + * @param <T> + * The column type + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface Renderer<T> { + + /** + * Called whenever the {@link Grid} updates a cell + * + * @param cell + * The cell that gets updated + * + * @param data + * The column data object + */ + public void renderCell(Cell cell, T data); +} diff --git a/client/src/com/vaadin/client/ui/grid/Row.java b/client/src/com/vaadin/client/ui/grid/Row.java new file mode 100644 index 0000000000..209da58fec --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/Row.java @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +import com.google.gwt.dom.client.Element; + +/** + * A representation of a row in an {@link Escalator}. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface Row { + /** + * Gets the escalator containing the row. + * + * @return the escalator containing the row + */ + public Escalator getEscalator(); + + /** + * Gets the row index. + * + * @return the row index + */ + public int getRow(); + + /** + * Gets the root element for this row. + * <p> + * The {@link EscalatorUpdater} may update the class names of the element + * and add inline styles, but may not modify the contained DOM structure. + * <p> + * If you wish to modify the cells within this row element, access them via + * the <code>List<{@link Cell}></code> objects passed in to + * {@code EscalatorUpdater.updateCells(Row, List)} + * + * @return the root element of the row + */ + public Element getElement(); +}
\ No newline at end of file diff --git a/client/src/com/vaadin/client/ui/grid/RowContainer.java b/client/src/com/vaadin/client/ui/grid/RowContainer.java new file mode 100644 index 0000000000..6ef1c913b3 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/RowContainer.java @@ -0,0 +1,126 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +/** + * A representation of the rows in each of the sections (header, body and + * footer) in an {@link Escalator}. + * + * @since 7.2 + * @author Vaadin Ltd + * @see Escalator#getHeader() + * @see Escalator#getBody() + * @see Escalator#getFooter() + */ +public interface RowContainer { + /** + * Returns the current {@link EscalatorUpdater} used to render cells. + * + * @return the current escalator updater + */ + public EscalatorUpdater getEscalatorUpdater(); + + /** + * Sets the {@link EscalatorUpdater} to use when displaying data in the + * escalator. + * + * @param escalatorUpdater + * the escalator updater to use to render cells. May not be + * <code>null</code> + * @throws IllegalArgumentException + * if {@code cellRenderer} is <code>null</code> + * @see EscalatorUpdater#NULL + */ + public void setEscalatorUpdater(EscalatorUpdater escalatorUpdater) + throws IllegalArgumentException; + + /** + * Removes rows at a certain index in the current row container. + * + * @param index + * the index of the first row to be removed + * @param numberOfRows + * the number of rows to remove, starting from the index + * @throws IndexOutOfBoundsException + * if any integer number in the range + * <code>[index..(index+numberOfRows)]</code> is not an existing + * row index + * @throws IllegalArgumentException + * if {@code numberOfRows} is less than 1. + */ + public void removeRows(int index, int numberOfRows) + throws IndexOutOfBoundsException, IllegalArgumentException; + + /** + * Adds rows at a certain index in this row container. + * <p> + * The new rows will be inserted between the row at the index, and the row + * before (an index of 0 means that the rows are inserted at the beginning). + * Therefore, the rows currently at the index and afterwards will be moved + * downwards. + * <p> + * The contents of the inserted rows will subsequently be queried from the + * escalator updater. + * <p> + * <em>Note:</em> Only the contents of the inserted rows will be rendered. + * If inserting new rows affects the contents of existing rows, + * {@link #refreshRows(int, int)} needs to be called for those rows + * separately. + * + * @param index + * the index of the row before which new rows are inserted, or + * {@link #getRowCount()} to add rows at the end + * @param numberOfRows + * the number of rows to insert after the <code>index</code> + * @see #setEscalatorUpdater(EscalatorUpdater) + * @throws IndexOutOfBoundsException + * if <code>index</code> is not an integer in the range + * <code>[0..{@link #getRowCount()}]</code> + * @throws IllegalArgumentException + * if {@code numberOfRows} is less than 1. + */ + public void insertRows(int index, int numberOfRows) + throws IndexOutOfBoundsException, IllegalArgumentException; + + /** + * Refreshes a range of rows in the current row container. + * <p> + * The data for the refreshed rows are queried from the current cell + * renderer. + * + * @param index + * the index of the first row that will be updated + * @param numberOfRows + * the number of rows to update, starting from the index + * @see #setEscalatorUpdater(EscalatorUpdater) + * @throws IndexOutOfBoundsException + * if any integer number in the range + * <code>[index..(index+numberOfColumns)]</code> is not an + * existing column index. + * @throws IllegalArgumentException + * if {@code numberOfRows} is less than 1. + */ + public void refreshRows(int index, int numberOfRows) + throws IndexOutOfBoundsException, IllegalArgumentException; + + /** + * Gets the number of rows in the current row container. + * + * @return the number of rows in the current row container + */ + public int getRowCount(); +} diff --git a/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java new file mode 100644 index 0000000000..0e9652e215 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java @@ -0,0 +1,90 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +import com.google.gwt.event.shared.GwtEvent; + +/** + * Event fired when the range of visible rows changes e.g. because of scrolling. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class RowVisibilityChangeEvent extends + GwtEvent<RowVisibilityChangeHandler> { + /** + * The type of this event. + */ + public static final Type<RowVisibilityChangeHandler> TYPE = new Type<RowVisibilityChangeHandler>(); + + private final int firstVisibleRow; + private final int visibleRowCount; + + /** + * Creates a new row visibility change event + * + * @param firstVisibleRow + * the index of the first visible row + * @param visibleRowCount + * the number of visible rows + */ + public RowVisibilityChangeEvent(int firstVisibleRow, int visibleRowCount) { + this.firstVisibleRow = firstVisibleRow; + this.visibleRowCount = visibleRowCount; + } + + /** + * Gets the index of the first row that is at least partially visible. + * + * @return the index of the first visible row + */ + public int getFirstVisibleRow() { + return firstVisibleRow; + } + + /** + * Gets the number of at least partially visible rows. + * + * @return the number of visible rows + */ + public int getVisibleRowCount() { + return visibleRowCount; + } + + /* + * (non-Javadoc) + * + * @see com.google.gwt.event.shared.GwtEvent#getAssociatedType() + */ + @Override + public Type<RowVisibilityChangeHandler> getAssociatedType() { + return TYPE; + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.event.shared.GwtEvent#dispatch(com.google.gwt.event.shared + * .EventHandler) + */ + @Override + protected void dispatch(RowVisibilityChangeHandler handler) { + handler.onRowVisibilityChange(this); + } + +} diff --git a/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java new file mode 100644 index 0000000000..dd24521499 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +import com.google.gwt.event.shared.EventHandler; + +/** + * Event handler that gets notified when the range of visible rows changes e.g. + * because of scrolling. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface RowVisibilityChangeHandler extends EventHandler { + + /** + * Called when the range of visible rows changes e.g. because of scrolling. + * + * @param event + * the row visibility change event describing the change + */ + void onRowVisibilityChange(RowVisibilityChangeEvent event); + +} diff --git a/client/src/com/vaadin/client/ui/grid/ScrollbarBundle.java b/client/src/com/vaadin/client/ui/grid/ScrollbarBundle.java new file mode 100644 index 0000000000..b9267178c1 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/ScrollbarBundle.java @@ -0,0 +1,403 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.Overflow; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.user.client.DOM; + +/** + * An element-like bundle representing a configurable and visual scrollbar in + * one axis. + * + * @since 7.2 + * @author Vaadin Ltd + * @see VerticalScrollbarBundle + * @see HorizontalScrollbarBundle + */ +abstract class ScrollbarBundle { + + /** + * The pixel size for OSX's invisible scrollbars. + * <p> + * Touch devices don't show a scrollbar at all, so the scrollbar size is + * irrelevant in their case. There doesn't seem to be any other popular + * platforms that has scrollbars similar to OSX. Thus, this behavior is + * tailored for OSX only, until additional platforms start behaving this + * way. + */ + private static final int OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX = 13; + + /** + * A representation of a single vertical scrollbar. + * + * @see VerticalScrollbarBundle#getElement() + */ + final static class VerticalScrollbarBundle extends ScrollbarBundle { + + @Override + public void setStylePrimaryName(String primaryStyleName) { + super.setStylePrimaryName(primaryStyleName); + root.addClassName(primaryStyleName + "-scroller-vertical"); + } + + @Override + protected void internalSetScrollPos(int px) { + root.setScrollTop(px); + } + + @Override + protected int internalGetScrollPos() { + return root.getScrollTop(); + } + + @Override + protected void internalSetScrollSize(int px) { + scrollSizeElement.getStyle().setHeight(px, Unit.PX); + } + + @Override + public int getScrollSize() { + return scrollSizeElement.getOffsetHeight(); + } + + @Override + protected void internalSetOffsetSize(int px) { + root.getStyle().setHeight(px, Unit.PX); + } + + @Override + public int getOffsetSize() { + return root.getOffsetHeight(); + } + + @Override + protected void internalSetScrollbarThickness(int px) { + root.getStyle().setWidth(px, Unit.PX); + scrollSizeElement.getStyle().setWidth(px, Unit.PX); + } + + @Override + protected int internalGetScrollbarThickness() { + return root.getOffsetWidth(); + } + + @Override + protected void forceScrollbar(boolean enable) { + if (enable) { + root.getStyle().setOverflowY(Overflow.SCROLL); + } else { + root.getStyle().clearOverflowY(); + } + } + } + + /** + * A representation of a single horizontal scrollbar. + * + * @see HorizontalScrollbarBundle#getElement() + */ + final static class HorizontalScrollbarBundle extends ScrollbarBundle { + + @Override + public void setStylePrimaryName(String primaryStyleName) { + super.setStylePrimaryName(primaryStyleName); + root.addClassName(primaryStyleName + "-scroller-horizontal"); + } + + @Override + protected void internalSetScrollPos(int px) { + root.setScrollLeft(px); + } + + @Override + protected int internalGetScrollPos() { + return root.getScrollLeft(); + } + + @Override + protected void internalSetScrollSize(int px) { + scrollSizeElement.getStyle().setWidth(px, Unit.PX); + } + + @Override + public int getScrollSize() { + return scrollSizeElement.getOffsetWidth(); + } + + @Override + protected void internalSetOffsetSize(int px) { + root.getStyle().setWidth(px, Unit.PX); + } + + @Override + public int getOffsetSize() { + return root.getOffsetWidth(); + } + + @Override + protected void internalSetScrollbarThickness(int px) { + root.getStyle().setHeight(px, Unit.PX); + scrollSizeElement.getStyle().setHeight(px, Unit.PX); + } + + @Override + protected int internalGetScrollbarThickness() { + return root.getOffsetHeight(); + } + + @Override + protected void forceScrollbar(boolean enable) { + if (enable) { + root.getStyle().setOverflowX(Overflow.SCROLL); + } else { + root.getStyle().clearOverflowX(); + } + } + } + + protected final Element root = DOM.createDiv(); + protected final Element scrollSizeElement = DOM.createDiv(); + protected boolean isInvisibleScrollbar = false; + + private int scrollPos = 0; + private int maxScrollPos = 0; + + private ScrollbarBundle() { + root.appendChild(scrollSizeElement); + } + + /** + * Sets the primary style name + * + * @param primaryStyleName + * The primary style name to use + */ + public void setStylePrimaryName(String primaryStyleName) { + root.setClassName(primaryStyleName + "-scroller"); + } + + /** + * Gets the root element of this scrollbar-composition. + * + * @return the root element + */ + public final Element getElement() { + return root; + } + + /** + * Modifies the scroll position of this scrollbar by a number of pixels + * + * @param delta + * the delta in pixels to change the scroll position by + */ + public final void setScrollPosByDelta(int delta) { + if (delta != 0) { + setScrollPos(getScrollPos() + delta); + } + } + + /** + * Modifies {@link #root root's} dimensions in the axis the scrollbar is + * representing. + * + * @param px + * the new size of {@link #root} in the dimension this scrollbar + * is representing + */ + protected abstract void internalSetOffsetSize(int px); + + /** + * Sets the length of the scrollbar. + * + * @param px + * the length of the scrollbar in pixels + */ + public final void setOffsetSize(int px) { + internalSetOffsetSize(px); + forceScrollbar(showsScrollHandle()); + recalculateMaxScrollPos(); + } + + /** + * Force the scrollbar to be visible with CSS. In practice, this means to + * set either <code>overflow-x</code> or <code>overflow-y</code> to " + * <code>scroll</code>" in the scrollbar's direction. + * <p> + * This is an IE8 workaround, since it doesn't always show scrollbars with + * <code>overflow: auto</code> enabled. + */ + protected abstract void forceScrollbar(boolean enable); + + /** + * Gets the length of the scrollbar + * + * @return the length of the scrollbar in pixels + */ + public abstract int getOffsetSize(); + + /** + * Sets the scroll position of the scrollbar in the axis the scrollbar is + * representing. + * + * @param px + * the new scroll position in pixels + */ + public final void setScrollPos(int px) { + int oldScrollPos = scrollPos; + scrollPos = Math.max(0, Math.min(maxScrollPos, px)); + + if (oldScrollPos != scrollPos) { + internalSetScrollPos(px); + } + } + + protected abstract void internalSetScrollPos(int px); + + /** + * Gets the scroll position of the scrollbar in the axis the scrollbar is + * representing. + * + * @return the new scroll position in pixels + */ + public final int getScrollPos() { + assert internalGetScrollPos() == scrollPos : "calculated scroll position (" + + scrollPos + + ") did not match the DOM element scroll position (" + + internalGetScrollPos() + ")"; + return scrollPos; + } + + protected abstract int internalGetScrollPos(); + + /** + * Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in + * such a way that the scrollbar is able to scroll a certain number of + * pixels in the axis it is representing. + * + * @param px + * the new size of {@link #scrollSizeElement} in the dimension + * this scrollbar is representing + */ + protected abstract void internalSetScrollSize(int px); + + /** + * Sets the amount of pixels the scrollbar needs to be able to scroll + * through. + * + * @param px + * the number of pixels the scrollbar should be able to scroll + * through + */ + public final void setScrollSize(int px) { + internalSetScrollSize(px); + forceScrollbar(showsScrollHandle()); + recalculateMaxScrollPos(); + } + + /** + * Gets the amount of pixels the scrollbar needs to be able to scroll + * through. + * + * @return the number of pixels the scrollbar should be able to scroll + * through + */ + public abstract int getScrollSize(); + + /** + * Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in the + * opposite axis to what the scrollbar is representing. + * + * @param px + * the dimension that {@link #scrollSizeElement} should take in + * the opposite axis to what the scrollbar is representing + */ + protected abstract void internalSetScrollbarThickness(int px); + + /** + * Sets the scrollbar's thickness. + * <p> + * If the thickness is set to 0, the scrollbar will be treated as an + * "invisible" scrollbar. This means, the DOM structure will be given a + * non-zero size, but {@link #getScrollbarThickness()} will still return the + * value 0. + * + * @param px + * the scrollbar's thickness in pixels + */ + public final void setScrollbarThickness(int px) { + isInvisibleScrollbar = (px == 0); + internalSetScrollbarThickness(px != 0 ? px + : OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX); + } + + /** + * Gets the scrollbar's thickness as defined in the DOM. + * + * @return the scrollbar's thickness as defined in the DOM, in pixels + */ + protected abstract int internalGetScrollbarThickness(); + + /** + * Gets the scrollbar's thickness. + * <p> + * This value will differ from the value in the DOM, if the thickness was + * set to 0 with {@link #setScrollbarThickness(int)}, as the scrollbar is + * then treated as "invisible." + * + * @return the scrollbar's thickness in pixels + */ + public final int getScrollbarThickness() { + if (!isInvisibleScrollbar) { + return internalGetScrollbarThickness(); + } else { + return 0; + } + } + + /** + * Checks whether the scrollbar's handle is visible. + * <p> + * In other words, this method checks whether the contents is larger than + * can visually fit in the element. + * + * @return <code>true</code> iff the scrollbar's handle is visible + */ + public boolean showsScrollHandle() { + return getOffsetSize() < getScrollSize(); + } + + public void recalculateMaxScrollPos() { + int scrollSize = getScrollSize(); + int offsetSize = getOffsetSize(); + maxScrollPos = Math.max(0, scrollSize - offsetSize); + + // make sure that the correct max scroll position is maintained. + setScrollPos(scrollPos); + } + + /** + * This is a method that JSNI can call to synchronize the object state from + * the DOM. + */ + @SuppressWarnings("unused") + private final void updateScrollPosFromDom() { + scrollPos = internalGetScrollPos(); + } +} diff --git a/client/src/com/vaadin/client/ui/grid/datasources/ListDataSource.java b/client/src/com/vaadin/client/ui/grid/datasources/ListDataSource.java new file mode 100644 index 0000000000..0a3edbd349 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/datasources/ListDataSource.java @@ -0,0 +1,357 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid.datasources; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +import com.vaadin.client.data.DataChangeHandler; +import com.vaadin.client.data.DataSource; + +/** + * A simple list based on an in-memory data source for simply adding a list of + * row pojos to the grid. Based on a wrapped list instance which supports adding + * and removing of items. + * + * <p> + * Usage: + * + * <pre> + * ListDataSource<Integer> ds = new ListDataSource<Integer>(1, 2, 3, 4); + * + * // Add item to the data source + * ds.asList().add(5); + * + * // Remove item from the data source + * ds.asList().remove(3); + * + * // Add multiple items + * ds.asList().addAll(Arrays.asList(5, 6, 7)); + * </pre> + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class ListDataSource<T> implements DataSource<T> { + + /** + * Wraps the datasource list and notifies the change handler of changing to + * the list + */ + private class ListWrapper implements List<T> { + + @Override + public int size() { + return ds.size(); + } + + @Override + public boolean isEmpty() { + return ds.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return contains(o); + } + + @Override + public Iterator<T> iterator() { + return new ListWrapperIterator(ds.iterator()); + } + + @Override + public Object[] toArray() { + return ds.toArray(); + } + + @Override + public <T> T[] toArray(T[] a) { + return toArray(a); + } + + @Override + public boolean add(T e) { + if (ds.add(e)) { + if (changeHandler != null) { + changeHandler.dataAdded(ds.size() - 1, 1); + } + return true; + } + return false; + } + + @Override + public boolean remove(Object o) { + int index = ds.indexOf(o); + if (ds.remove(o)) { + if (changeHandler != null) { + changeHandler.dataRemoved(index, 1); + } + return true; + } + return false; + } + + @Override + public boolean containsAll(Collection<?> c) { + return ds.containsAll(c); + } + + @Override + public boolean addAll(Collection<? extends T> c) { + int idx = ds.size(); + if (ds.addAll(c)) { + if (changeHandler != null) { + changeHandler.dataAdded(idx, c.size()); + } + return true; + } + return false; + } + + @Override + public boolean addAll(int index, Collection<? extends T> c) { + if (ds.addAll(index, c)) { + if (changeHandler != null) { + changeHandler.dataAdded(index, c.size()); + } + return true; + } + return false; + } + + @Override + public boolean removeAll(Collection<?> c) { + if (ds.removeAll(c)) { + if (changeHandler != null) { + // Have to update the whole list as the removal does not + // have to be a continuous range + changeHandler.dataUpdated(0, ds.size()); + } + return true; + } + return false; + } + + @Override + public boolean retainAll(Collection<?> c) { + if (ds.retainAll(c)) { + if (changeHandler != null) { + // Have to update the whole list as the retain does not + // have to be a continuous range + changeHandler.dataUpdated(0, ds.size()); + } + return true; + } + return false; + } + + @Override + public void clear() { + int size = ds.size(); + ds.clear(); + if (changeHandler != null) { + changeHandler.dataRemoved(0, size); + } + } + + @Override + public T get(int index) { + return ds.get(index); + } + + @Override + public T set(int index, T element) { + T prev = ds.set(index, element); + if (changeHandler != null) { + changeHandler.dataUpdated(index, 1); + } + return prev; + } + + @Override + public void add(int index, T element) { + ds.add(index, element); + if (changeHandler != null) { + changeHandler.dataAdded(index, 1); + } + } + + @Override + public T remove(int index) { + T removed = ds.remove(index); + if (changeHandler != null) { + changeHandler.dataRemoved(index, 1); + } + return removed; + } + + @Override + public int indexOf(Object o) { + return ds.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return ds.lastIndexOf(o); + } + + @Override + public ListIterator<T> listIterator() { + // TODO could be implemented by a custom iterator. + throw new UnsupportedOperationException( + "List iterators not supported at this time."); + } + + @Override + public ListIterator<T> listIterator(int index) { + // TODO could be implemented by a custom iterator. + throw new UnsupportedOperationException( + "List iterators not supported at this time."); + } + + @Override + public List<T> subList(int fromIndex, int toIndex) { + throw new UnsupportedOperationException("Sub lists not supported."); + } + } + + /** + * Iterator returned by {@link ListWrapper} + */ + private class ListWrapperIterator implements Iterator<T> { + + private final Iterator<T> iterator; + + /** + * Constructs a new iterator + */ + public ListWrapperIterator(Iterator<T> iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public T next() { + return iterator.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException( + "Iterator.remove() is not supported by this iterator."); + } + } + + /** + * Datasource for providing row pojo's + */ + private final List<T> ds; + + /** + * Wrapper that wraps the data source + */ + private final ListWrapper wrapper; + + /** + * Handler for listening to changes in the underlying list. + */ + private DataChangeHandler changeHandler; + + /** + * Constructs a new list data source. + * <p> + * Note: Modifications to the original list will not be reflected in the + * data source after the data source has been constructed. To add or remove + * items to the data source after it has been constructed use + * {@link ListDataSource#asList()}. + * + * + * @param datasource + * The list to use for providing the data to the grid + */ + public ListDataSource(List<T> datasource) { + if (datasource == null) { + throw new IllegalArgumentException("datasource cannot be null"); + } + ds = new ArrayList<T>(datasource); + wrapper = new ListWrapper(); + } + + /** + * Constructs a data source with a set of rows. You can dynamically add and + * remove rows from the data source via the list you get from + * {@link ListDataSource#asList()} + * + * @param rows + * The rows to initially add to the data source + */ + public ListDataSource(T... rows) { + if (rows == null) { + ds = new ArrayList<T>(); + } else { + ds = new ArrayList<T>(Arrays.asList(rows)); + } + wrapper = new ListWrapper(); + } + + @Override + public void ensureAvailability(int firstRowIndex, int numberOfRows) { + if (firstRowIndex >= ds.size()) { + throw new IllegalStateException( + "Trying to fetch rows outside of array"); + } + } + + @Override + public T getRow(int rowIndex) { + return ds.get(rowIndex); + } + + @Override + public int getEstimatedSize() { + return ds.size(); + } + + @Override + public void setDataChangeHandler(DataChangeHandler dataChangeHandler) { + this.changeHandler = dataChangeHandler; + } + + /** + * Gets the list that backs this datasource. Any changes made to this list + * will be reflected in the datasource. + * <p> + * Note: The list is not the same list as passed into the data source via + * the constructor. + * + * @return Returns a list implementation that wraps the real list that backs + * the data source and provides events for the data source + * listeners. + */ + public List<T> asList() { + return wrapper; + } +} diff --git a/client/src/com/vaadin/client/ui/grid/renderers/DateRenderer.java b/client/src/com/vaadin/client/ui/grid/renderers/DateRenderer.java new file mode 100644 index 0000000000..d1f770f414 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/renderers/DateRenderer.java @@ -0,0 +1,94 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid.renderers; + +import java.util.Date; + +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.i18n.client.TimeZone; +import com.vaadin.client.ui.grid.Cell; +import com.vaadin.client.ui.grid.Renderer; + +/** + * A renderer for rendering dates into cells + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class DateRenderer implements Renderer<Date> { + + private DateTimeFormat format = DateTimeFormat.getShortDateTimeFormat(); + + private TimeZone timeZone = TimeZone.createTimeZone(new Date() + .getTimezoneOffset()); + + @Override + public void renderCell(Cell cell, Date date) { + String dateStr = format.format(date, timeZone); + cell.getElement().setInnerText(dateStr); + } + + /** + * Gets the format of how the date is formatted. + * + * @return the format + * @see <a + * href="http://www.gwtproject.org/javadoc/latest/com/google/gwt/i18n/client/DateTimeFormat.html">GWT + * documentation on DateTimeFormat</a> + */ + public DateTimeFormat getFormat() { + return format; + } + + /** + * Sets the format used for formatting the dates. + * + * @param format + * the format to set + * @see <a + * href="http://www.gwtproject.org/javadoc/latest/com/google/gwt/i18n/client/DateTimeFormat.html">GWT + * documentation on DateTimeFormat</a> + */ + public void setFormat(DateTimeFormat format) { + if (format == null) { + throw new IllegalArgumentException("Format should not be null"); + } + this.format = format; + } + + /** + * Returns the time zone of the date. + * + * @return the time zone + */ + public TimeZone getTimeZone() { + return timeZone; + } + + /** + * Sets the time zone of the the date. By default uses the time zone of the + * browser. + * + * @param timeZone + * the timeZone to set + */ + public void setTimeZone(TimeZone timeZone) { + if (timeZone == null) { + throw new IllegalArgumentException("Timezone should not be null"); + } + this.timeZone = timeZone; + } +} diff --git a/client/src/com/vaadin/client/ui/grid/renderers/HtmlRenderer.java b/client/src/com/vaadin/client/ui/grid/renderers/HtmlRenderer.java new file mode 100644 index 0000000000..0787dc2332 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/renderers/HtmlRenderer.java @@ -0,0 +1,42 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid.renderers; + +import com.google.gwt.safehtml.shared.SafeHtml; +import com.google.gwt.safehtml.shared.SafeHtmlUtils; +import com.vaadin.client.ui.grid.Cell; +import com.vaadin.client.ui.grid.Renderer; + +/** + * Renders a string as HTML into a cell. + * <p> + * The html string is rendered as is without any escaping. It is up to the + * developer to ensure that the html string honors the {@link SafeHtml} + * contract. For more information see + * {@link SafeHtmlUtils#fromSafeConstant(String)}. + * + * @since 7.2 + * @author Vaadin Ltd + * @see SafeHtmlUtils#fromSafeConstant(String) + */ +public class HtmlRenderer implements Renderer<String> { + + @Override + public void renderCell(Cell cell, String htmlString) { + cell.getElement().setInnerSafeHtml( + SafeHtmlUtils.fromSafeConstant(htmlString)); + } +} diff --git a/client/src/com/vaadin/client/ui/grid/renderers/NumberRenderer.java b/client/src/com/vaadin/client/ui/grid/renderers/NumberRenderer.java new file mode 100644 index 0000000000..f4efea33a5 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/renderers/NumberRenderer.java @@ -0,0 +1,64 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid.renderers; + +import com.google.gwt.i18n.client.NumberFormat; +import com.vaadin.client.ui.grid.Cell; +import com.vaadin.client.ui.grid.Renderer; + +/** + * Renders a number into a cell using a specific {@link NumberFormat}. By + * default uses the default number format returned by + * {@link NumberFormat#getDecimalFormat()}. + * + * @since 7.2 + * @author Vaadin Ltd + * @param <T> + * The number type to render. + */ +public class NumberRenderer<T extends Number> implements Renderer<T> { + + private NumberFormat format = NumberFormat.getDecimalFormat(); + + /** + * Gets the number format that the number should be formatted in. + * + * @return the number format used to render the number + */ + public NumberFormat getFormat() { + return format; + } + + /** + * Sets the number format to use for formatting the number. + * + * @param format + * the format to use + * @throws IllegalArgumentException + * when the format is null + */ + public void setFormat(NumberFormat format) throws IllegalArgumentException { + if (format == null) { + throw new IllegalArgumentException("Format cannot be null"); + } + this.format = format; + } + + @Override + public void renderCell(Cell cell, Number number) { + cell.getElement().setInnerText(format.format(number)); + } +} diff --git a/client/src/com/vaadin/client/ui/grid/renderers/TextRenderer.java b/client/src/com/vaadin/client/ui/grid/renderers/TextRenderer.java new file mode 100644 index 0000000000..1f06a555c3 --- /dev/null +++ b/client/src/com/vaadin/client/ui/grid/renderers/TextRenderer.java @@ -0,0 +1,33 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid.renderers; + +import com.vaadin.client.ui.grid.Cell; +import com.vaadin.client.ui.grid.Renderer; + +/** + * Renderer that renders text into a cell. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class TextRenderer implements Renderer<String> { + + @Override + public void renderCell(Cell cell, String text) { + cell.getElement().setInnerText(text); + } +} diff --git a/client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java b/client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java new file mode 100644 index 0000000000..5c5e88bf69 --- /dev/null +++ b/client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java @@ -0,0 +1,178 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; + +import org.easymock.EasyMock; +import org.junit.Test; + +import com.vaadin.client.data.DataChangeHandler; +import com.vaadin.client.ui.grid.datasources.ListDataSource; + +/** + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class ListDataSourceTest { + + @Test + public void testDataSourceConstruction() throws Exception { + + ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3); + + assertEquals(4, ds.getEstimatedSize()); + assertEquals(0, (int) ds.getRow(0)); + assertEquals(1, (int) ds.getRow(1)); + assertEquals(2, (int) ds.getRow(2)); + assertEquals(3, (int) ds.getRow(3)); + + ds = new ListDataSource<Integer>(Arrays.asList(0, 1, 2, 3)); + + assertEquals(4, ds.getEstimatedSize()); + assertEquals(0, (int) ds.getRow(0)); + assertEquals(1, (int) ds.getRow(1)); + assertEquals(2, (int) ds.getRow(2)); + assertEquals(3, (int) ds.getRow(3)); + } + + @Test + public void testListAddOperation() throws Exception { + + ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3); + + DataChangeHandler handler = EasyMock + .createNiceMock(DataChangeHandler.class); + ds.setDataChangeHandler(handler); + + handler.dataAdded(4, 1); + EasyMock.expectLastCall(); + + EasyMock.replay(handler); + + ds.asList().add(4); + + assertEquals(5, ds.getEstimatedSize()); + assertEquals(0, (int) ds.getRow(0)); + assertEquals(1, (int) ds.getRow(1)); + assertEquals(2, (int) ds.getRow(2)); + assertEquals(3, (int) ds.getRow(3)); + assertEquals(4, (int) ds.getRow(4)); + } + + @Test + public void testListAddAllOperation() throws Exception { + + ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3); + + DataChangeHandler handler = EasyMock + .createNiceMock(DataChangeHandler.class); + ds.setDataChangeHandler(handler); + + handler.dataAdded(4, 3); + EasyMock.expectLastCall(); + + EasyMock.replay(handler); + + ds.asList().addAll(Arrays.asList(4, 5, 6)); + + assertEquals(7, ds.getEstimatedSize()); + assertEquals(0, (int) ds.getRow(0)); + assertEquals(1, (int) ds.getRow(1)); + assertEquals(2, (int) ds.getRow(2)); + assertEquals(3, (int) ds.getRow(3)); + assertEquals(4, (int) ds.getRow(4)); + assertEquals(5, (int) ds.getRow(5)); + assertEquals(6, (int) ds.getRow(6)); + } + + @Test + public void testListRemoveOperation() throws Exception { + + ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3); + + DataChangeHandler handler = EasyMock + .createNiceMock(DataChangeHandler.class); + ds.setDataChangeHandler(handler); + + handler.dataRemoved(3, 1); + EasyMock.expectLastCall(); + + EasyMock.replay(handler); + + ds.asList().remove(2); + + assertEquals(3, ds.getEstimatedSize()); + assertEquals(0, (int) ds.getRow(0)); + assertEquals(1, (int) ds.getRow(1)); + assertEquals(3, (int) ds.getRow(2)); + } + + @Test + public void testListRemoveAllOperation() throws Exception { + + ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3); + + DataChangeHandler handler = EasyMock + .createNiceMock(DataChangeHandler.class); + ds.setDataChangeHandler(handler); + + handler.dataRemoved(0, 3); + EasyMock.expectLastCall(); + + EasyMock.replay(handler); + + ds.asList().removeAll(Arrays.asList(0, 2, 3)); + + assertEquals(1, ds.getEstimatedSize()); + assertEquals(1, (int) ds.getRow(0)); + } + + @Test + public void testListClearOperation() throws Exception { + + ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3); + + DataChangeHandler handler = EasyMock + .createNiceMock(DataChangeHandler.class); + ds.setDataChangeHandler(handler); + + handler.dataRemoved(0, 4); + EasyMock.expectLastCall(); + + EasyMock.replay(handler); + + ds.asList().clear(); + + assertEquals(0, ds.getEstimatedSize()); + } + + @Test(expected = IllegalStateException.class) + public void testFetchingNonExistantItem() { + ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3); + ds.ensureAvailability(5, 1); + } + + @Test(expected = UnsupportedOperationException.class) + public void testUnsupportedIteratorRemove() { + ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3); + ds.asList().iterator().remove(); + } + +} diff --git a/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java new file mode 100644 index 0000000000..e97bb339e4 --- /dev/null +++ b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java @@ -0,0 +1,104 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.client.ui.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.vaadin.shared.ui.grid.Range; + +@SuppressWarnings("static-method") +public class PartitioningTest { + + @Test + public void selfRangeTest() { + final Range range = Range.between(0, 10); + final Range[] partitioning = range.partitionWith(range); + + assertTrue("before is empty", partitioning[0].isEmpty()); + assertTrue("inside is self", partitioning[1].equals(range)); + assertTrue("after is empty", partitioning[2].isEmpty()); + } + + @Test + public void beforeRangeTest() { + final Range beforeRange = Range.between(0, 10); + final Range afterRange = Range.between(10, 20); + final Range[] partitioning = beforeRange.partitionWith(afterRange); + + assertTrue("before is self", partitioning[0].equals(beforeRange)); + assertTrue("inside is empty", partitioning[1].isEmpty()); + assertTrue("after is empty", partitioning[2].isEmpty()); + } + + @Test + public void afterRangeTest() { + final Range beforeRange = Range.between(0, 10); + final Range afterRange = Range.between(10, 20); + final Range[] partitioning = afterRange.partitionWith(beforeRange); + + assertTrue("before is empty", partitioning[0].isEmpty()); + assertTrue("inside is empty", partitioning[1].isEmpty()); + assertTrue("after is self", partitioning[2].equals(afterRange)); + } + + @Test + public void beforeAndInsideRangeTest() { + final Range beforeRange = Range.between(0, 10); + final Range afterRange = Range.between(5, 15); + final Range[] partitioning = beforeRange.partitionWith(afterRange); + + assertEquals("before", Range.between(0, 5), partitioning[0]); + assertEquals("inside", Range.between(5, 10), partitioning[1]); + assertTrue("after is empty", partitioning[2].isEmpty()); + } + + @Test + public void insideRangeTest() { + final Range fullRange = Range.between(0, 20); + final Range insideRange = Range.between(5, 15); + final Range[] partitioning = insideRange.partitionWith(fullRange); + + assertTrue("before is empty", partitioning[0].isEmpty()); + assertEquals("inside", Range.between(5, 15), partitioning[1]); + assertTrue("after is empty", partitioning[2].isEmpty()); + } + + @Test + public void insideAndBelowTest() { + final Range beforeRange = Range.between(0, 10); + final Range afterRange = Range.between(5, 15); + final Range[] partitioning = afterRange.partitionWith(beforeRange); + + assertTrue("before is empty", partitioning[0].isEmpty()); + assertEquals("inside", Range.between(5, 10), partitioning[1]); + assertEquals("after", Range.between(10, 15), partitioning[2]); + } + + @Test + public void aboveAndBelowTest() { + final Range fullRange = Range.between(0, 20); + final Range insideRange = Range.between(5, 15); + final Range[] partitioning = fullRange.partitionWith(insideRange); + + assertEquals("before", Range.between(0, 5), partitioning[0]); + assertEquals("inside", Range.between(5, 15), partitioning[1]); + assertEquals("after", Range.between(15, 20), partitioning[2]); + } +} diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java new file mode 100644 index 0000000000..b22e6a209b --- /dev/null +++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java @@ -0,0 +1,148 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.data; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import com.vaadin.data.Container.Indexed; +import com.vaadin.server.AbstractExtension; +import com.vaadin.shared.data.DataProviderRpc; +import com.vaadin.shared.data.DataProviderState; +import com.vaadin.shared.data.DataRequestRpc; +import com.vaadin.ui.components.grid.Grid; + +/** + * Provides Vaadin server-side container data source to a + * {@link com.vaadin.client.ui.grid.GridConnector}. This is currently + * implemented as an Extension hardcoded to support a specific connector type. + * This will be changed once framework support for something more flexible has + * been implemented. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class RpcDataProviderExtension extends AbstractExtension { + + private final Indexed container; + + /** + * Creates a new data provider using the given container. + * + * @param container + * the container to make available + */ + public RpcDataProviderExtension(Indexed container) { + this.container = container; + + // TODO support for reacting to events from the container added later + + registerRpc(new DataRequestRpc() { + @Override + public void requestRows(int firstRow, int numberOfRows) { + pushRows(firstRow, numberOfRows); + } + }); + + getState().containerSize = container.size(); + } + + private void pushRows(int firstRow, int numberOfRows) { + List<?> itemIds = container.getItemIds(firstRow, numberOfRows); + Collection<?> propertyIds = container.getContainerPropertyIds(); + List<String[]> rows = new ArrayList<String[]>(itemIds.size()); + for (Object itemId : itemIds) { + rows.add(getRowData(propertyIds, itemId)); + } + getRpcProxy(DataProviderRpc.class).setRowData(firstRow, rows); + } + + private String[] getRowData(Collection<?> propertyIds, Object itemId) { + Item item = container.getItem(itemId); + String[] row = new String[propertyIds.size()]; + + int i = 0; + for (Object propertyId : propertyIds) { + Object value = item.getItemProperty(propertyId).getValue(); + String stringValue = String.valueOf(value); + row[i++] = stringValue; + } + return row; + } + + @Override + protected DataProviderState getState() { + return (DataProviderState) super.getState(); + } + + /** + * Makes the data source available to the given {@link Grid} component. + * + * @param component + * the remote data grid component to extend + */ + public void extend(Grid component) { + super.extend(component); + } + + /** + * Informs the client side that new rows have been inserted into the data + * source. + * + * @param index + * the index at which new rows have been inserted + * @param count + * the number of rows inserted at <code>index</code> + */ + public void insertRowData(int index, int count) { + getState().containerSize += count; + getRpcProxy(DataProviderRpc.class).insertRowData(index, count); + } + + /** + * Informs the client side that rows have been removed from the data source. + * + * @param firstIndex + * the index of the first row removed + * @param count + * the number of rows removed + */ + public void removeRowData(int firstIndex, int count) { + getState().containerSize -= count; + getRpcProxy(DataProviderRpc.class).removeRowData(firstIndex, count); + } + + /** + * Informs the client side that data of a row has been modified in the data + * source. + * + * @param index + * the index of the row that was updated + */ + public void updateRowData(int index) { + /* + * TODO: ignore duplicate requests for the same index during the same + * roundtrip. + */ + Object itemId = container.getIdByIndex(index); + String[] row = getRowData(container.getContainerPropertyIds(), itemId); + getRpcProxy(DataProviderRpc.class).setRowData(index, + Collections.singletonList(row)); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/ColumnGroup.java b/server/src/com/vaadin/ui/components/grid/ColumnGroup.java new file mode 100644 index 0000000000..6b14ef81d4 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/ColumnGroup.java @@ -0,0 +1,165 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.ui.components.grid; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.vaadin.shared.ui.grid.ColumnGroupState; + +/** + * Column groups are used to group columns together for adding common auxiliary + * headers and footers. Columns groups are added to {@link ColumnGroupRow}'s. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class ColumnGroup implements Serializable { + + /** + * List of property ids belonging to this group + */ + private List<Object> columns; + + /** + * The grid the column group is associated with + */ + private final Grid grid; + + /** + * The column group row the column group is attached to + */ + private final ColumnGroupRow row; + + /** + * The common state between the server and the client + */ + private final ColumnGroupState state; + + /** + * Constructs a new column group + * + * @param grid + * the grid the column group is associated with + * @param state + * the state representing the data of the grid. Sent to the + * client + * @param propertyIds + * the property ids of the columns that belongs to the group + * @param groups + * the sub groups who should be included in this group + * + */ + ColumnGroup(Grid grid, ColumnGroupRow row, ColumnGroupState state, + List<Object> propertyIds) { + if (propertyIds == null) { + throw new IllegalArgumentException( + "propertyIds cannot be null. Use empty list instead."); + } + + this.state = state; + this.row = row; + columns = Collections.unmodifiableList(new ArrayList<Object>( + propertyIds)); + this.grid = grid; + } + + /** + * Sets the text displayed in the header of the column group. + * + * @param header + * the text displayed in the header of the column + */ + public void setHeaderCaption(String header) { + checkGroupIsAttached(); + state.header = header; + grid.markAsDirty(); + } + + /** + * Sets the text displayed in the header of the column group. + * + * @return the text displayed in the header of the column + */ + public String getHeaderCaption() { + checkGroupIsAttached(); + return state.header; + } + + /** + * Sets the text displayed in the footer of the column group. + * + * @param footer + * the text displayed in the footer of the column + */ + public void setFooterCaption(String footer) { + checkGroupIsAttached(); + state.footer = footer; + grid.markAsDirty(); + } + + /** + * The text displayed in the footer of the column group. + * + * @return the text displayed in the footer of the column + */ + public String getFooterCaption() { + checkGroupIsAttached(); + return state.footer; + } + + /** + * Is a property id in this group or in some sub group of this group. + * + * @param propertyId + * the property id to check for + * @return <code>true</code> if the property id is included in this group. + */ + public boolean isColumnInGroup(Object propertyId) { + if (columns.contains(propertyId)) { + return true; + } + return false; + } + + /** + * Returns a list of property ids where all also the child groups property + * ids are included. + * + * @return a unmodifiable list with all the columns in the group. Includes + * any subgroup columns as well. + */ + public List<Object> getColumns() { + return columns; + } + + /** + * Checks if column group is attached to a row and throws an + * {@link IllegalStateException} if it is not. + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + protected void checkGroupIsAttached() throws IllegalStateException { + if (!row.getState().groups.contains(state)) { + throw new IllegalStateException( + "Column Group has been removed from the row."); + } + } +} diff --git a/server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java b/server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java new file mode 100644 index 0000000000..b90b4df2c5 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java @@ -0,0 +1,303 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.ui.components.grid; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.vaadin.server.KeyMapper; +import com.vaadin.shared.ui.grid.ColumnGroupRowState; +import com.vaadin.shared.ui.grid.ColumnGroupState; + +/** + * A column group row represents an auxiliary header or footer row added to the + * grid. A column group row includes column groups that group columns together. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class ColumnGroupRow implements Serializable { + + /** + * The common state shared between the client and server + */ + private final ColumnGroupRowState state; + + /** + * The column groups in this row + */ + private List<ColumnGroup> groups = new ArrayList<ColumnGroup>(); + + /** + * Grid that the group row belongs to + */ + private final Grid grid; + + /** + * The column keys used to identify the column on the client side + */ + private final KeyMapper<Object> columnKeys; + + /** + * Constructs a new column group + * + * @param grid + * The grid that the column group is associated to + * @param state + * The shared state which contains the data shared between server + * and client + * @param columnKeys + * The column key mapper for converting property ids to client + * side column identifiers + */ + ColumnGroupRow(Grid grid, ColumnGroupRowState state, + KeyMapper<Object> columnKeys) { + this.grid = grid; + this.columnKeys = columnKeys; + this.state = state; + } + + /** + * Gets the shared state for the column group row. Used internally to send + * the group row to the client. + * + * @return The current state of the row + */ + ColumnGroupRowState getState() { + return state; + } + + /** + * Add a new group to the row by using property ids for the columns. + * + * @param propertyIds + * The property ids of the columns that should be included in the + * group. A column can only belong in group on a row at a time. + * @return a column group representing the collection of columns added to + * the group + */ + public ColumnGroup addGroup(Object... propertyIds) + throws IllegalArgumentException { + assert propertyIds != null : "propertyIds cannot be null."; + + for (Object propertyId : propertyIds) { + if (hasColumnBeenGrouped(propertyId)) { + throw new IllegalArgumentException("Column " + + String.valueOf(propertyId) + + " already belongs to another group."); + } + } + + validateNewGroupProperties(Arrays.asList(propertyIds)); + + ColumnGroupState state = new ColumnGroupState(); + for (Object propertyId : propertyIds) { + assert propertyId != null : "null items in columns array not supported."; + state.columns.add(columnKeys.key(propertyId)); + } + this.state.groups.add(state); + + ColumnGroup group = new ColumnGroup(grid, this, state, + Arrays.asList(propertyIds)); + groups.add(group); + + grid.markAsDirty(); + return group; + } + + private void validateNewGroupProperties(List<Object> propertyIds) + throws IllegalArgumentException { + + /* + * Validate parent grouping + */ + int rowIndex = grid.getColumnGroupRows().indexOf(this); + int parentRowIndex = rowIndex - 1; + + // Get the parent row of this row. + ColumnGroupRow parentRow = null; + if (parentRowIndex > -1) { + parentRow = grid.getColumnGroupRows().get(parentRowIndex); + } + + if (parentRow == null) { + // A parentless row is always valid and is usually the first row + // added to the grid + return; + } + + for (Object id : propertyIds) { + if (parentRow.hasColumnBeenGrouped(id)) { + /* + * If a property has been grouped in the parent row then all of + * the properties in the parent group also needs to be included + * in the child group for the groups to be valid + */ + ColumnGroup parentGroup = parentRow.getGroupForProperty(id); + if (!propertyIds.containsAll(parentGroup.getColumns())) { + throw new IllegalArgumentException( + "Grouped properties overlaps previous grouping bounderies"); + } + } + } + } + + /** + * Add a new group to the row by using column instances. + * + * @param columns + * the columns that should belong to the group + * @return a column group representing the collection of columns added to + * the group + */ + public ColumnGroup addGroup(GridColumn... columns) + throws IllegalArgumentException { + assert columns != null : "columns cannot be null"; + + List<Object> propertyIds = new ArrayList<Object>(); + for (GridColumn column : columns) { + assert column != null : "null items in columns array not supported."; + + String columnId = column.getState().id; + Object propertyId = grid.getPropertyIdByColumnId(columnId); + propertyIds.add(propertyId); + } + return addGroup(propertyIds.toArray()); + } + + /** + * Add a new group to the row by using other already greated groups + * + * @param groups + * the subgroups of the group + * @return a column group representing the collection of columns added to + * the group + * + */ + public ColumnGroup addGroup(ColumnGroup... groups) + throws IllegalArgumentException { + assert groups != null : "groups cannot be null"; + + // Gather all groups columns into one list + List<Object> propertyIds = new ArrayList<Object>(); + for (ColumnGroup group : groups) { + propertyIds.addAll(group.getColumns()); + } + + validateNewGroupProperties(propertyIds); + + ColumnGroupState state = new ColumnGroupState(); + ColumnGroup group = new ColumnGroup(grid, this, state, propertyIds); + this.groups.add(group); + + // Update state + for (Object propertyId : group.getColumns()) { + state.columns.add(columnKeys.key(propertyId)); + } + this.state.groups.add(state); + + grid.markAsDirty(); + return group; + } + + /** + * Removes a group from the row. Does not remove the group from subgroups, + * to remove it from the subgroup invoke removeGroup on the subgroup. + * + * @param group + * the group to remove + */ + public void removeGroup(ColumnGroup group) { + int index = groups.indexOf(group); + groups.remove(index); + state.groups.remove(index); + grid.markAsDirty(); + } + + /** + * Get the groups in the row. + * + * @return unmodifiable list of groups in this row + */ + public List<ColumnGroup> getGroups() { + return Collections.unmodifiableList(groups); + } + + /** + * Checks if a property id has been added to a group in this row. + * + * @param propertyId + * the property id to check for + * @return <code>true</code> if the column is included in a group + */ + private boolean hasColumnBeenGrouped(Object propertyId) { + return getGroupForProperty(propertyId) != null; + } + + private ColumnGroup getGroupForProperty(Object propertyId) { + for (ColumnGroup group : groups) { + if (group.isColumnInGroup(propertyId)) { + return group; + } + } + return null; + } + + /** + * Is the header visible for the row. + * + * @return <code>true</code> if header is visible + */ + public boolean isHeaderVisible() { + return state.headerVisible; + } + + /** + * Sets the header visible for the row. + * + * @param visible + * should the header be shown + */ + public void setHeaderVisible(boolean visible) { + state.headerVisible = visible; + grid.markAsDirty(); + } + + /** + * Is the footer visible for the row. + * + * @return <code>true</code> if footer is visible + */ + public boolean isFooterVisible() { + return state.footerVisible; + } + + /** + * Sets the footer visible for the row. + * + * @param visible + * should the footer be shown + */ + public void setFooterVisible(boolean visible) { + state.footerVisible = visible; + grid.markAsDirty(); + } + +} diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java new file mode 100644 index 0000000000..4126ec6d93 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/Grid.java @@ -0,0 +1,861 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.ui.components.grid; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.vaadin.data.Container; +import com.vaadin.data.Container.Indexed.ItemAddEvent; +import com.vaadin.data.Container.Indexed.ItemRemoveEvent; +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.Container.ItemSetChangeListener; +import com.vaadin.data.Container.ItemSetChangeNotifier; +import com.vaadin.data.Container.PropertySetChangeEvent; +import com.vaadin.data.Container.PropertySetChangeListener; +import com.vaadin.data.Container.PropertySetChangeNotifier; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.data.Property.ValueChangeNotifier; +import com.vaadin.data.RpcDataProviderExtension; +import com.vaadin.server.KeyMapper; +import com.vaadin.shared.ui.grid.ColumnGroupRowState; +import com.vaadin.shared.ui.grid.GridClientRpc; +import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridServerRpc; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.Range; +import com.vaadin.shared.ui.grid.ScrollDestination; +import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.Component; + +/** + * Data grid component + * + * <h3>Lazy loading</h3> TODO To be revised when the data data source + * implementation has been don. + * + * <h3>Columns</h3> The grid columns are based on the property ids of the + * underlying data source. Each property id represents one column in the grid. + * To retrive a column in the grid you can use {@link Grid#getColumn(Object)} + * with the property id of the column. A grid column contains properties like + * the width, the footer and header captions of the column. + * + * <h3>Auxiliary headers and footers</h3> TODO To be revised when column + * grouping is implemented. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class Grid extends AbstractComponent { + + /** + * A helper class that handles the client-side Escalator logic relating to + * making sure that whatever is currently visible to the user, is properly + * initialized and otherwise handled on the server side (as far as + * requried). + * <p> + * This bookeeping includes, but is not limited to: + * <ul> + * <li>listening to the currently visible {@link Property Properties'} value + * changes on the server side and sending those back to the client; and + * <li>attaching and detaching {@link Component Components} from the Vaadin + * Component hierarchy. + * </ul> + */ + private final class ActiveRowHandler { + /** + * A map from itemId to the value change listener used for all of its + * properties + */ + private final Map<Object, GridValueChangeListener> valueChangeListeners = new HashMap<Object, GridValueChangeListener>(); + + /** + * The currently active range. Practically, it's the range of row + * indices being displayed currently. + */ + private Range activeRange = Range.withLength(0, 0); + + /** + * A hook for making sure that appropriate data is "active". All other + * rows should be "inactive". + * <p> + * "Active" can mean different things in different contexts. For + * example, only the Properties in the active range need + * ValueChangeListeners. Also, whenever a row with a Component becomes + * active, it needs to be attached (and conversely, when inactive, it + * needs to be detached). + * + * @param firstActiveRow + * the first active row + * @param activeRowCount + * the number of active rows + */ + public void setActiveRows(int firstActiveRow, int activeRowCount) { + + final Range newActiveRange = Range.withLength(firstActiveRow, + activeRowCount); + + // TODO [[Components]] attach and detach components + + /*- + * Example + * + * New Range: [3, 4, 5, 6, 7] + * Old Range: [1, 2, 3, 4, 5] + * Result: [1, 2][3, 4, 5] [] + */ + final Range[] depractionPartition = activeRange + .partitionWith(newActiveRange); + removeValueChangeListeners(depractionPartition[0]); + removeValueChangeListeners(depractionPartition[2]); + + /*- + * Example + * + * Old Range: [1, 2, 3, 4, 5] + * New Range: [3, 4, 5, 6, 7] + * Result: [] [3, 4, 5][6, 7] + */ + final Range[] activationPartition = newActiveRange + .partitionWith(activeRange); + addValueChangeListeners(activationPartition[0]); + addValueChangeListeners(activationPartition[2]); + + activeRange = newActiveRange; + } + + private void addValueChangeListeners(Range range) { + for (int i = range.getStart(); i < range.getEnd(); i++) { + + final Object itemId = datasource.getIdByIndex(i); + final Item item = datasource.getItem(itemId); + + if (valueChangeListeners.containsKey(itemId)) { + /* + * This might occur when items are removed from above the + * viewport, the escalator scrolls up to compensate, but the + * same items remain in the view: It looks as if one row was + * scrolled, when in fact the whole viewport was shifted up. + */ + continue; + } + + GridValueChangeListener listener = new GridValueChangeListener( + itemId); + valueChangeListeners.put(itemId, listener); + + for (final Object propertyId : item.getItemPropertyIds()) { + final Property<?> property = item + .getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .addValueChangeListener(listener); + } + } + } + } + + private void removeValueChangeListeners(Range range) { + for (int i = range.getStart(); i < range.getEnd(); i++) { + final Object itemId = datasource.getIdByIndex(i); + final Item item = datasource.getItem(itemId); + final GridValueChangeListener listener = valueChangeListeners + .remove(itemId); + + if (listener != null) { + for (final Object propertyId : item.getItemPropertyIds()) { + final Property<?> property = item + .getItemProperty(propertyId); + + /* + * Because listener != null, we can be certain that this + * property is a ValueChangeNotifier: It wouldn't be + * inserted in addValueChangeListeners if the property + * wasn't a suitable type. I.e. No need for "instanceof" + * check. + */ + ((ValueChangeNotifier) property) + .removeValueChangeListener(listener); + } + } + } + } + + public void clear() { + removeValueChangeListeners(activeRange); + /* + * we're doing an assert for emptiness there (instead of a + * carte-blanche ".clear()"), to be absolutely sure that everything + * is cleaned up properly, and that we have no dangling listeners. + */ + assert valueChangeListeners.isEmpty() : "GridValueChangeListeners are leaking"; + + activeRange = Range.withLength(0, 0); + } + + /** + * Manages removed properties in active rows. + * + * @param removedPropertyIds + * the property ids that have been removed from the container + */ + public void propertiesRemoved(Collection<Object> removedPropertyIds) { + /* + * no-op, for now. + * + * The Container should be responsible for cleaning out any + * ValueChangeListeners from removed Properties. Components will + * benefit from this, however. + */ + } + + /** + * Manages added properties in active rows. + * + * @param addedPropertyIds + * the property ids that have been added to the container + */ + public void propertiesAdded(Collection<Object> addedPropertyIds) { + for (int i = activeRange.getStart(); i < activeRange.getEnd(); i++) { + final Object itemId = datasource.getIdByIndex(i); + final Item item = datasource.getItem(itemId); + final GridValueChangeListener listener = valueChangeListeners + .get(itemId); + assert (listener != null) : "a listener should've been pre-made by addValueChangeListeners"; + + for (final Object propertyId : addedPropertyIds) { + final Property<?> property = item + .getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .addValueChangeListener(listener); + } + } + } + } + + /** + * Handles the insertion of rows. + * <p> + * This method's responsibilities are to: + * <ul> + * <li>shift the internal bookkeeping by <code>count</code> if the + * insertion happens above currently active range + * <li>ignore rows inserted below the currently active range + * <li>shift (and deactivate) rows pushed out of view + * <li>activate rows that are inserted in the current viewport + * </ul> + * + * @param firstIndex + * the index of the first inserted rows + * @param count + * the number of rows inserted at <code>firstIndex</code> + */ + public void insertRows(int firstIndex, int count) { + if (firstIndex < activeRange.getStart()) { + activeRange = activeRange.offsetBy(count); + } else if (firstIndex < activeRange.getEnd()) { + final Range deprecatedRange = Range.withLength( + activeRange.getEnd(), count); + removeValueChangeListeners(deprecatedRange); + + final Range freshRange = Range.between(firstIndex, count); + addValueChangeListeners(freshRange); + } else { + // out of view, noop + } + } + + /** + * Removes a single item by its id. + * + * @param itemId + * the id of the removed id. <em>Note:</em> this item does + * not exist anymore in the datasource + */ + public void removeItemId(Object itemId) { + final GridValueChangeListener removedListener = valueChangeListeners + .remove(itemId); + if (removedListener != null) { + /* + * We removed an item from somewhere in the visible range, so we + * make the active range shorter. The empty hole will be filled + * by the client-side code when it asks for more information. + */ + activeRange = Range.withLength(activeRange.getStart(), + activeRange.length() - 1); + } + } + } + + /** + * A class to listen to changes in property values in the Container added + * with {@link Grid#setContainerDatasource(Container.Indexed)}, and notifies + * the data source to update the client-side representation of the modified + * item. + * <p> + * One instance of this class can (and should) be reused for all the + * properties in an item, since this class will inform that the entire row + * needs to be re-evaluated (in contrast to a property-based change + * management) + * <p> + * Since there's no Container-wide possibility to listen to any kind of + * value changes, an instance of this class needs to be attached to each and + * every Item's Property in the container. + * + * @see Grid#addValueChangeListener(Container, Object, Object) + * @see Grid#valueChangeListeners + */ + private class GridValueChangeListener implements ValueChangeListener { + private final Object itemId; + + public GridValueChangeListener(Object itemId) { + /* + * Using an assert instead of an exception throw, just to optimize + * prematurely + */ + assert itemId != null : "null itemId not accepted"; + this.itemId = itemId; + } + + @Override + public void valueChange(ValueChangeEvent event) { + datasourceExtension.updateRowData(datasource.indexOfId(itemId)); + } + } + + /** + * The data source attached to the grid + */ + private Container.Indexed datasource; + + /** + * Property id to column instance mapping + */ + private final Map<Object, GridColumn> columns = new HashMap<Object, GridColumn>(); + + /** + * Key generator for column server-to-client communication + */ + private final KeyMapper<Object> columnKeys = new KeyMapper<Object>(); + + /** + * The column groups added to the grid + */ + private final List<ColumnGroupRow> columnGroupRows = new ArrayList<ColumnGroupRow>(); + + /** + * Property listener for listening to changes in data source properties. + */ + private final PropertySetChangeListener propertyListener = new PropertySetChangeListener() { + + @Override + public void containerPropertySetChange(PropertySetChangeEvent event) { + Collection<?> properties = new HashSet<Object>(event.getContainer() + .getContainerPropertyIds()); + + // Cleanup columns that are no longer in grid + List<Object> removedColumns = new LinkedList<Object>(); + for (Object columnId : columns.keySet()) { + if (!properties.contains(columnId)) { + removedColumns.add(columnId); + } + } + for (Object columnId : removedColumns) { + GridColumn column = columns.remove(columnId); + columnKeys.remove(columnId); + getState().columns.remove(column.getState()); + } + activeRowHandler.propertiesRemoved(removedColumns); + + // Add new columns + HashSet<Object> addedPropertyIds = new HashSet<Object>(); + for (Object propertyId : properties) { + if (!columns.containsKey(propertyId)) { + appendColumn(propertyId); + addedPropertyIds.add(propertyId); + } + } + activeRowHandler.propertiesAdded(addedPropertyIds); + + Object frozenPropertyId = columnKeys + .get(getState(false).lastFrozenColumnId); + if (!columns.containsKey(frozenPropertyId)) { + setLastFrozenPropertyId(null); + } + } + }; + + private ItemSetChangeListener itemListener = new ItemSetChangeListener() { + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + + if (event instanceof ItemAddEvent) { + ItemAddEvent addEvent = (ItemAddEvent) event; + int firstIndex = addEvent.getFirstIndex(); + int count = addEvent.getAddedItemsCount(); + datasourceExtension.insertRowData(firstIndex, count); + activeRowHandler.insertRows(firstIndex, count); + } + + else if (event instanceof ItemRemoveEvent) { + ItemRemoveEvent removeEvent = (ItemRemoveEvent) event; + int firstIndex = removeEvent.getFirstIndex(); + int count = removeEvent.getRemovedItemsCount(); + datasourceExtension.removeRowData(firstIndex, count); + + /* + * Unfortunately, there's no sane way of getting the rest of the + * removed itemIds. + * + * Fortunately, the only time _currently_ an event with more + * than one removed item seems to be when calling + * AbstractInMemoryContainer.removeAllElements(). Otherwise, + * it's only removing one item at a time. + * + * We _could_ have a backup of all the itemIds, and compare to + * that one, but we really really don't want to go there. + */ + activeRowHandler.removeItemId(removeEvent.getFirstItemId()); + } + + else { + // TODO no diff info available, redraw everything + throw new UnsupportedOperationException("bare " + + "ItemSetChangeEvents are currently " + + "not supported, use a container that " + + "uses AddItemEvents and RemoveItemEvents."); + } + } + }; + + private RpcDataProviderExtension datasourceExtension; + + private final ActiveRowHandler activeRowHandler = new ActiveRowHandler(); + + /** + * Creates a new Grid using the given datasource. + * + * @param datasource + * the data source for the grid + */ + public Grid(Container.Indexed datasource) { + setContainerDatasource(datasource); + + registerRpc(new GridServerRpc() { + @Override + public void setVisibleRows(int firstVisibleRow, int visibleRowCount) { + activeRowHandler + .setActiveRows(firstVisibleRow, visibleRowCount); + } + }); + } + + /** + * Sets the grid data source. + * + * @param container + * The container data source. Cannot be null. + * @throws IllegalArgumentException + * if the data source is null + */ + public void setContainerDatasource(Container.Indexed container) { + if (container == null) { + throw new IllegalArgumentException( + "Cannot set the datasource to null"); + } + if (datasource == container) { + return; + } + + // Remove old listeners + if (datasource instanceof PropertySetChangeNotifier) { + ((PropertySetChangeNotifier) datasource) + .removePropertySetChangeListener(propertyListener); + } + if (datasource instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) datasource) + .removeItemSetChangeListener(itemListener); + } + activeRowHandler.clear(); + + if (datasourceExtension != null) { + removeExtension(datasourceExtension); + } + + datasource = container; + datasourceExtension = new RpcDataProviderExtension(container); + datasourceExtension.extend(this); + + // Listen to changes in properties and remove columns if needed + if (datasource instanceof PropertySetChangeNotifier) { + ((PropertySetChangeNotifier) datasource) + .addPropertySetChangeListener(propertyListener); + } + if (datasource instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) datasource) + .addItemSetChangeListener(itemListener); + } + /* + * activeRowHandler will be updated by the client-side request that + * occurs on container change - no need to actively re-insert any + * ValueChangeListeners at this point. + */ + + getState().columns.clear(); + setLastFrozenPropertyId(null); + + // Add columns + for (Object propertyId : datasource.getContainerPropertyIds()) { + if (!columns.containsKey(propertyId)) { + GridColumn column = appendColumn(propertyId); + + // Add by default property id as column header + column.setHeaderCaption(String.valueOf(propertyId)); + } + } + + } + + /** + * Returns the grid data source. + * + * @return the container data source of the grid + */ + public Container.Indexed getContainerDatasource() { + return datasource; + } + + /** + * Returns a column based on the property id + * + * @param propertyId + * the property id of the column + * @return the column or <code>null</code> if not found + */ + public GridColumn getColumn(Object propertyId) { + return columns.get(propertyId); + } + + /** + * Sets the header rows visible. + * + * @param visible + * <code>true</code> if the header rows should be visible + */ + public void setColumnHeadersVisible(boolean visible) { + getState().columnHeadersVisible = visible; + } + + /** + * Are the header rows visible? + * + * @return <code>true</code> if the headers of the columns are visible + */ + public boolean isColumnHeadersVisible() { + return getState(false).columnHeadersVisible; + } + + /** + * Sets the footer rows visible. + * + * @param visible + * <code>true</code> if the footer rows should be visible + */ + public void setColumnFootersVisible(boolean visible) { + getState().columnFootersVisible = visible; + } + + /** + * Are the footer rows visible. + * + * @return <code>true</code> if the footer rows should be visible + */ + public boolean isColumnFootersVisible() { + return getState(false).columnFootersVisible; + } + + /** + * <p> + * Adds a new column group to the grid. + * + * <p> + * Column group rows are rendered in the header and footer of the grid. + * Column group rows are made up of column groups which groups together + * columns for adding a common auxiliary header or footer for the columns. + * </p> + * </p> + * + * <p> + * Example usage: + * + * <pre> + * // Add a new column group row to the grid + * ColumnGroupRow row = grid.addColumnGroupRow(); + * + * // Group "Column1" and "Column2" together to form a header in the row + * ColumnGroup column12 = row.addGroup("Column1", "Column2"); + * + * // Set a common header for "Column1" and "Column2" + * column12.setHeader("Column 1&2"); + * </pre> + * + * </p> + * + * @return a column group instance you can use to add column groups + */ + public ColumnGroupRow addColumnGroupRow() { + ColumnGroupRowState state = new ColumnGroupRowState(); + ColumnGroupRow row = new ColumnGroupRow(this, state, columnKeys); + columnGroupRows.add(row); + getState().columnGroupRows.add(state); + return row; + } + + /** + * Adds a new column group to the grid at a specific index + * + * @param rowIndex + * the index of the row + * @return a column group instance you can use to add column groups + */ + public ColumnGroupRow addColumnGroupRow(int rowIndex) { + ColumnGroupRowState state = new ColumnGroupRowState(); + ColumnGroupRow row = new ColumnGroupRow(this, state, columnKeys); + columnGroupRows.add(rowIndex, row); + getState().columnGroupRows.add(rowIndex, state); + return row; + } + + /** + * Removes a column group. + * + * @param row + * the row to remove + */ + public void removeColumnGroupRow(ColumnGroupRow row) { + columnGroupRows.remove(row); + getState().columnGroupRows.remove(row.getState()); + } + + /** + * Gets the column group rows. + * + * @return an unmodifiable list of column group rows + */ + public List<ColumnGroupRow> getColumnGroupRows() { + return Collections.unmodifiableList(new ArrayList<ColumnGroupRow>( + columnGroupRows)); + } + + /** + * Used internally by the {@link Grid} to get a {@link GridColumn} by + * referencing its generated state id. Also used by {@link GridColumn} to + * verify if it has been detached from the {@link Grid}. + * + * @param columnId + * the client id generated for the column when the column is + * added to the grid + * @return the column with the id or <code>null</code> if not found + */ + GridColumn getColumnByColumnId(String columnId) { + Object propertyId = getPropertyIdByColumnId(columnId); + return getColumn(propertyId); + } + + /** + * Used internally by the {@link Grid} to get a property id by referencing + * the columns generated state id. + * + * @param columnId + * The state id of the column + * @return The column instance or null if not found + */ + Object getPropertyIdByColumnId(String columnId) { + return columnKeys.get(columnId); + } + + @Override + protected GridState getState() { + return (GridState) super.getState(); + } + + @Override + protected GridState getState(boolean markAsDirty) { + return (GridState) super.getState(markAsDirty); + } + + /** + * Creates a new column based on a property id and appends it as the last + * column. + * + * @param datasourcePropertyId + * The property id of a property in the datasource + */ + private GridColumn appendColumn(Object datasourcePropertyId) { + if (datasourcePropertyId == null) { + throw new IllegalArgumentException("Property id cannot be null"); + } + assert datasource.getContainerPropertyIds().contains( + datasourcePropertyId) : "Datasource should contain the property id"; + + GridColumnState columnState = new GridColumnState(); + columnState.id = columnKeys.key(datasourcePropertyId); + getState().columns.add(columnState); + + GridColumn column = new GridColumn(this, columnState); + columns.put(datasourcePropertyId, column); + + return column; + } + + /** + * Sets (or unsets) the rightmost frozen column in the grid. + * <p> + * All columns up to and including the given column will be frozen in place + * when the grid is scrolled sideways. + * + * @param lastFrozenColumn + * the rightmost column to freeze, or <code>null</code> to not + * have any columns frozen + * @throws IllegalArgumentException + * if {@code lastFrozenColumn} is not a column from this grid + */ + void setLastFrozenColumn(GridColumn lastFrozenColumn) { + /* + * TODO: If and when Grid supports column reordering or insertion of + * columns before other columns, make sure to mention that adding + * columns before lastFrozenColumn will change the frozen column count + */ + + if (lastFrozenColumn == null) { + getState().lastFrozenColumnId = null; + } else if (columns.containsValue(lastFrozenColumn)) { + getState().lastFrozenColumnId = lastFrozenColumn.getState().id; + } else { + throw new IllegalArgumentException( + "The given column isn't attached to this grid"); + } + } + + /** + * Sets (or unsets) the rightmost frozen column in the grid. + * <p> + * All columns up to and including the indicated property will be frozen in + * place when the grid is scrolled sideways. + * <p> + * <em>Note:</em> If the container used by this grid supports a propertyId + * <code>null</code>, it can never be defined as the last frozen column, as + * a <code>null</code> parameter will always reset the frozen columns in + * Grid. + * + * @param propertyId + * the property id corresponding to the column that should be the + * last frozen column, or <code>null</code> to not have any + * columns frozen. + * @throws IllegalArgumentException + * if {@code lastFrozenColumn} is not a column from this grid + */ + public void setLastFrozenPropertyId(Object propertyId) { + final GridColumn column; + if (propertyId == null) { + column = null; + } else { + column = getColumn(propertyId); + if (column == null) { + throw new IllegalArgumentException( + "property id does not exist."); + } + } + setLastFrozenColumn(column); + } + + /** + * Gets the rightmost frozen column in the grid. + * <p> + * <em>Note:</em> Most often, this method returns the very value set with + * {@link #setLastFrozenPropertyId(Object)}. This value, however, can be + * reset to <code>null</code> if the column is detached from this grid. + * + * @return the rightmost frozen column in the grid, or <code>null</code> if + * no columns are frozen. + */ + public Object getLastFrozenPropertyId() { + return columnKeys.get(getState().lastFrozenColumnId); + } + + /** + * Scrolls to a certain item, using {@link ScrollDestination#ANY}. + * + * @param itemId + * id of item to scroll to. + * @throws IllegalArgumentException + * if the provided id is not recognized by the data source. + */ + public void scrollToItem(Object itemId) throws IllegalArgumentException { + scrollToItem(itemId, ScrollDestination.ANY); + } + + /** + * Scrolls to a certain item, using user-specified scroll destination. + * + * @param itemId + * id of item to scroll to. + * @param destination + * value specifying desired position of scrolled-to row. + * @throws IllegalArgumentException + * if the provided id is not recognized by the data source. + */ + public void scrollToItem(Object itemId, ScrollDestination destination) + throws IllegalArgumentException { + + int row = datasource.indexOfId(itemId); + + if (row == -1) { + throw new IllegalArgumentException( + "Item with specified ID does not exist in data source"); + } + + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToRow(row, destination); + } + + /** + * Scrolls to the beginning of the first data row. + */ + public void scrollToStart() { + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToStart(); + } + + /** + * Scrolls to the end of the last data row. + */ + public void scrollToEnd() { + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToEnd(); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/GridColumn.java b/server/src/com/vaadin/ui/components/grid/GridColumn.java new file mode 100644 index 0000000000..852db21275 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/GridColumn.java @@ -0,0 +1,216 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.ui.components.grid; + +import java.io.Serializable; + +import com.vaadin.shared.ui.grid.GridColumnState; + +/** + * A column in the grid. Can be obtained by calling + * {@link Grid#getColumn(Object propertyId)}. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class GridColumn implements Serializable { + + /** + * The state of the column shared to the client + */ + private final GridColumnState state; + + /** + * The grid this column is associated with + */ + private final Grid grid; + + /** + * Internally used constructor. + * + * @param grid + * The grid this column belongs to. Should not be null. + * @param state + * the shared state of this column + */ + GridColumn(Grid grid, GridColumnState state) { + this.grid = grid; + this.state = state; + } + + /** + * Returns the serializable state of this column that is sent to the client + * side connector. + * + * @return the internal state of the column + */ + GridColumnState getState() { + return state; + } + + /** + * Returns the caption of the header. By default the header caption is the + * property id of the column. + * + * @return the text in the header + * + * @throws IllegalStateException + * if the column no longer is attached to the grid + */ + public String getHeaderCaption() throws IllegalStateException { + checkColumnIsAttached(); + return state.header; + } + + /** + * Sets the caption of the header. + * + * @param caption + * the text to show in the caption + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public void setHeaderCaption(String caption) throws IllegalStateException { + checkColumnIsAttached(); + state.header = caption; + grid.markAsDirty(); + } + + /** + * Returns the caption of the footer. By default the captions are + * <code>null</code>. + * + * @return the text in the footer + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public String getFooterCaption() throws IllegalStateException { + checkColumnIsAttached(); + return state.footer; + } + + /** + * Sets the caption of the footer. + * + * @param caption + * the text to show in the caption + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public void setFooterCaption(String caption) throws IllegalStateException { + checkColumnIsAttached(); + state.footer = caption; + grid.markAsDirty(); + } + + /** + * Returns the width (in pixels). By default a column is 100px wide. + * + * @return the width in pixels of the column + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public int getWidth() throws IllegalStateException { + checkColumnIsAttached(); + return state.width; + } + + /** + * Sets the width (in pixels). + * + * @param pixelWidth + * the new pixel width of the column + * @throws IllegalStateException + * if the column is no longer attached to any grid + * @throws IllegalArgumentException + * thrown if pixel width is less than zero + */ + public void setWidth(int pixelWidth) throws IllegalStateException, + IllegalArgumentException { + checkColumnIsAttached(); + if (pixelWidth < 0) { + throw new IllegalArgumentException( + "Pixel width should be greated than 0"); + } + state.width = pixelWidth; + grid.markAsDirty(); + } + + /** + * Marks the column width as undefined meaning that the grid is free to + * resize the column based on the cell contents and available space in the + * grid. + */ + public void setWidthUndefined() { + checkColumnIsAttached(); + state.width = -1; + grid.markAsDirty(); + } + + /** + * Is this column visible in the grid. By default all columns are visible. + * + * @return <code>true</code> if the column is visible + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public boolean isVisible() throws IllegalStateException { + checkColumnIsAttached(); + return state.visible; + } + + /** + * Set the visibility of this column + * + * @param visible + * is the column visible + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public void setVisible(boolean visible) throws IllegalStateException { + checkColumnIsAttached(); + state.visible = visible; + grid.markAsDirty(); + } + + /** + * Checks if column is attached and throws an {@link IllegalStateException} + * if it is not + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + protected void checkColumnIsAttached() throws IllegalStateException { + if (grid.getColumnByColumnId(state.id) == null) { + throw new IllegalStateException("Column no longer exists."); + } + } + + /** + * Sets this column as the last frozen column in its grid. + * + * @throws IllegalArgumentException + * if the column is no longer attached to any grid + * @see Grid#setLastFrozenColumn(GridColumn) + */ + public void setLastFrozenColumn() { + checkColumnIsAttached(); + grid.setLastFrozenColumn(this); + } +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridColumnGroups.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumnGroups.java new file mode 100644 index 0000000000..4350bf1a7b --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumnGroups.java @@ -0,0 +1,265 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.tests.server.component.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.KeyMapper; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.ui.components.grid.ColumnGroup; +import com.vaadin.ui.components.grid.ColumnGroupRow; +import com.vaadin.ui.components.grid.Grid; + +/** + * + * @since + * @author Vaadin Ltd + */ +public class GridColumnGroups { + + private Grid grid; + + private GridState state; + + private Method getStateMethod; + + private Field columnIdGeneratorField; + + private KeyMapper<Object> columnIdMapper; + + @Before + public void setup() throws Exception { + IndexedContainer ds = new IndexedContainer(); + for (int c = 0; c < 10; c++) { + ds.addContainerProperty("column" + c, String.class, ""); + } + grid = new Grid(ds); + + getStateMethod = Grid.class.getDeclaredMethod("getState"); + getStateMethod.setAccessible(true); + + state = (GridState) getStateMethod.invoke(grid); + + columnIdGeneratorField = Grid.class.getDeclaredField("columnKeys"); + columnIdGeneratorField.setAccessible(true); + + columnIdMapper = (KeyMapper<Object>) columnIdGeneratorField.get(grid); + } + + @Test + public void testColumnGroupRows() throws Exception { + + // No column group rows by default + List<ColumnGroupRow> rows = grid.getColumnGroupRows(); + assertEquals(0, rows.size()); + + // Add some rows + ColumnGroupRow row1 = grid.addColumnGroupRow(); + ColumnGroupRow row3 = grid.addColumnGroupRow(); + ColumnGroupRow row2 = grid.addColumnGroupRow(1); + + rows = grid.getColumnGroupRows(); + assertEquals(3, rows.size()); + assertEquals(row1, rows.get(0)); + assertEquals(row2, rows.get(1)); + assertEquals(row3, rows.get(2)); + + // Header should be visible by default, footer should not + assertTrue(row1.isHeaderVisible()); + assertFalse(row1.isFooterVisible()); + + row1.setHeaderVisible(false); + assertFalse(row1.isHeaderVisible()); + row1.setHeaderVisible(true); + assertTrue(row1.isHeaderVisible()); + + row1.setFooterVisible(true); + assertTrue(row1.isFooterVisible()); + row1.setFooterVisible(false); + assertFalse(row1.isFooterVisible()); + + row1.setHeaderVisible(true); + row1.setFooterVisible(true); + assertTrue(row1.isHeaderVisible()); + assertTrue(row1.isFooterVisible()); + + row1.setHeaderVisible(false); + row1.setFooterVisible(false); + assertFalse(row1.isHeaderVisible()); + assertFalse(row1.isFooterVisible()); + } + + @Test + public void testColumnGroupsInState() throws Exception { + + // Add a new row + ColumnGroupRow row = grid.addColumnGroupRow(); + assertTrue(state.columnGroupRows.size() == 1); + + // Add a group by property id + ColumnGroup columns12 = row.addGroup("column1", "column2"); + assertTrue(state.columnGroupRows.get(0).groups.size() == 1); + + // Set header of column + columns12.setHeaderCaption("Column12"); + assertEquals("Column12", + state.columnGroupRows.get(0).groups.get(0).header); + + // Set footer of column + columns12.setFooterCaption("Footer12"); + assertEquals("Footer12", + state.columnGroupRows.get(0).groups.get(0).footer); + + // Add another group by column instance + ColumnGroup columns34 = row.addGroup(grid.getColumn("column3"), + grid.getColumn("column4")); + assertTrue(state.columnGroupRows.get(0).groups.size() == 2); + + // add another group row + ColumnGroupRow row2 = grid.addColumnGroupRow(); + assertTrue(state.columnGroupRows.size() == 2); + + // add a group by combining the two previous groups + ColumnGroup columns1234 = row2.addGroup(columns12, columns34); + assertTrue(columns1234.getColumns().size() == 4); + + // Insert a group as the second group + ColumnGroupRow newRow2 = grid.addColumnGroupRow(1); + assertTrue(state.columnGroupRows.size() == 3); + } + + @Test + public void testAddingColumnGroups() throws Exception { + + ColumnGroupRow row = grid.addColumnGroupRow(); + + // By property id + ColumnGroup columns01 = row.addGroup("column0", "column1"); + assertEquals(2, columns01.getColumns().size()); + assertEquals("column0", columns01.getColumns().get(0)); + assertTrue(columns01.isColumnInGroup("column0")); + assertEquals("column1", columns01.getColumns().get(1)); + assertTrue(columns01.isColumnInGroup("column1")); + + // By grid column + ColumnGroup columns23 = row.addGroup(grid.getColumn("column2"), + grid.getColumn("column3")); + assertEquals(2, columns23.getColumns().size()); + assertEquals("column2", columns23.getColumns().get(0)); + assertTrue(columns23.isColumnInGroup("column2")); + assertEquals("column3", columns23.getColumns().get(1)); + assertTrue(columns23.isColumnInGroup("column3")); + + // Combine groups + ColumnGroupRow row2 = grid.addColumnGroupRow(); + ColumnGroup columns0123 = row2.addGroup(columns01, columns23); + assertEquals(4, columns0123.getColumns().size()); + assertEquals("column0", columns0123.getColumns().get(0)); + assertTrue(columns0123.isColumnInGroup("column0")); + assertEquals("column1", columns0123.getColumns().get(1)); + assertTrue(columns0123.isColumnInGroup("column1")); + assertEquals("column2", columns0123.getColumns().get(2)); + assertTrue(columns0123.isColumnInGroup("column2")); + assertEquals("column3", columns0123.getColumns().get(3)); + assertTrue(columns0123.isColumnInGroup("column3")); + } + + @Test + public void testColumnGroupHeadersAndFooters() throws Exception { + + ColumnGroupRow row = grid.addColumnGroupRow(); + ColumnGroup group = row.addGroup("column1", "column2"); + + // Header + assertNull(group.getHeaderCaption()); + group.setHeaderCaption("My header"); + assertEquals("My header", group.getHeaderCaption()); + group.setHeaderCaption(null); + assertNull(group.getHeaderCaption()); + + // Footer + assertNull(group.getFooterCaption()); + group.setFooterCaption("My footer"); + assertEquals("My footer", group.getFooterCaption()); + group.setFooterCaption(null); + assertNull(group.getFooterCaption()); + } + + @Test + public void testColumnGroupDetachment() throws Exception { + + ColumnGroupRow row = grid.addColumnGroupRow(); + ColumnGroup group = row.addGroup("column1", "column2"); + + // Remove group + row.removeGroup(group); + + try { + group.setHeaderCaption("Header"); + fail("Should throw exception for setting header caption on detached group"); + } catch (IllegalStateException ise) { + + } + + try { + group.setFooterCaption("Footer"); + fail("Should throw exception for setting footer caption on detached group"); + } catch (IllegalStateException ise) { + + } + } + + @Test + public void testColumnGroupLimits() throws Exception { + + ColumnGroupRow row = grid.addColumnGroupRow(); + row.addGroup("column1", "column2"); + row.addGroup("column3", "column4"); + + try { + row.addGroup("column2", "column3"); + fail("Adding a group with already grouped properties should throw exception"); + } catch (IllegalArgumentException iae) { + + } + + ColumnGroupRow row2 = grid.addColumnGroupRow(); + + try { + row2.addGroup("column2", "column3"); + fail("Adding a group that breaks previous grouping boundaries should throw exception"); + } catch (IllegalArgumentException iae) { + + } + + // This however should not throw an exception as it spans completely + // over the parent rows groups + row2.addGroup("column1", "column2", "column3", "column4"); + + } +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java new file mode 100644 index 0000000000..da07611b48 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java @@ -0,0 +1,228 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.tests.server.component.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.KeyMapper; +import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.GridColumn; + +public class GridColumns { + + private Grid grid; + + private GridState state; + + private Method getStateMethod; + + private Field columnIdGeneratorField; + + private KeyMapper<Object> columnIdMapper; + + @Before + public void setup() throws Exception { + IndexedContainer ds = new IndexedContainer(); + for (int c = 0; c < 10; c++) { + ds.addContainerProperty("column" + c, String.class, ""); + } + grid = new Grid(ds); + + getStateMethod = Grid.class.getDeclaredMethod("getState"); + getStateMethod.setAccessible(true); + + state = (GridState) getStateMethod.invoke(grid); + + columnIdGeneratorField = Grid.class.getDeclaredField("columnKeys"); + columnIdGeneratorField.setAccessible(true); + + columnIdMapper = (KeyMapper<Object>) columnIdGeneratorField.get(grid); + } + + @Test + public void testColumnGeneration() throws Exception { + + for (Object propertyId : grid.getContainerDatasource() + .getContainerPropertyIds()) { + + // All property ids should get a column + GridColumn column = grid.getColumn(propertyId); + assertNotNull(column); + + // Property id should be the column header by default + assertEquals(propertyId.toString(), column.getHeaderCaption()); + } + } + + @Test + public void testModifyingColumnProperties() throws Exception { + + // Modify first column + GridColumn column = grid.getColumn("column1"); + assertNotNull(column); + + column.setFooterCaption("CustomFooter"); + assertEquals("CustomFooter", column.getFooterCaption()); + assertEquals(column.getFooterCaption(), + getColumnState("column1").footer); + + column.setHeaderCaption("CustomHeader"); + assertEquals("CustomHeader", column.getHeaderCaption()); + assertEquals(column.getHeaderCaption(), + getColumnState("column1").header); + + column.setVisible(false); + assertFalse(column.isVisible()); + assertFalse(getColumnState("column1").visible); + + column.setVisible(true); + assertTrue(column.isVisible()); + assertTrue(getColumnState("column1").visible); + + column.setWidth(100); + assertEquals(100, column.getWidth()); + assertEquals(column.getWidth(), getColumnState("column1").width); + + try { + column.setWidth(-1); + fail("Setting width to -1 should throw exception"); + } catch (IllegalArgumentException iae) { + + } + + assertEquals(100, column.getWidth()); + assertEquals(100, getColumnState("column1").width); + } + + @Test + public void testRemovingColumn() throws Exception { + + GridColumn column = grid.getColumn("column1"); + assertNotNull(column); + + // Remove column + grid.getContainerDatasource().removeContainerProperty("column1"); + + try { + column.setHeaderCaption("asd"); + + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + try { + column.setFooterCaption("asd"); + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + try { + column.setVisible(false); + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + try { + column.setWidth(123); + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + assertNull(grid.getColumn("column1")); + assertNull(getColumnState("column1")); + } + + @Test + public void testAddingColumn() throws Exception { + grid.getContainerDatasource().addContainerProperty("columnX", + String.class, ""); + GridColumn column = grid.getColumn("columnX"); + assertNotNull(column); + } + + @Test + public void testHeaderVisiblility() throws Exception { + + assertTrue(grid.isColumnHeadersVisible()); + assertTrue(state.columnHeadersVisible); + + grid.setColumnHeadersVisible(false); + assertFalse(grid.isColumnHeadersVisible()); + assertFalse(state.columnHeadersVisible); + + grid.setColumnHeadersVisible(true); + assertTrue(grid.isColumnHeadersVisible()); + assertTrue(state.columnHeadersVisible); + } + + @Test + public void testFooterVisibility() throws Exception { + + assertFalse(grid.isColumnFootersVisible()); + assertFalse(state.columnFootersVisible); + + grid.setColumnFootersVisible(false); + assertFalse(grid.isColumnFootersVisible()); + assertFalse(state.columnFootersVisible); + + grid.setColumnFootersVisible(true); + assertTrue(grid.isColumnFootersVisible()); + assertTrue(state.columnFootersVisible); + } + + @Test + public void testFrozenColumnByPropertyId() { + assertNull("Grid should not start with a frozen column", + grid.getLastFrozenPropertyId()); + + Object propertyId = grid.getContainerDatasource() + .getContainerPropertyIds().iterator().next(); + grid.setLastFrozenPropertyId(propertyId); + assertEquals(propertyId, grid.getLastFrozenPropertyId()); + + grid.getContainerDatasource().removeContainerProperty(propertyId); + assertNull(grid.getLastFrozenPropertyId()); + } + + private GridColumnState getColumnState(Object propertyId) { + String columnId = columnIdMapper.key(propertyId); + for (GridColumnState columnState : state.columns) { + if (columnState.id.equals(columnId)) { + return columnState; + } + } + return null; + } + +} diff --git a/shared/src/com/vaadin/shared/data/DataProviderRpc.java b/shared/src/com/vaadin/shared/data/DataProviderRpc.java new file mode 100644 index 0000000000..79e3f17f8d --- /dev/null +++ b/shared/src/com/vaadin/shared/data/DataProviderRpc.java @@ -0,0 +1,61 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.shared.data; + +import java.util.List; + +import com.vaadin.shared.communication.ClientRpc; + +/** + * RPC interface used for pushing container data to the client. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface DataProviderRpc extends ClientRpc { + + /** + * Sends updated row data to a client. + * + * @param firstRowIndex + * the index of the first updated row + * @param rowData + * the updated row data + */ + public void setRowData(int firstRowIndex, List<String[]> rowData); + + /** + * Informs the client to remove row data. + * + * @param firstRowIndex + * the index of the first removed row + * @param count + * the number of rows removed from <code>firstRowIndex</code> and + * onwards + */ + public void removeRowData(int firstRowIndex, int count); + + /** + * Informs the client to insert new row data. + * + * @param firstRowIndex + * the index of the first new row + * @param count + * the number of rows inserted at <code>firstRowIndex</code> + */ + public void insertRowData(int firstRowIndex, int count); +} diff --git a/shared/src/com/vaadin/shared/data/DataProviderState.java b/shared/src/com/vaadin/shared/data/DataProviderState.java new file mode 100644 index 0000000000..2eabe0b0e1 --- /dev/null +++ b/shared/src/com/vaadin/shared/data/DataProviderState.java @@ -0,0 +1,32 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.shared.data; + +import com.vaadin.shared.communication.SharedState; + +/** + * Shared state used by client-side data sources. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class DataProviderState extends SharedState { + /** + * The size of the container. + */ + public int containerSize; +} diff --git a/shared/src/com/vaadin/shared/data/DataRequestRpc.java b/shared/src/com/vaadin/shared/data/DataRequestRpc.java new file mode 100644 index 0000000000..eaf17df8f6 --- /dev/null +++ b/shared/src/com/vaadin/shared/data/DataRequestRpc.java @@ -0,0 +1,38 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.shared.data; + +import com.vaadin.shared.communication.ServerRpc; + +/** + * RPC interface used for requesting container data to the client. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface DataRequestRpc extends ServerRpc { + + /** + * Request rows from the server. + * + * @param firstRowIndex + * the index of the first requested row + * @param numberOfRows + * the number of requested rows + */ + public void requestRows(int firstRowIndex, int numberOfRows); +} diff --git a/shared/src/com/vaadin/shared/ui/grid/ColumnGroupRowState.java b/shared/src/com/vaadin/shared/ui/grid/ColumnGroupRowState.java new file mode 100644 index 0000000000..a8e0f87457 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/ColumnGroupRowState.java @@ -0,0 +1,46 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.shared.ui.grid; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * The column group row data shared between the server and client + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class ColumnGroupRowState implements Serializable { + + /** + * The groups that has been added to the row + */ + public List<ColumnGroupState> groups = new ArrayList<ColumnGroupState>(); + + /** + * Is the header shown + */ + public boolean headerVisible = true; + + /** + * Is the footer shown + */ + public boolean footerVisible = false; + +} diff --git a/shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java b/shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java new file mode 100644 index 0000000000..3992b6611f --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.shared.ui.grid; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * The column group data shared between the server and the client + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class ColumnGroupState implements Serializable { + + /** + * The columns that is included in the group + */ + public List<String> columns = new ArrayList<String>(); + + /** + * The header text of the group + */ + public String header; + + /** + * The footer text of the group + */ + public String footer; +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java new file mode 100644 index 0000000000..00cc93d371 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java @@ -0,0 +1,53 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.shared.ui.grid; + +import com.vaadin.shared.communication.ClientRpc; + +/** + * Server-to-client RPC interface for the Grid component. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface GridClientRpc extends ClientRpc { + + /** + * Command client Grid to scroll to a specific data row. + * + * @param row + * zero-based row index. If the row index is below zero or above + * the row count of the client-side data source, a client-side + * exception will be triggered. Since this exception has no + * handling by default, an out-of-bounds value will cause a + * client-side crash. + * @param destination + * desired placement of scrolled-to row. See the documentation + * for {@link ScrollDestination} for more information. + */ + public void scrollToRow(int row, ScrollDestination destination); + + /** + * Command client Grid to scroll to the first row. + */ + public void scrollToStart(); + + /** + * Command client Grid to scroll to the last row. + */ + public void scrollToEnd(); + +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java b/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java new file mode 100644 index 0000000000..0301c5ead2 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.shared.ui.grid; + +import java.io.Serializable; + +/** + * Column state DTO for transferring column properties from the server to the + * client + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class GridColumnState implements Serializable { + + /** + * Id used by grid connector to map server side column with client side + * column + */ + public String id; + + /** + * Header caption for the column + */ + public String header; + + /** + * Footer caption for the column + */ + public String footer; + + /** + * Has the column been hidden. By default the column is visible. + */ + public boolean visible = true; + + /** + * Column width in pixels. Default column width is 100px. + */ + public int width = 100; + +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridConstants.java b/shared/src/com/vaadin/shared/ui/grid/GridConstants.java new file mode 100644 index 0000000000..5b88fad5a8 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridConstants.java @@ -0,0 +1,33 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.shared.ui.grid; + +/** + * Container class for common constants and default values used by the Grid + * component. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public final class GridConstants { + + /** + * Default padding in pixels when scrolling programmatically, without an + * explicitly defined padding value. + */ + public static final int DEFAULT_PADDING = 0; + +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java new file mode 100644 index 0000000000..db0a31ed2c --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.shared.ui.grid; + +import com.vaadin.shared.communication.ServerRpc; + +/** + * TODO + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface GridServerRpc extends ServerRpc { + + /** + * TODO + * + * @param firstVisibleRow + * the index of the first visible row + * @param visibleRowCount + * the number of rows visible, counted from + * <code>firstVisibleRow</code> + */ + void setVisibleRows(int firstVisibleRow, int visibleRowCount); + +} diff --git a/shared/src/com/vaadin/shared/ui/grid/GridState.java b/shared/src/com/vaadin/shared/ui/grid/GridState.java new file mode 100644 index 0000000000..93e602a539 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridState.java @@ -0,0 +1,64 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.shared.ui.grid; + +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.shared.AbstractComponentState; + +/** + * The shared state for the {@link com.vaadin.ui.components.grid.Grid} component + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class GridState extends AbstractComponentState { + { + // FIXME Grid currently does not support undefined size + width = "400px"; + height = "400px"; + } + + /** + * Columns in grid. Column order implicitly deferred from list order. + */ + public List<GridColumnState> columns = new ArrayList<GridColumnState>(); + + /** + * Is the column header row visible + */ + public boolean columnHeadersVisible = true; + + /** + * Is the column footer row visible + */ + public boolean columnFootersVisible = false; + + /** + * The column groups added to the grid + */ + public List<ColumnGroupRowState> columnGroupRows = new ArrayList<ColumnGroupRowState>(); + + /** + * The id for the last frozen column. + * + * @see GridColumnState#id + */ + public String lastFrozenColumnId = null; + +} diff --git a/shared/src/com/vaadin/shared/ui/grid/Range.java b/shared/src/com/vaadin/shared/ui/grid/Range.java new file mode 100644 index 0000000000..3114a79c82 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/Range.java @@ -0,0 +1,378 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.shared.ui.grid; + +/** + * An immutable representation of a range, marked by start and end points. + * <p> + * The range is treated as inclusive at the start, and exclusive at the end. + * I.e. the range [0..1[ has the length 1, and represents one integer: 0. + * <p> + * The range is considered {@link #isEmpty() empty} if the start is the same as + * the end. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public final class Range { + private final int start; + private final int end; + + /** + * Creates a range object representing a single integer. + * + * @param integer + * the number to represent as a range + * @return the range represented by <code>integer</code> + */ + public static Range withOnly(final int integer) { + return new Range(integer, integer + 1); + } + + /** + * Creates a range between two integers. + * <p> + * The range start is <em>inclusive</em> and the end is <em>exclusive</em>. + * So, a range "between" 0 and 5 represents the numbers 0, 1, 2, 3 and 4, + * but not 5. + * + * @param start + * the start of the the range, inclusive + * @param end + * the end of the range, exclusive + * @return a range representing <code>[start..end[</code> + * @throws IllegalArgumentException + * if <code>start > end</code> + */ + public static Range between(final int start, final int end) + throws IllegalArgumentException { + return new Range(start, end); + } + + /** + * Creates a range from a start point, with a given length. + * + * @param start + * the first integer to include in the range + * @param length + * the length of the resulting range + * @return a range starting from <code>start</code>, with + * <code>length</code> number of integers following + * @throws IllegalArgumentException + * if length < 0 + */ + public static Range withLength(final int start, final int length) + throws IllegalArgumentException { + if (length < 0) { + /* + * The constructor of Range will throw an exception if start > + * start+length (i.e. if length is negative). We're throwing the + * same exception type, just with a more descriptive message. + */ + throw new IllegalArgumentException("length must not be negative"); + } + return new Range(start, start + length); + } + + /** + * Creates a new range between two numbers: <code>[start..end[</code>. + * + * @param start + * the start integer, inclusive + * @param end + * the end integer, exclusive + * @throws IllegalArgumentException + * if <code>start > end</code> + */ + private Range(final int start, final int end) + throws IllegalArgumentException { + if (start > end) { + throw new IllegalArgumentException( + "start must not be greater than end"); + } + + this.start = start; + this.end = end; + } + + /** + * Returns the <em>inclusive</em> start point of this range. + * + * @return the start point of this range + */ + public int getStart() { + return start; + } + + /** + * Returns the <em>exclusive</em> end point of this range. + * + * @return the end point of this range + */ + public int getEnd() { + return end; + } + + /** + * The number of integers contained in the range. + * + * @return the number of integers contained in the range + */ + public int length() { + return getEnd() - getStart(); + } + + /** + * Checks whether the range has no elements between the start and end. + * + * @return <code>true</code> iff the range contains no elements. + */ + public boolean isEmpty() { + return getStart() >= getEnd(); + } + + /** + * Checks whether this range and another range are at least partially + * covering the same values. + * + * @param other + * the other range to check against + * @return <code>true</code> if this and <code>other</code> intersect + */ + public boolean intersects(final Range other) { + return getStart() < other.getEnd() && other.getStart() < getEnd(); + } + + /** + * Checks whether an integer is found within this range. + * + * @param integer + * an integer to test for presence in this range + * @return <code>true</code> iff <code>integer</code> is in this range + */ + public boolean contains(final int integer) { + return getStart() <= integer && integer < getEnd(); + } + + /** + * Checks whether this range is a subset of another range. + * + * @return <code>true</code> iff <code>other</code> completely wraps this + * range + */ + public boolean isSubsetOf(final Range other) { + return other.getStart() <= getStart() && getEnd() <= other.getEnd(); + } + + /** + * Overlay this range with another one, and partition the ranges according + * to how they position relative to each other. + * <p> + * The three partitions are returned as a three-element Range array: + * <ul> + * <li>Elements in this range that occur before elements in + * <code>other</code>. + * <li>Elements that are shared between the two ranges. + * <li>Elements in this range that occur after elements in + * <code>other</code>. + * </ul> + * + * @param other + * the other range to act as delimiters. + * @return a three-element Range array of partitions depicting the elements + * before (index 0), shared/inside (index 1) and after (index 2). + */ + public Range[] partitionWith(final Range other) { + final Range[] splitBefore = splitAt(other.getStart()); + final Range rangeBefore = splitBefore[0]; + final Range[] splitAfter = splitBefore[1].splitAt(other.getEnd()); + final Range rangeInside = splitAfter[0]; + final Range rangeAfter = splitAfter[1]; + return new Range[] { rangeBefore, rangeInside, rangeAfter }; + } + + /** + * Get a range that is based on this one, but offset by a number. + * + * @param offset + * the number to offset by + * @return a copy of this range, offset by <code>offset</code> + */ + public Range offsetBy(final int offset) { + if (offset == 0) { + return this; + } else { + return new Range(start + offset, end + offset); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + getStart() + ".." + getEnd() + + "[" + (isEmpty() ? " (empty)" : ""); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + end; + result = prime * result + start; + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Range other = (Range) obj; + if (end != other.end) { + return false; + } + if (start != other.start) { + return false; + } + return true; + } + + /** + * Checks whether this range starts before the start of another range. + * + * @param other + * the other range to compare against + * @return <code>true</code> iff this range starts before the + * <code>other</code> + */ + public boolean startsBefore(final Range other) { + return getStart() < other.getStart(); + } + + /** + * Checks whether this range ends before the start of another range. + * + * @param other + * the other range to compare against + * @return <code>true</code> iff this range ends before the + * <code>other</code> + */ + public boolean endsBefore(final Range other) { + return getEnd() <= other.getStart(); + } + + /** + * Checks whether this range ends after the end of another range. + * + * @param other + * the other range to compare against + * @return <code>true</code> iff this range ends after the + * <code>other</code> + */ + public boolean endsAfter(final Range other) { + return getEnd() > other.getEnd(); + } + + /** + * Checks whether this range starts after the end of another range. + * + * @param other + * the other range to compare against + * @return <code>true</code> iff this range starts after the + * <code>other</code> + */ + public boolean startsAfter(final Range other) { + return getStart() >= other.getEnd(); + } + + /** + * Split the range into two at a certain integer. + * <p> + * <em>Example:</em> <code>[5..10[.splitAt(7) == [5..7[, [7..10[</code> + * + * @param integer + * the integer at which to split the range into two + * @return an array of two ranges, with <code>[start..integer[</code> in the + * first element, and <code>[integer..end[</code> in the second + * element. + * <p> + * If {@code integer} is less than {@code start}, [empty, + * {@code this} ] is returned. if <code>integer</code> is equal to + * or greater than {@code end}, [{@code this}, empty] is returned + * instead. + */ + public Range[] splitAt(final int integer) { + if (integer < start) { + return new Range[] { Range.withLength(start, 0), this }; + } else if (integer >= end) { + return new Range[] { this, Range.withLength(end, 0) }; + } else { + return new Range[] { new Range(start, integer), + new Range(integer, end) }; + } + } + + /** + * Split the range into two after a certain number of integers into the + * range. + * <p> + * Calling this method is equivalent to calling + * <code>{@link #splitAt(int) splitAt}({@link #getStart()}+length);</code> + * <p> + * <em>Example:</em> + * <code>[5..10[.splitAtFromStart(2) == [5..7[, [7..10[</code> + * + * @param length + * the length at which to split this range into two + * @return an array of two ranges, having the <code>length</code>-first + * elements of this range, and the second range having the rest. If + * <code>length</code> ≤ 0, the first element will be empty, and + * the second element will be this range. If <code>length</code> + * ≥ {@link #length()}, the first element will be this range, + * and the second element will be empty. + */ + public Range[] splitAtFromStart(final int length) { + return splitAt(getStart() + length); + } + + /** + * Combines two ranges to create a range containing all values in both + * ranges, provided there are no gaps between the ranges. + * + * @param other + * the range to combine with this range + * + * @return the combined range + * + * @throws IllegalArgumentException + * if the two ranges aren't connected + */ + public Range combineWith(Range other) throws IllegalArgumentException { + if (getStart() > other.getEnd() || other.getStart() > getEnd()) { + throw new IllegalArgumentException("There is a gap between " + this + + " and " + other); + } + + return Range.between(Math.min(getStart(), other.getStart()), + Math.max(getEnd(), other.getEnd())); + } +} diff --git a/shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java b/shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java new file mode 100644 index 0000000000..decc2fab5f --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.shared.ui.grid; + +/** + * Enumeration, specifying the destinations that are supported when scrolling + * rows or columns into view. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public enum ScrollDestination { + + /** + * Scroll as little as possible to show the target element. If the element + * fits into view, this works as START or END depending on the current + * scroll position. If the element does not fit into view, this works as + * START. + */ + ANY, + + /** + * Scrolls so that the element is shown at the start of the viewport. The + * viewport will, however, not scroll beyond its contents. + */ + START, + + /** + * Scrolls so that the element is shown in the middle of the viewport. The + * viewport will, however, not scroll beyond its contents, given more + * elements than what the viewport is able to show at once. Under no + * circumstances will the viewport scroll before its first element. + */ + MIDDLE, + + /** + * Scrolls so that the element is shown at the end of the viewport. The + * viewport will, however, not scroll before its first element. + */ + END + +} diff --git a/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java b/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java new file mode 100644 index 0000000000..b042cee509 --- /dev/null +++ b/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java @@ -0,0 +1,318 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.shared.ui.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +@SuppressWarnings("static-method") +public class RangeTest { + + @Test(expected = IllegalArgumentException.class) + public void startAfterEndTest() { + Range.between(10, 9); + } + + @Test(expected = IllegalArgumentException.class) + public void negativeLengthTest() { + Range.withLength(10, -1); + } + + @Test + public void constructorEquivalenceTest() { + assertEquals("10 == [10,11[", Range.withOnly(10), Range.between(10, 11)); + assertEquals("[10,20[ == 10, length 10", Range.between(10, 20), + Range.withLength(10, 10)); + assertEquals("10 == 10, length 1", Range.withOnly(10), + Range.withLength(10, 1)); + } + + @Test + public void boundsTest() { + { + final Range range = Range.between(0, 10); + assertEquals("between(0, 10) start", 0, range.getStart()); + assertEquals("between(0, 10) end", 10, range.getEnd()); + } + + { + final Range single = Range.withOnly(10); + assertEquals("withOnly(10) start", 10, single.getStart()); + assertEquals("withOnly(10) end", 11, single.getEnd()); + } + + { + final Range length = Range.withLength(10, 5); + assertEquals("withLength(10, 5) start", 10, length.getStart()); + assertEquals("withLength(10, 5) end", 15, length.getEnd()); + } + } + + @Test + @SuppressWarnings("boxing") + public void equalsTest() { + final Range range1 = Range.between(0, 10); + final Range range2 = Range.withLength(0, 11); + + assertTrue("null", !range1.equals(null)); + assertTrue("reflexive", range1.equals(range1)); + assertEquals("symmetric", range1.equals(range2), range2.equals(range1)); + } + + @Test + public void containsTest() { + final int start = 0; + final int end = 10; + final Range range = Range.between(start, end); + + assertTrue("start should be contained", range.contains(start)); + assertTrue("start-1 should not be contained", + !range.contains(start - 1)); + assertTrue("end should not be contained", !range.contains(end)); + assertTrue("end-1 should be contained", range.contains(end - 1)); + + assertTrue("[0..10[ contains 5", Range.between(0, 10).contains(5)); + assertTrue("empty range does not contain 5", !Range.between(5, 5) + .contains(5)); + } + + @Test + public void emptyTest() { + assertTrue("[0..0[ should be empty", Range.between(0, 0).isEmpty()); + assertTrue("Range of length 0 should be empty", Range.withLength(0, 0) + .isEmpty()); + + assertTrue("[0..1[ should not be empty", !Range.between(0, 1).isEmpty()); + assertTrue("Range of length 1 should not be empty", + !Range.withLength(0, 1).isEmpty()); + } + + @Test + public void splitTest() { + final Range startRange = Range.between(0, 10); + final Range[] splitRanges = startRange.splitAt(5); + assertEquals("[0..10[ split at 5, lower", Range.between(0, 5), + splitRanges[0]); + assertEquals("[0..10[ split at 5, upper", Range.between(5, 10), + splitRanges[1]); + } + + @Test + public void split_valueBefore() { + Range range = Range.between(10, 20); + Range[] splitRanges = range.splitAt(5); + + assertEquals(Range.between(10, 10), splitRanges[0]); + assertEquals(range, splitRanges[1]); + } + + @Test + public void split_valueAfter() { + Range range = Range.between(10, 20); + Range[] splitRanges = range.splitAt(25); + + assertEquals(range, splitRanges[0]); + assertEquals(Range.between(20, 20), splitRanges[1]); + } + + @Test + public void emptySplitTest() { + final Range range = Range.between(5, 10); + final Range[] split1 = range.splitAt(0); + assertTrue("split1, [0]", split1[0].isEmpty()); + assertEquals("split1, [1]", range, split1[1]); + + final Range[] split2 = range.splitAt(15); + assertEquals("split2, [0]", range, split2[0]); + assertTrue("split2, [1]", split2[1].isEmpty()); + } + + @Test + public void lengthTest() { + assertEquals("withLength length", 5, Range.withLength(10, 5).length()); + assertEquals("between length", 5, Range.between(10, 15).length()); + assertEquals("withOnly 10 length", 1, Range.withOnly(10).length()); + } + + @Test + public void intersectsTest() { + assertTrue("[0..10[ intersects [5..15[", Range.between(0, 10) + .intersects(Range.between(5, 15))); + assertTrue("[0..10[ does not intersect [10..20[", !Range.between(0, 10) + .intersects(Range.between(10, 20))); + } + + @Test + public void intersects_emptyInside() { + assertTrue("[5..5[ does intersect with [0..10[", Range.between(5, 5) + .intersects(Range.between(0, 10))); + assertTrue("[0..10[ does intersect with [5..5[", Range.between(0, 10) + .intersects(Range.between(5, 5))); + } + + @Test + public void intersects_emptyOutside() { + assertTrue("[15..15[ does not intersect with [0..10[", + !Range.between(15, 15).intersects(Range.between(0, 10))); + assertTrue("[0..10[ does not intersect with [15..15[", + !Range.between(0, 10).intersects(Range.between(15, 15))); + } + + @Test + public void subsetTest() { + assertTrue("[5..10[ is subset of [0..20[", Range.between(5, 10) + .isSubsetOf(Range.between(0, 20))); + + final Range range = Range.between(0, 10); + assertTrue("range is subset of self", range.isSubsetOf(range)); + + assertTrue("[0..10[ is not subset of [5..15[", !Range.between(0, 10) + .isSubsetOf(Range.between(5, 15))); + } + + @Test + public void offsetTest() { + assertEquals(Range.between(5, 15), Range.between(0, 10).offsetBy(5)); + } + + @Test + public void rangeStartsBeforeTest() { + final Range former = Range.between(0, 5); + final Range latter = Range.between(1, 5); + assertTrue("former should starts before latter", + former.startsBefore(latter)); + assertTrue("latter shouldn't start before latter", + !latter.startsBefore(former)); + + assertTrue("no overlap allowed", + !Range.between(0, 5).startsBefore(Range.between(0, 10))); + } + + @Test + public void rangeStartsAfterTest() { + final Range former = Range.between(0, 5); + final Range latter = Range.between(5, 10); + assertTrue("latter should start after former", + latter.startsAfter(former)); + assertTrue("former shouldn't start after latter", + !former.startsAfter(latter)); + + assertTrue("no overlap allowed", + !Range.between(5, 10).startsAfter(Range.between(0, 6))); + } + + @Test + public void rangeEndsBeforeTest() { + final Range former = Range.between(0, 5); + final Range latter = Range.between(5, 10); + assertTrue("latter should end before former", former.endsBefore(latter)); + assertTrue("former shouldn't end before latter", + !latter.endsBefore(former)); + + assertTrue("no overlap allowed", + !Range.between(5, 10).endsBefore(Range.between(9, 15))); + } + + @Test + public void rangeEndsAfterTest() { + final Range former = Range.between(1, 5); + final Range latter = Range.between(1, 6); + assertTrue("latter should end after former", latter.endsAfter(former)); + assertTrue("former shouldn't end after latter", + !former.endsAfter(latter)); + + assertTrue("no overlap allowed", + !Range.between(0, 10).endsAfter(Range.between(5, 10))); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_notOverlappingFirstSmaller() { + Range.between(0, 10).combineWith(Range.between(11, 20)); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_notOverlappingSecondLarger() { + Range.between(11, 20).combineWith(Range.between(0, 10)); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_firstEmptyNotOverlapping() { + Range.between(15, 15).combineWith(Range.between(0, 10)); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_secondEmptyNotOverlapping() { + Range.between(0, 10).combineWith(Range.between(15, 15)); + } + + @Test + public void combine_barelyOverlapping() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(10, 20); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(0, combined1.getStart()); + assertEquals(20, combined1.getEnd()); + } + + @Test + public void combine_subRange() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(2, 8); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(r1, combined1); + } + + @Test + public void combine_intersecting() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(5, 15); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(0, combined1.getStart()); + assertEquals(15, combined1.getEnd()); + + } + + @Test + public void combine_emptyInside() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(5, 5); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(r1, combined1); + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/BasicEscalator.html b/uitest/src/com/vaadin/tests/components/grid/BasicEscalator.html new file mode 100644 index 0000000000..70aa0fe195 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/BasicEscalator.html @@ -0,0 +1,176 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head profile="http://selenium-ide.openqa.org/profiles/test-case"> +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +<link rel="selenium.base" href="http://localhost:8888/" /> +<title>BasicEscalator</title> +</head> +<body> +<table cellpadding="1" cellspacing="1" border="1"> +<thead> +<tr><td rowspan="1" colspan="3">BasicEscalator</td></tr> +</thead><tbody> +<tr> + <td>open</td> + <td>/run/com.vaadin.tests.components.grid.BasicEscalator?restartApplication</td> + <td></td> +</tr> +<tr> + <td>verifyText</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]</td> + <td>Row 0: 0,0 (0)</td> +</tr> +<tr> + <td>verifyText</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[17]/domChild[9]</td> + <td>Cell: 9,17 (179)</td> +</tr> +<tr> + <td>verifyTextNotPresent</td> + <td>Cell: 0,100</td> + <td></td> +</tr> +<tr> + <td>verifyTextNotPresent</td> + <td>Cell: 0,101</td> + <td></td> +</tr> +<tr> + <td>type</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td> + <td>0</td> +</tr> +<tr> + <td>type</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[1]/VTextField[0]</td> + <td>1</td> +</tr> +<tr> + <td>click</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]</td> + <td></td> +</tr> +<tr> + <td>verifyText</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[18]/domChild[0]</td> + <td>Row 0: 0,100 (190)</td> +</tr> +<tr> + <td>type</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td> + <td>11</td> +</tr> +<tr> + <td>click</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]</td> + <td></td> +</tr> +<tr> + <td>verifyText</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[17]/domChild[0]</td> + <td>Row 11: 0,101 (200)</td> +</tr> +<tr> + <td>type</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td> + <td>0</td> +</tr> +<tr> + <td>type</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[1]/VTextField[0]</td> + <td>100</td> +</tr> +<tr> + <td>click</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]</td> + <td></td> +</tr> +<tr> + <td>verifyText</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[16]/domChild[0]</td> + <td>Row 0: 0,102 (210)</td> +</tr> +<tr> + <td>verifyText</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[1]/domChild[0]</td> + <td>Row 16: 0,118 (370)</td> +</tr> +<tr> + <td>scroll</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[0]</td> + <td>1109</td> +</tr> +<tr> + <td>verifyTextPresent</td> + <td>Row 56: 0,158</td> + <td></td> +</tr> +<tr> + <td>verifyTextPresent</td> + <td>Row 72: 0,174</td> + <td></td> +</tr> +<tr> + <td>scroll</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[0]</td> + <td>3690</td> +</tr> +<tr> + <td>verifyTextPresent</td> + <td>Row 201: 0,99</td> + <td></td> +</tr> +<tr> + <td>type</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[2]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td> + <td>201</td> +</tr> +<tr> + <td>type</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[2]/VHorizontalLayout[0]/Slot[1]/VTextField[0]</td> + <td>1</td> +</tr> +<tr> + <td>click</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[2]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]</td> + <td></td> +</tr> +<tr> + <td>verifyText</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[1]/domChild[0]</td> + <td>Row 200: 0,98 (960)</td> +</tr> +<tr> + <td>verifyTextNotPresent</td> + <td>Row 201:</td> + <td></td> +</tr> +<tr> + <td>type</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[3]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td> + <td>0</td> +</tr> +<tr> + <td>type</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[3]/VHorizontalLayout[0]/Slot[1]/VTextField[0]</td> + <td>2</td> +</tr> +<tr> + <td>click</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[3]/VHorizontalLayout[0]/Slot[2]/VButton[0]</td> + <td></td> +</tr> +<tr> + <td>verifyText</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[16]/domChild[0]</td> + <td>Row 184: 10,82 (974)</td> +</tr> +<tr> + <td>verifyText</td> + <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[1]/domChild[0]</td> + <td>Row 200: 10,98 (1006)</td> +</tr> +</tbody></table> +</body> +</html> diff --git a/uitest/src/com/vaadin/tests/components/grid/BasicEscalator.java b/uitest/src/com/vaadin/tests/components/grid/BasicEscalator.java new file mode 100644 index 0000000000..fc9c217328 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/BasicEscalator.java @@ -0,0 +1,304 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.tests.components.grid; + +import java.util.Random; + +import com.vaadin.annotations.Widgetset; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.tests.widgetset.TestingWidgetSet; +import com.vaadin.tests.widgetset.server.grid.TestGrid; +import com.vaadin.ui.Button; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Layout; +import com.vaadin.ui.NativeSelect; +import com.vaadin.ui.TextField; + +/** + * @since 7.2 + * @author Vaadin Ltd + */ +@Widgetset(TestingWidgetSet.NAME) +public class BasicEscalator extends AbstractTestUI { + public static final String ESCALATOR = "escalator"; + public static final String INSERT_ROWS_OFFSET = "iro"; + public static final String INSERT_ROWS_AMOUNT = "ira"; + public static final String INSERT_ROWS_BUTTON = "irb"; + + private final Random random = new Random(); + + @Override + protected void setup(final VaadinRequest request) { + final TestGrid grid = new TestGrid(); + grid.setId(ESCALATOR); + addComponent(grid); + + final Layout insertRowsLayout = new HorizontalLayout(); + final TextField insertRowsOffset = new TextField(); + insertRowsOffset.setId(INSERT_ROWS_OFFSET); + insertRowsLayout.addComponent(insertRowsOffset); + final TextField insertRowsAmount = new TextField(); + insertRowsAmount.setId(INSERT_ROWS_AMOUNT); + insertRowsLayout.addComponent(insertRowsAmount); + insertRowsLayout.addComponent(new Button("insert rows", + new Button.ClickListener() { + @Override + public void buttonClick(final ClickEvent event) { + final int offset = Integer.parseInt(insertRowsOffset + .getValue()); + final int amount = Integer.parseInt(insertRowsAmount + .getValue()); + grid.insertRows(offset, amount); + } + }) { + { + setId(INSERT_ROWS_BUTTON); + } + }); + addComponent(insertRowsLayout); + + final Layout removeRowsLayout = new HorizontalLayout(); + final TextField removeRowsOffset = new TextField(); + removeRowsLayout.addComponent(removeRowsOffset); + final TextField removeRowsAmount = new TextField(); + removeRowsLayout.addComponent(removeRowsAmount); + removeRowsLayout.addComponent(new Button("remove rows", + new Button.ClickListener() { + @Override + public void buttonClick(final ClickEvent event) { + final int offset = Integer.parseInt(removeRowsOffset + .getValue()); + final int amount = Integer.parseInt(removeRowsAmount + .getValue()); + grid.removeRows(offset, amount); + } + })); + addComponent(removeRowsLayout); + + final Layout insertColumnsLayout = new HorizontalLayout(); + final TextField insertColumnsOffset = new TextField(); + insertColumnsLayout.addComponent(insertColumnsOffset); + final TextField insertColumnsAmount = new TextField(); + insertColumnsLayout.addComponent(insertColumnsAmount); + insertColumnsLayout.addComponent(new Button("insert columns", + new Button.ClickListener() { + @Override + public void buttonClick(final ClickEvent event) { + final int offset = Integer.parseInt(insertColumnsOffset + .getValue()); + final int amount = Integer.parseInt(insertColumnsAmount + .getValue()); + grid.insertColumns(offset, amount); + } + })); + addComponent(insertColumnsLayout); + + final Layout removeColumnsLayout = new HorizontalLayout(); + final TextField removeColumnsOffset = new TextField(); + removeColumnsLayout.addComponent(removeColumnsOffset); + final TextField removeColumnsAmount = new TextField(); + removeColumnsLayout.addComponent(removeColumnsAmount); + removeColumnsLayout.addComponent(new Button("remove columns", + new Button.ClickListener() { + @Override + public void buttonClick(final ClickEvent event) { + final int offset = Integer.parseInt(removeColumnsOffset + .getValue()); + final int amount = Integer.parseInt(removeColumnsAmount + .getValue()); + grid.removeColumns(offset, amount); + } + })); + addComponent(removeColumnsLayout); + + final HorizontalLayout rowScroll = new HorizontalLayout(); + final NativeSelect destination = new NativeSelect(); + destination.setNullSelectionAllowed(false); + destination.addItem("any"); + destination.setValue("any"); + destination.addItem("start"); + destination.addItem("end"); + destination.addItem("middle"); + rowScroll.addComponent(destination); + final TextField rowIndex = new TextField(); + rowScroll.addComponent(rowIndex); + final TextField rowPadding = new TextField(); + rowScroll.addComponent(rowPadding); + rowScroll.addComponent(new Button("scroll to row", + new Button.ClickListener() { + @Override + public void buttonClick(final ClickEvent event) { + int index; + try { + index = Integer.parseInt(rowIndex.getValue()); + } catch (NumberFormatException e) { + index = 0; + } + + int padding; + try { + padding = Integer.parseInt(rowPadding.getValue()); + } catch (NumberFormatException e) { + padding = 0; + } + + grid.scrollToRow(index, + (String) destination.getValue(), padding); + } + })); + addComponent(rowScroll); + + final HorizontalLayout colScroll = new HorizontalLayout(); + final NativeSelect colDestination = new NativeSelect(); + colDestination.setNullSelectionAllowed(false); + colDestination.addItem("any"); + colDestination.setValue("any"); + colDestination.addItem("start"); + colDestination.addItem("end"); + colDestination.addItem("middle"); + colScroll.addComponent(colDestination); + final TextField colIndex = new TextField(); + colScroll.addComponent(colIndex); + final TextField colPadding = new TextField(); + colScroll.addComponent(colPadding); + colScroll.addComponent(new Button("scroll to column", + new Button.ClickListener() { + @Override + public void buttonClick(final ClickEvent event) { + int index; + try { + index = Integer.parseInt(colIndex.getValue()); + } catch (NumberFormatException e) { + index = 0; + } + + int padding; + try { + padding = Integer.parseInt(colPadding.getValue()); + } catch (NumberFormatException e) { + padding = 0; + } + + grid.scrollToColumn(index, + (String) colDestination.getValue(), padding); + } + })); + addComponent(colScroll); + + final TextField freezeCount = new TextField(); + freezeCount.setConverter(Integer.class); + freezeCount.setNullRepresentation(""); + addComponent(new HorizontalLayout(freezeCount, new Button( + "set frozen columns", new Button.ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + grid.setFrozenColumns(((Integer) freezeCount + .getConvertedValue()).intValue()); + freezeCount.setValue(null); + } + }))); + + addComponent(new Button("Resize randomly", new Button.ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + int width = random.nextInt(300) + 500; + int height = random.nextInt(300) + 200; + grid.setWidth(width + "px"); + grid.setHeight(height + "px"); + } + })); + + addComponent(new Button("Random headers count", + new Button.ClickListener() { + private int headers = 1; + + @Override + public void buttonClick(ClickEvent event) { + int diff = 0; + while (diff == 0) { + final int nextHeaders = random.nextInt(4); + diff = nextHeaders - headers; + headers = nextHeaders; + } + if (diff > 0) { + grid.insertHeaders(0, diff); + } else if (diff < 0) { + grid.removeHeaders(0, -diff); + } + } + })); + + addComponent(new Button("Random footers count", + new Button.ClickListener() { + private int footers = 1; + + @Override + public void buttonClick(ClickEvent event) { + int diff = 0; + while (diff == 0) { + final int nextFooters = random.nextInt(4); + diff = nextFooters - footers; + footers = nextFooters; + } + if (diff > 0) { + grid.insertFooters(0, diff); + } else if (diff < 0) { + grid.removeFooters(0, -diff); + } + } + })); + + final Layout resizeColumnsLayout = new HorizontalLayout(); + final TextField resizeColumnIndex = new TextField(); + resizeColumnsLayout.addComponent(resizeColumnIndex); + final TextField resizeColumnPx = new TextField(); + resizeColumnsLayout.addComponent(resizeColumnPx); + resizeColumnsLayout.addComponent(new Button("resize column", + new Button.ClickListener() { + @Override + public void buttonClick(final ClickEvent event) { + final int index = Integer.parseInt(resizeColumnIndex + .getValue()); + final int px = Integer.parseInt(resizeColumnPx + .getValue()); + grid.setColumnWidth(index, px); + } + })); + addComponent(resizeColumnsLayout); + + addComponent(new Button("Autoresize columns", + new Button.ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + grid.calculateColumnWidths(); + } + })); + } + + @Override + protected String getTestDescription() { + return null; + } + + @Override + protected Integer getTicketNumber() { + return null; + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java new file mode 100644 index 0000000000..c28feb8d10 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java @@ -0,0 +1,343 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.tests.components.grid; + +import java.util.ArrayList; +import java.util.LinkedHashMap; + +import com.vaadin.data.Item; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.tests.components.AbstractComponentTest; +import com.vaadin.ui.components.grid.ColumnGroup; +import com.vaadin.ui.components.grid.ColumnGroupRow; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.GridColumn; + +/** + * Tests the basic features like columns, footers and headers + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class GridBasicFeatures extends AbstractComponentTest<Grid> { + + private final int COLUMNS = 10; + + private int columnGroupRows = 0; + + private final int ROWS = 1000; + + private IndexedContainer ds; + + @Override + protected Grid constructComponent() { + + // Build data source + ds = new IndexedContainer(); + + for (int col = 0; col < COLUMNS; col++) { + ds.addContainerProperty(getColumnProperty(col), String.class, ""); + } + + for (int row = 0; row < ROWS; row++) { + Item item = ds.addItem(Integer.valueOf(row)); + for (int col = 0; col < COLUMNS; col++) { + item.getItemProperty(getColumnProperty(col)).setValue( + "(" + row + ", " + col + ")"); + } + } + + // Create grid + Grid grid = new Grid(ds); + + // Add footer values (header values are automatically created) + for (int col = 0; col < COLUMNS; col++) { + grid.getColumn(getColumnProperty(col)).setFooterCaption( + "Footer " + col); + } + + // Set varying column widths + for (int col = 0; col < COLUMNS; col++) { + grid.getColumn("Column" + col).setWidth(100 + col * 50); + } + + createGridActions(); + + createColumnActions(); + + createHeaderActions(); + + createFooterActions(); + + createColumnGroupActions(); + + createRowActions(); + + return grid; + } + + protected void createGridActions() { + LinkedHashMap<String, String> primaryStyleNames = new LinkedHashMap<String, String>(); + primaryStyleNames.put("v-grid", "v-grid"); + primaryStyleNames.put("v-escalator", "v-escalator"); + primaryStyleNames.put("my-grid", "my-grid"); + + createMultiClickAction("Primary style name", "State", + primaryStyleNames, new Command<Grid, String>() { + + @Override + public void execute(Grid grid, String value, Object data) { + grid.setPrimaryStyleName(value); + + } + }, primaryStyleNames.get("v-grid")); + } + + protected void createHeaderActions() { + createCategory("Headers", null); + + createBooleanAction("Visible", "Headers", true, + new Command<Grid, Boolean>() { + + @Override + public void execute(Grid grid, Boolean value, Object data) { + grid.setColumnHeadersVisible(value); + } + }); + } + + protected void createFooterActions() { + createCategory("Footers", null); + + createBooleanAction("Visible", "Footers", false, + new Command<Grid, Boolean>() { + + @Override + public void execute(Grid grid, Boolean value, Object data) { + grid.setColumnFootersVisible(value); + } + }); + } + + protected void createColumnActions() { + createCategory("Columns", null); + + for (int c = 0; c < COLUMNS; c++) { + createCategory(getColumnProperty(c), "Columns"); + + createBooleanAction("Visible", getColumnProperty(c), true, + new Command<Grid, Boolean>() { + + @Override + public void execute(Grid grid, Boolean value, + Object columnIndex) { + Object propertyId = (new ArrayList(grid + .getContainerDatasource() + .getContainerPropertyIds()) + .get((Integer) columnIndex)); + GridColumn column = grid.getColumn(propertyId); + column.setVisible(!column.isVisible()); + } + }, c); + + createClickAction("Remove", getColumnProperty(c), + new Command<Grid, String>() { + + @Override + public void execute(Grid grid, String value, Object data) { + grid.getContainerDatasource() + .removeContainerProperty("Column" + data); + } + }, null, c); + + createClickAction("Freeze", getColumnProperty(c), + new Command<Grid, String>() { + + @Override + public void execute(Grid grid, String value, Object data) { + grid.setLastFrozenPropertyId("Column" + data); + } + }, null, c); + + createCategory("Column" + c + " Width", getColumnProperty(c)); + + createClickAction("Auto", "Column" + c + " Width", + new Command<Grid, Integer>() { + + @Override + public void execute(Grid grid, Integer value, + Object columnIndex) { + Object propertyId = (new ArrayList(grid + .getContainerDatasource() + .getContainerPropertyIds()) + .get((Integer) columnIndex)); + GridColumn column = grid.getColumn(propertyId); + column.setWidthUndefined(); + } + }, -1, c); + + for (int w = 50; w < 300; w += 50) { + createClickAction(w + "px", "Column" + c + " Width", + new Command<Grid, Integer>() { + + @Override + public void execute(Grid grid, Integer value, + Object columnIndex) { + Object propertyId = (new ArrayList(grid + .getContainerDatasource() + .getContainerPropertyIds()) + .get((Integer) columnIndex)); + GridColumn column = grid.getColumn(propertyId); + column.setWidth(value); + } + }, w, c); + } + } + } + + private static String getColumnProperty(int c) { + return "Column" + c; + } + + protected void createColumnGroupActions() { + createCategory("Column groups", null); + + createClickAction("Add group row", "Column groups", + new Command<Grid, String>() { + + @Override + public void execute(Grid grid, String value, Object data) { + final ColumnGroupRow row = grid.addColumnGroupRow(); + columnGroupRows++; + createCategory("Column group row " + columnGroupRows, + "Column groups"); + + createBooleanAction("Header Visible", + "Column group row " + columnGroupRows, true, + new Command<Grid, Boolean>() { + + @Override + public void execute(Grid grid, + Boolean value, Object columnIndex) { + row.setHeaderVisible(value); + } + }, row); + + createBooleanAction("Footer Visible", + "Column group row " + columnGroupRows, false, + new Command<Grid, Boolean>() { + + @Override + public void execute(Grid grid, + Boolean value, Object columnIndex) { + row.setFooterVisible(value); + } + }, row); + + for (int i = 0; i < COLUMNS; i++) { + final int columnIndex = i; + createClickAction("Group Column " + columnIndex + + " & " + (columnIndex + 1), + "Column group row " + columnGroupRows, + new Command<Grid, Integer>() { + + @Override + public void execute(Grid c, + Integer value, Object data) { + final ColumnGroup group = row + .addGroup( + "Column" + value, + "Column" + + (value + 1)); + + group.setHeaderCaption("Column " + + value + " & " + + (value + 1)); + + group.setFooterCaption("Column " + + value + " & " + + (value + 1)); + } + }, i, row); + } + } + }, null, null); + + } + + protected void createRowActions() { + createCategory("Body rows", null); + + createClickAction("Add first row", "Body rows", + new Command<Grid, String>() { + @Override + public void execute(Grid c, String value, Object data) { + Item item = ds.addItemAt(0, new Object()); + for (int i = 0; i < COLUMNS; i++) { + item.getItemProperty(getColumnProperty(i)) + .setValue("newcell: " + i); + } + } + }, null); + + createClickAction("Remove first row", "Body rows", + new Command<Grid, String>() { + @Override + public void execute(Grid c, String value, Object data) { + Object firstItemId = ds.getIdByIndex(0); + ds.removeItem(firstItemId); + } + }, null); + + createClickAction("Modify first row (getItemProperty)", "Body rows", + new Command<Grid, String>() { + @Override + public void execute(Grid c, String value, Object data) { + Object firstItemId = ds.getIdByIndex(0); + Item item = ds.getItem(firstItemId); + for (int i = 0; i < COLUMNS; i++) { + item.getItemProperty(getColumnProperty(i)) + .setValue("modified: " + i); + } + } + }, null); + + createClickAction("Modify first row (getContainerProperty)", + "Body rows", new Command<Grid, String>() { + @Override + public void execute(Grid c, String value, Object data) { + Object firstItemId = ds.getIdByIndex(0); + for (Object containerPropertyId : ds + .getContainerPropertyIds()) { + ds.getContainerProperty(firstItemId, + containerPropertyId).setValue( + "modified: " + containerPropertyId); + } + } + }, null); + } + + @Override + protected Integer getTicketNumber() { + return 12829; + } + + @Override + protected Class<Grid> getTestClass() { + return Grid.class; + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java new file mode 100644 index 0000000000..bc43f2be98 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java @@ -0,0 +1,373 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.tests.components.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.interactions.Actions; + +import com.vaadin.tests.tb3.MultiBrowserTest; + +/** + * + * @since + * @author Vaadin Ltd + */ +public class GridBasicFeaturesTest extends MultiBrowserTest { + + @Test + public void testColumnHeaderCaptions() throws Exception { + openTestURL(); + + // Column headers should be visible + List<WebElement> cells = getGridHeaderRowCells(); + assertEquals(10, cells.size()); + assertEquals("Column0", cells.get(0).getText()); + assertEquals("Column1", cells.get(1).getText()); + assertEquals("Column2", cells.get(2).getText()); + } + + @Test + public void testColumnFooterCaptions() throws Exception { + openTestURL(); + + // footer row should by default be hidden + List<WebElement> cells = getGridFooterRowCells(); + assertEquals(0, cells.size()); + + // Open footer row + selectMenuPath("Component", "Footers", "Visible"); + + // Footers should now be visible + cells = getGridFooterRowCells(); + assertEquals(10, cells.size()); + assertEquals("Footer 0", cells.get(0).getText()); + assertEquals("Footer 1", cells.get(1).getText()); + assertEquals("Footer 2", cells.get(2).getText()); + } + + @Test + public void testColumnGroupHeaders() throws Exception { + openTestURL(); + + // Hide column headers for this test + selectMenuPath("Component", "Headers", "Visible"); + + List<WebElement> cells = getGridHeaderRowCells(); + + // header row should be empty + assertEquals(0, cells.size()); + + // add a group row + selectMenuPath("Component", "Column groups", "Add group row"); + + // Empty group row cells should be present + cells = getGridHeaderRowCells(); + assertEquals(10, cells.size()); + + // Group columns 0 & 1 + selectMenuPath("Component", "Column groups", "Column group row 1", + "Group Column 0 & 1"); + + cells = getGridHeaderRowCells(); + assertEquals("Column 0 & 1", cells.get(0).getText()); + } + + @Test + public void testColumnGroupFooters() throws Exception { + openTestURL(); + + // add a group row + selectMenuPath("Component", "Column groups", "Add group row"); + + // Set footer visible + selectMenuPath("Component", "Column groups", "Column group row 1", + "Footer Visible"); + + // Group columns 0 & 1 + selectMenuPath("Component", "Column groups", "Column group row 1", + "Group Column 0 & 1"); + + List<WebElement> cells = getGridFooterRowCells(); + assertEquals("Column 0 & 1", cells.get(0).getText()); + } + + @Test + public void testGroupingSameColumnsOnRowThrowsException() throws Exception { + openTestURL(); + + // add a group row + selectMenuPath("Component", "Column groups", "Add group row"); + + // Group columns 0 & 1 + selectMenuPath("Component", "Column groups", "Column group row 1", + "Group Column 0 & 1"); + + // Group columns 1 & 2 shoud fail + selectMenuPath("Component", "Column groups", "Column group row 1", + "Group Column 1 & 2"); + + assertTrue(getLogRow(0) + .contains( + "Exception occured, java.lang.IllegalArgumentExceptionColumn Column1 already belongs to another group.")); + } + + @Test + public void testHidingColumn() throws Exception { + openTestURL(); + + // Column 0 should be visible + List<WebElement> cells = getGridHeaderRowCells(); + assertEquals("Column0", cells.get(0).getText()); + + // Hide column 0 + selectMenuPath("Component", "Columns", "Column0", "Visible"); + + // Column 1 should now be the first cell + cells = getGridHeaderRowCells(); + assertEquals("Column1", cells.get(0).getText()); + } + + @Test + public void testRemovingColumn() throws Exception { + openTestURL(); + + // Column 0 should be visible + List<WebElement> cells = getGridHeaderRowCells(); + assertEquals("Column0", cells.get(0).getText()); + + // Hide column 0 + selectMenuPath("Component", "Columns", "Column0", "Remove"); + + // Column 1 should now be the first cell + cells = getGridHeaderRowCells(); + assertEquals("Column1", cells.get(0).getText()); + } + + @Test + public void testFreezingColumn() throws Exception { + openTestURL(); + + // Freeze column 2 + selectMenuPath("Component", "Columns", "Column2", "Freeze"); + + WebElement cell = getBodyCellByRowAndColumn(1, 1); + assertTrue(cell.getAttribute("class").contains("frozen")); + + cell = getBodyCellByRowAndColumn(1, 2); + assertTrue(cell.getAttribute("class").contains("frozen")); + } + + @Test + public void testInitialColumnWidths() throws Exception { + openTestURL(); + + // Default borders and margins implemented by escalator + int cellBorder = 1 + 1; + int cellMargin = 2 + 2; + + WebElement cell = getBodyCellByRowAndColumn(1, 1); + assertEquals((100 - cellBorder - cellMargin) + "px", + cell.getCssValue("width")); + + cell = getBodyCellByRowAndColumn(1, 2); + assertEquals((150 - cellBorder - cellMargin) + "px", + cell.getCssValue("width")); + + cell = getBodyCellByRowAndColumn(1, 3); + assertEquals((200 - cellBorder - cellMargin) + "px", + cell.getCssValue("width")); + } + + @Test + public void testColumnWidths() throws Exception { + openTestURL(); + + // Default borders and margins implemented by escalator + int cellBorder = 1 + 1; + int cellMargin = 2 + 2; + + // Default column width is 100px + WebElement cell = getBodyCellByRowAndColumn(1, 1); + assertEquals((100 - cellBorder - cellMargin) + "px", + cell.getCssValue("width")); + + // Set first column to be 200px wide + selectMenuPath("Component", "Columns", "Column0", "Column0 Width", + "200px"); + + cell = getBodyCellByRowAndColumn(1, 1); + assertEquals((200 - cellBorder - cellMargin) + "px", + cell.getCssValue("width")); + + // Set second column to be 150px wide + selectMenuPath("Component", "Columns", "Column1", "Column1 Width", + "150px"); + cell = getBodyCellByRowAndColumn(1, 2); + assertEquals((150 - cellBorder - cellMargin) + "px", + cell.getCssValue("width")); + + // Set first column to be auto sized (defaults to 100px currently) + selectMenuPath("Component", "Columns", "Column0", "Column0 Width", + "Auto"); + + cell = getBodyCellByRowAndColumn(1, 1); + assertEquals((100 - cellBorder - cellMargin) + "px", + cell.getCssValue("width")); + } + + @Test + public void testPrimaryStyleNames() throws Exception { + openTestURL(); + + // v-grid is default primary style namea + assertPrimaryStylename("v-grid"); + + selectMenuPath("Component", "State", "Primary style name", + "v-escalator"); + assertPrimaryStylename("v-escalator"); + + selectMenuPath("Component", "State", "Primary style name", "my-grid"); + assertPrimaryStylename("my-grid"); + + selectMenuPath("Component", "State", "Primary style name", "v-grid"); + assertPrimaryStylename("v-grid"); + } + + /** + * Test that the current view is updated when a server-side container change + * occurs (without scrolling back and forth) + */ + @Test + public void testItemSetChangeEvent() throws Exception { + openTestURL(); + + final By newRow = By.xpath("//td[text()='newcell: 0']"); + + assertTrue("Unexpected initial state", !elementIsFound(newRow)); + + selectMenuPath("Component", "Body rows", "Add first row"); + assertTrue("Add row failed", elementIsFound(newRow)); + + selectMenuPath("Component", "Body rows", "Remove first row"); + assertTrue("Remove row failed", !elementIsFound(newRow)); + } + + /** + * Test that the current view is updated when a property's value is reflect + * to the client, when the value is modified server-side. + */ + @Test + public void testPropertyValueChangeEvent() throws Exception { + openTestURL(); + + assertEquals("Unexpected cell initial state", "(0, 0)", + getBodyCellByRowAndColumn(1, 1).getText()); + + selectMenuPath("Component", "Body rows", + "Modify first row (getItemProperty)"); + assertEquals("(First) modification with getItemProperty failed", + "modified: 0", getBodyCellByRowAndColumn(1, 1).getText()); + + selectMenuPath("Component", "Body rows", + "Modify first row (getContainerProperty)"); + assertEquals("(Second) modification with getItemProperty failed", + "modified: Column0", getBodyCellByRowAndColumn(1, 1).getText()); + } + + private boolean elementIsFound(By locator) { + try { + return driver.findElement(locator) != null; + } catch (NoSuchElementException e) { + return false; + } + } + + private void assertPrimaryStylename(String stylename) { + assertTrue(getGridElement().getAttribute("class").contains(stylename)); + + String tableWrapperStyleName = getTableWrapper().getAttribute("class"); + assertTrue(tableWrapperStyleName.contains(stylename + "-tablewrapper")); + + String hscrollStyleName = getHorizontalScroller().getAttribute("class"); + assertTrue(hscrollStyleName.contains(stylename + "-scroller")); + assertTrue(hscrollStyleName + .contains(stylename + "-scroller-horizontal")); + + String vscrollStyleName = getVerticalScroller().getAttribute("class"); + assertTrue(vscrollStyleName.contains(stylename + "-scroller")); + assertTrue(vscrollStyleName.contains(stylename + "-scroller-vertical")); + } + + private WebElement getBodyCellByRowAndColumn(int row, int column) { + return getDriver().findElement( + By.xpath("//div[@id='testComponent']//tbody/tr[" + row + + "]/td[" + column + "]")); + } + + private void selectSubMenu(String menuCaption) { + selectMenu(menuCaption); + new Actions(getDriver()).moveByOffset(100, 0).build().perform(); + } + + private void selectMenu(String menuCaption) { + getDriver().findElement( + By.xpath("//span[text() = '" + menuCaption + "']")).click(); + } + + private void selectMenuPath(String... menuCaptions) { + selectMenu(menuCaptions[0]); + for (int i = 1; i < menuCaptions.length; i++) { + selectSubMenu(menuCaptions[i]); + } + } + + private WebElement getVerticalScroller() { + return getDriver().findElement( + By.xpath("//div[@id='testComponent']/div[1]")); + } + + private WebElement getHorizontalScroller() { + return getDriver().findElement( + By.xpath("//div[@id='testComponent']/div[2]")); + } + + private WebElement getTableWrapper() { + return getDriver().findElement( + By.xpath("//div[@id='testComponent']/div[3]")); + } + + private WebElement getGridElement() { + return getDriver().findElement(By.id("testComponent")); + } + + private List<WebElement> getGridHeaderRowCells() { + return getDriver().findElements( + By.xpath("//div[@id='testComponent']//thead//th")); + } + + private List<WebElement> getGridFooterRowCells() { + return getDriver().findElements( + By.xpath("//div[@id='testComponent']//tfoot//td")); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridColumnGroups.java b/uitest/src/com/vaadin/tests/components/grid/GridColumnGroups.java new file mode 100644 index 0000000000..66e7651f76 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/GridColumnGroups.java @@ -0,0 +1,111 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.tests.components.grid; + +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.ui.components.grid.ColumnGroup; +import com.vaadin.ui.components.grid.ColumnGroupRow; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.GridColumn; + +/** + * + * @since + * @author Vaadin Ltd + */ +public class GridColumnGroups extends AbstractTestUI { + + private final int COLUMNS = 4; + + @Override + protected void setup(VaadinRequest request) { + + // Setup grid + IndexedContainer ds = new IndexedContainer(); + for (int col = 0; col < COLUMNS; col++) { + ds.addContainerProperty("Column" + col, String.class, ""); + } + Grid grid = new Grid(ds); + addComponent(grid); + + /*- + * --------------------------------------------- + * | Header 1 | <- Auxiliary row 2 + * |-------------------------------------------| + * | Header 2 | Header 3 | <- Auxiliary row 1 + * |-------------------------------------------| + * | Column 1 | Column 2 | Column 3 | Column 4 | <- Column headers + * --------------------------------------------| + * | ... | ... | ... | ... | + * | ... | ... | ... | ... | + * --------------------------------------------| + * | Column 1 | Column 2 | Column 3 | Column 4 | <- Column footers + * --------------------------------------------| + * | Footer 2 | Footer 3 | <- Auxiliary row 1 + * --------------------------------------------| + * | Footer 1 | <- Auxiliary row 2 + * --------------------------------------------- + -*/ + + // Set column footers (headers are generated automatically) + grid.setColumnFootersVisible(true); + for (Object propertyId : ds.getContainerPropertyIds()) { + GridColumn column = grid.getColumn(propertyId); + column.setFooterCaption(String.valueOf(propertyId)); + } + + // First auxiliary row + ColumnGroupRow auxRow1 = grid.addColumnGroupRow(); + + // Using property id to create a column group + ColumnGroup columns12 = auxRow1.addGroup("Column0", "Column1"); + columns12.setHeaderCaption("Header 2"); + columns12.setFooterCaption("Footer 2"); + + // Using grid columns to create a column group + GridColumn column3 = grid.getColumn("Column2"); + GridColumn column4 = grid.getColumn("Column3"); + ColumnGroup columns34 = auxRow1.addGroup(column3, column4); + columns34.setHeaderCaption("Header 3"); + columns34.setFooterCaption("Footer 3"); + + // Second auxiliary row + ColumnGroupRow auxRow2 = grid.addColumnGroupRow(); + + // Using previous groups to create a column group + ColumnGroup columns1234 = auxRow2.addGroup(columns12, columns34); + columns1234.setHeaderCaption("Header 1"); + columns1234.setFooterCaption("Footer 1"); + + } + + @Override + protected String getTestDescription() { + return "Grid should support headers and footer groups"; + } + + @Override + protected Integer getTicketNumber() { + return 12894; + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridScrolling.java b/uitest/src/com/vaadin/tests/components/grid/GridScrolling.java new file mode 100644 index 0000000000..d514fbd0c5 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/GridScrolling.java @@ -0,0 +1,115 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.tests.components.grid; + +import com.vaadin.data.Item; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.VaadinRequest; +import com.vaadin.shared.ui.grid.ScrollDestination; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.ui.Button; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.Button.ClickListener; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.components.grid.Grid; + +/** + * + */ +@SuppressWarnings("serial") +public class GridScrolling extends AbstractTestUI { + + private Grid grid; + + private IndexedContainer ds; + + @Override + @SuppressWarnings("unchecked") + protected void setup(VaadinRequest request) { + // Build data source + ds = new IndexedContainer(); + + for (int col = 0; col < 5; col++) { + ds.addContainerProperty("col" + col, String.class, ""); + } + + for (int row = 0; row < 65536; row++) { + Item item = ds.addItem(Integer.valueOf(row)); + for (int col = 0; col < 5; col++) { + item.getItemProperty("col" + col).setValue( + "(" + row + ", " + col + ")"); + } + } + + grid = new Grid(ds); + + HorizontalLayout hl = new HorizontalLayout(); + hl.addComponent(grid); + hl.setMargin(true); + hl.setSpacing(true); + + VerticalLayout vl = new VerticalLayout(); + vl.setSpacing(true); + + // Add scroll buttons + Button scrollUpButton = new Button("Top", new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + grid.scrollToStart(); + } + }); + scrollUpButton.setSizeFull(); + vl.addComponent(scrollUpButton); + + for (int i = 1; i < 7; ++i) { + final int row = (ds.size() / 7) * i; + Button scrollButton = new Button("Scroll to row " + row, + new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + grid.scrollToItem(Integer.valueOf(row), + ScrollDestination.MIDDLE); + } + }); + scrollButton.setSizeFull(); + vl.addComponent(scrollButton); + } + + Button scrollDownButton = new Button("Bottom", new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + grid.scrollToEnd(); + } + }); + scrollDownButton.setSizeFull(); + vl.addComponent(scrollDownButton); + + hl.addComponent(vl); + addComponent(hl); + } + + @Override + protected String getTestDescription() { + return "Test Grid programmatic scrolling features"; + } + + @Override + protected Integer getTicketNumber() { + return 13327; + } + +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java new file mode 100644 index 0000000000..df2b8eb075 --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java @@ -0,0 +1,46 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.tests.widgetset.client.grid; + +import com.vaadin.shared.communication.ClientRpc; + +public interface TestGridClientRpc extends ClientRpc { + void insertRows(int offset, int amount); + + void removeRows(int offset, int amount); + + void insertColumns(int offset, int amount); + + void removeColumns(int offset, int amount); + + void scrollToRow(int index, String destination, int padding); + + void scrollToColumn(int index, String destination, int padding); + + void setFrozenColumns(int frozenColumns); + + void insertHeaders(int index, int amount); + + void removeHeaders(int index, int amount); + + void insertFooters(int index, int amount); + + void removeFooters(int index, int amount); + + void setColumnWidth(int index, int px); + + void calculateColumnWidths(); +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java new file mode 100644 index 0000000000..b8ea380301 --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java @@ -0,0 +1,127 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.tests.widgetset.client.grid; + +import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.grid.ScrollDestination; +import com.vaadin.tests.widgetset.server.grid.TestGrid; + +/** + * @since 7.2 + * @author Vaadin Ltd + */ +@Connect(TestGrid.class) +public class TestGridConnector extends AbstractComponentConnector { + @Override + protected void init() { + super.init(); + registerRpc(TestGridClientRpc.class, new TestGridClientRpc() { + @Override + public void insertRows(int offset, int amount) { + getWidget().insertRows(offset, amount); + } + + @Override + public void removeRows(int offset, int amount) { + getWidget().removeRows(offset, amount); + } + + @Override + public void removeColumns(int offset, int amount) { + getWidget().removeColumns(offset, amount); + } + + @Override + public void insertColumns(int offset, int amount) { + getWidget().insertColumns(offset, amount); + } + + @Override + public void scrollToRow(int index, String destination, int padding) { + getWidget().scrollToRow(index, getDestination(destination), + padding); + } + + @Override + public void scrollToColumn(int index, String destination, + int padding) { + getWidget().scrollToColumn(index, getDestination(destination), + padding); + } + + private ScrollDestination getDestination(String destination) { + final ScrollDestination d; + if (destination.equals("start")) { + d = ScrollDestination.START; + } else if (destination.equals("middle")) { + d = ScrollDestination.MIDDLE; + } else if (destination.equals("end")) { + d = ScrollDestination.END; + } else { + d = ScrollDestination.ANY; + } + return d; + } + + @Override + public void setFrozenColumns(int frozenColumns) { + getWidget().getColumnConfiguration().setFrozenColumnCount( + frozenColumns); + } + + @Override + public void insertHeaders(int index, int amount) { + getWidget().getHeader().insertRows(index, amount); + } + + @Override + public void removeHeaders(int index, int amount) { + getWidget().getHeader().removeRows(index, amount); + } + + @Override + public void insertFooters(int index, int amount) { + getWidget().getFooter().insertRows(index, amount); + } + + @Override + public void removeFooters(int index, int amount) { + getWidget().getFooter().removeRows(index, amount); + } + + @Override + public void setColumnWidth(int index, int px) { + getWidget().getColumnConfiguration().setColumnWidth(index, px); + } + + @Override + public void calculateColumnWidths() { + getWidget().calculateColumnWidths(); + } + }); + } + + @Override + public VTestGrid getWidget() { + return (VTestGrid) super.getWidget(); + } + + @Override + public TestGridState getState() { + return (TestGridState) super.getState(); + } +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java new file mode 100644 index 0000000000..73d6ba311c --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java @@ -0,0 +1,29 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.tests.widgetset.client.grid; + +import com.vaadin.shared.AbstractComponentState; + +/** + * @since 7.2 + * @author Vaadin Ltd + */ +public class TestGridState extends AbstractComponentState { + public static final String DEFAULT_HEIGHT = "400.0px"; + + /* TODO: this should be "100%" before setting final. */ + public static final String DEFAULT_WIDTH = "800.0px"; +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java new file mode 100644 index 0000000000..0230367b85 --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java @@ -0,0 +1,216 @@ +package com.vaadin.tests.widgetset.client.grid; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gwt.user.client.ui.Composite; +import com.vaadin.client.ui.grid.Cell; +import com.vaadin.client.ui.grid.ColumnConfiguration; +import com.vaadin.client.ui.grid.Escalator; +import com.vaadin.client.ui.grid.EscalatorUpdater; +import com.vaadin.client.ui.grid.Row; +import com.vaadin.client.ui.grid.RowContainer; +import com.vaadin.shared.ui.grid.ScrollDestination; + +public class VTestGrid extends Composite { + + private static class Data { + private int columnCounter = 0; + private int rowCounter = 0; + private final List<Integer> columns = new ArrayList<Integer>(); + private final List<Integer> rows = new ArrayList<Integer>(); + + @SuppressWarnings("boxing") + public void insertRows(final int offset, final int amount) { + final List<Integer> newRows = new ArrayList<Integer>(); + for (int i = 0; i < amount; i++) { + newRows.add(rowCounter++); + } + rows.addAll(offset, newRows); + } + + @SuppressWarnings("boxing") + public void insertColumns(final int offset, final int amount) { + final List<Integer> newColumns = new ArrayList<Integer>(); + for (int i = 0; i < amount; i++) { + newColumns.add(columnCounter++); + } + columns.addAll(offset, newColumns); + } + + public EscalatorUpdater createHeaderUpdater() { + return new EscalatorUpdater() { + @Override + public void updateCells(final Row row, + final Iterable<Cell> cellsToUpdate) { + for (final Cell cell : cellsToUpdate) { + if (cell.getColumn() % 3 == 0) { + cell.setColSpan(2); + } + + final Integer columnName = columns + .get(cell.getColumn()); + cell.getElement().setInnerText("Header " + columnName); + } + } + }; + } + + public EscalatorUpdater createFooterUpdater() { + return new EscalatorUpdater() { + @Override + public void updateCells(final Row row, + final Iterable<Cell> cellsToUpdate) { + for (final Cell cell : cellsToUpdate) { + if (cell.getColumn() % 3 == 1) { + cell.setColSpan(2); + } + + final Integer columnName = columns + .get(cell.getColumn()); + cell.getElement().setInnerText("Footer " + columnName); + } + } + }; + } + + public EscalatorUpdater createBodyUpdater() { + return new EscalatorUpdater() { + private int i = 0; + + public void renderCell(final Cell cell) { + final Integer columnName = columns.get(cell.getColumn()); + final Integer rowName = rows.get(cell.getRow()); + final String cellInfo = columnName + "," + rowName + " (" + + i + ")"; + + if (cell.getColumn() > 0) { + cell.getElement().setInnerText("Cell: " + cellInfo); + } else { + cell.getElement().setInnerText( + "Row " + cell.getRow() + ": " + cellInfo); + } + + if (cell.getColumn() % 3 == cell.getRow() % 3) { + cell.setColSpan(3); + } + + final double c = i * .1; + final int r = (int) ((Math.cos(c) + 1) * 128); + final int g = (int) ((Math.cos(c / Math.PI) + 1) * 128); + final int b = (int) ((Math.cos(c / (Math.PI * 2)) + 1) * 128); + cell.getElement() + .getStyle() + .setBackgroundColor( + "rgb(" + r + "," + g + "," + b + ")"); + if ((r * .8 + g * 1.3 + b * .9) / 3 < 127) { + cell.getElement().getStyle().setColor("white"); + } else { + cell.getElement().getStyle().clearColor(); + } + + i++; + } + + @Override + public void updateCells(final Row row, + final Iterable<Cell> cellsToUpdate) { + for (final Cell cell : cellsToUpdate) { + renderCell(cell); + } + } + }; + } + + public void removeRows(final int offset, final int amount) { + for (int i = 0; i < amount; i++) { + rows.remove(offset); + } + } + + public void removeColumns(final int offset, final int amount) { + for (int i = 0; i < amount; i++) { + columns.remove(offset); + } + } + } + + private final Escalator escalator = new Escalator(); + private final Data data = new Data(); + + public VTestGrid() { + initWidget(escalator); + final RowContainer header = escalator.getHeader(); + header.setEscalatorUpdater(data.createHeaderUpdater()); + header.insertRows(0, 1); + + final RowContainer footer = escalator.getFooter(); + footer.setEscalatorUpdater(data.createFooterUpdater()); + footer.insertRows(0, 1); + + escalator.getBody().setEscalatorUpdater(data.createBodyUpdater()); + + insertRows(0, 100); + insertColumns(0, 10); + + setWidth(TestGridState.DEFAULT_WIDTH); + setHeight(TestGridState.DEFAULT_HEIGHT); + + } + + public void insertRows(final int offset, final int number) { + data.insertRows(offset, number); + escalator.getBody().insertRows(offset, number); + } + + public void insertColumns(final int offset, final int number) { + data.insertColumns(offset, number); + escalator.getColumnConfiguration().insertColumns(offset, number); + } + + public ColumnConfiguration getColumnConfiguration() { + return escalator.getColumnConfiguration(); + } + + public void scrollToRow(final int index, + final ScrollDestination destination, final int padding) { + escalator.scrollToRow(index, destination, padding); + } + + public void scrollToColumn(final int index, + final ScrollDestination destination, final int padding) { + escalator.scrollToColumn(index, destination, padding); + } + + public void removeRows(final int offset, final int amount) { + data.removeRows(offset, amount); + escalator.getBody().removeRows(offset, amount); + } + + public void removeColumns(final int offset, final int amount) { + data.removeColumns(offset, amount); + escalator.getColumnConfiguration().removeColumns(offset, amount); + } + + @Override + public void setWidth(String width) { + escalator.setWidth(width); + } + + @Override + public void setHeight(String height) { + escalator.setHeight(height); + } + + public RowContainer getHeader() { + return escalator.getHeader(); + } + + public RowContainer getFooter() { + return escalator.getFooter(); + } + + public void calculateColumnWidths() { + escalator.calculateColumnWidths(); + } +} diff --git a/uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java b/uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java new file mode 100644 index 0000000000..bdc5d46c1d --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java @@ -0,0 +1,92 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * 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.vaadin.tests.widgetset.server.grid; + +import com.vaadin.tests.widgetset.client.grid.TestGridClientRpc; +import com.vaadin.tests.widgetset.client.grid.TestGridState; +import com.vaadin.ui.AbstractComponent; + +/** + * @since 7.2 + * @author Vaadin Ltd + */ +public class TestGrid extends AbstractComponent { + public TestGrid() { + setWidth(TestGridState.DEFAULT_WIDTH); + setHeight(TestGridState.DEFAULT_HEIGHT); + } + + @Override + protected TestGridState getState() { + return (TestGridState) super.getState(); + } + + public void insertRows(int offset, int amount) { + rpc().insertRows(offset, amount); + } + + public void removeRows(int offset, int amount) { + rpc().removeRows(offset, amount); + } + + public void insertColumns(int offset, int amount) { + rpc().insertColumns(offset, amount); + } + + public void removeColumns(int offset, int amount) { + rpc().removeColumns(offset, amount); + } + + private TestGridClientRpc rpc() { + return getRpcProxy(TestGridClientRpc.class); + } + + public void scrollToRow(int index, String destination, int padding) { + rpc().scrollToRow(index, destination, padding); + } + + public void scrollToColumn(int index, String destination, int padding) { + rpc().scrollToColumn(index, destination, padding); + } + + public void setFrozenColumns(int frozenColumns) { + rpc().setFrozenColumns(frozenColumns); + } + + public void insertHeaders(int index, int amount) { + rpc().insertHeaders(index, amount); + } + + public void removeHeaders(int index, int amount) { + rpc().removeHeaders(index, amount); + } + + public void insertFooters(int index, int amount) { + rpc().insertFooters(index, amount); + } + + public void removeFooters(int index, int amount) { + rpc().removeFooters(index, amount); + } + + public void setColumnWidth(int index, int px) { + rpc().setColumnWidth(index, px); + } + + public void calculateColumnWidths() { + rpc().calculateColumnWidths(); + } +} |