summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--client/src/com/vaadin/client/connectors/GridConnector.java197
-rw-r--r--client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java66
-rw-r--r--client/src/com/vaadin/client/data/AbstractRemoteDataSource.java15
-rw-r--r--client/src/com/vaadin/client/widget/grid/DetailsGenerator.java1
-rw-r--r--client/src/com/vaadin/client/widgets/Grid.java12
-rw-r--r--server/src/com/vaadin/data/RpcDataProviderExtension.java104
-rw-r--r--server/src/com/vaadin/ui/Grid.java361
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java2
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/DetailsConnectorChange.java147
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java16
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java17
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridState.java10
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java115
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridDetailsServerTest.java269
14 files changed, 1317 insertions, 15 deletions
diff --git a/client/src/com/vaadin/client/connectors/GridConnector.java b/client/src/com/vaadin/client/connectors/GridConnector.java
index d3045ee13b..0807690023 100644
--- a/client/src/com/vaadin/client/connectors/GridConnector.java
+++ b/client/src/com/vaadin/client/connectors/GridConnector.java
@@ -31,12 +31,15 @@ import java.util.logging.Logger;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.client.DeferredWorker;
import com.vaadin.client.MouseEventDetailsBuilder;
import com.vaadin.client.annotations.OnStateChange;
import com.vaadin.client.communication.StateChangeEvent;
+import com.vaadin.client.connectors.RpcDataSourceConnector.DetailsListener;
import com.vaadin.client.connectors.RpcDataSourceConnector.RpcDataSource;
import com.vaadin.client.data.DataSource.RowHandle;
import com.vaadin.client.renderers.Renderer;
@@ -45,6 +48,7 @@ import com.vaadin.client.ui.AbstractHasComponentsConnector;
import com.vaadin.client.ui.SimpleManagedLayout;
import com.vaadin.client.widget.grid.CellReference;
import com.vaadin.client.widget.grid.CellStyleGenerator;
+import com.vaadin.client.widget.grid.DetailsGenerator;
import com.vaadin.client.widget.grid.EditorHandler;
import com.vaadin.client.widget.grid.RowReference;
import com.vaadin.client.widget.grid.RowStyleGenerator;
@@ -72,8 +76,10 @@ import com.vaadin.client.widgets.Grid.FooterCell;
import com.vaadin.client.widgets.Grid.FooterRow;
import com.vaadin.client.widgets.Grid.HeaderCell;
import com.vaadin.client.widgets.Grid.HeaderRow;
+import com.vaadin.shared.Connector;
import com.vaadin.shared.data.sort.SortDirection;
import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.grid.DetailsConnectorChange;
import com.vaadin.shared.ui.grid.EditorClientRpc;
import com.vaadin.shared.ui.grid.EditorServerRpc;
import com.vaadin.shared.ui.grid.GridClientRpc;
@@ -103,7 +109,7 @@ import elemental.json.JsonValue;
*/
@Connect(com.vaadin.ui.Grid.class)
public class GridConnector extends AbstractHasComponentsConnector implements
- SimpleManagedLayout {
+ SimpleManagedLayout, DeferredWorker {
private static final class CustomCellStyleGenerator implements
CellStyleGenerator<JsonObject> {
@@ -382,6 +388,127 @@ public class GridConnector extends AbstractHasComponentsConnector implements
}
};
+ private static class CustomDetailsGenerator implements DetailsGenerator {
+
+ private final Map<Integer, ComponentConnector> indexToDetailsMap = new HashMap<Integer, ComponentConnector>();
+
+ @Override
+ @SuppressWarnings("boxing")
+ public Widget getDetails(int rowIndex) {
+ ComponentConnector componentConnector = indexToDetailsMap
+ .get(rowIndex);
+ if (componentConnector != null) {
+ return componentConnector.getWidget();
+ } else {
+ return null;
+ }
+ }
+
+ public void setDetailsConnectorChanges(
+ Set<DetailsConnectorChange> changes) {
+ /*
+ * To avoid overwriting connectors while moving them about, we'll
+ * take all the affected connectors, first all remove those that are
+ * removed or moved, then we add back those that are moved or added.
+ */
+
+ /* Remove moved/removed connectors from bookkeeping */
+ for (DetailsConnectorChange change : changes) {
+ Integer oldIndex = change.getOldIndex();
+ Connector removedConnector = indexToDetailsMap.remove(oldIndex);
+
+ Connector connector = change.getConnector();
+ assert removedConnector == null || connector == null
+ || removedConnector.equals(connector) : "Index "
+ + oldIndex + " points to " + removedConnector
+ + " while " + connector + " was expected";
+ }
+
+ /* Add moved/added connectors to bookkeeping */
+ for (DetailsConnectorChange change : changes) {
+ Integer newIndex = change.getNewIndex();
+ ComponentConnector connector = (ComponentConnector) change
+ .getConnector();
+
+ if (connector != null) {
+ assert newIndex != null : "An existing connector has a missing new index.";
+
+ ComponentConnector prevConnector = indexToDetailsMap.put(
+ newIndex, connector);
+
+ assert prevConnector == null : "Connector collision at index "
+ + newIndex
+ + " between old "
+ + prevConnector
+ + " and new " + connector;
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("boxing")
+ private class DetailsConnectorFetcher implements DeferredWorker {
+
+ /** A flag making sure that we don't call scheduleFinally many times. */
+ private boolean fetcherHasBeenCalled = false;
+
+ /** A rolling counter for unique values. */
+ private int detailsFetchCounter = 0;
+
+ /** A collection that tracks the amount of requests currently underway. */
+ private Set<Integer> pendingFetches = new HashSet<Integer>(5);
+
+ private final ScheduledCommand lazyDetailsFetcher = new ScheduledCommand() {
+ @Override
+ public void execute() {
+ int currentFetchId = detailsFetchCounter++;
+ pendingFetches.add(currentFetchId);
+ getRpcProxy(GridServerRpc.class).sendDetailsComponents(
+ currentFetchId);
+ fetcherHasBeenCalled = false;
+
+ assert assertRequestDoesNotTimeout(currentFetchId);
+ }
+ };
+
+ public void schedule() {
+ if (!fetcherHasBeenCalled) {
+ Scheduler.get().scheduleFinally(lazyDetailsFetcher);
+ fetcherHasBeenCalled = true;
+ }
+ }
+
+ public void responseReceived(int fetchId) {
+ /* Ignore negative fetchIds (they're pushed, not fetched) */
+ if (fetchId >= 0) {
+ boolean success = pendingFetches.remove(fetchId);
+ assert success : "Received a response with an unidentified fetch id";
+ }
+ }
+
+ @Override
+ public boolean isWorkPending() {
+ return fetcherHasBeenCalled || !pendingFetches.isEmpty();
+ }
+
+ private boolean assertRequestDoesNotTimeout(final int fetchId) {
+ /*
+ * This method will not be compiled without asserts enabled. This
+ * only makes sure that any request does not time out.
+ *
+ * TODO Should this be an explicit check? Is it worth the overhead?
+ */
+ new Timer() {
+ @Override
+ public void run() {
+ assert !pendingFetches.contains(fetchId) : "Fetch id "
+ + fetchId + " timed out.";
+ }
+ }.schedule(1000);
+ return true;
+ }
+ }
+
/**
* Maps a generated column id to a grid column instance
*/
@@ -438,6 +565,29 @@ public class GridConnector extends AbstractHasComponentsConnector implements
private String lastKnownTheme = null;
+ private final CustomDetailsGenerator customDetailsGenerator = new CustomDetailsGenerator();
+
+ private final DetailsConnectorFetcher detailsConnectorFetcher = new DetailsConnectorFetcher();
+
+ private final DetailsListener detailsListener = new DetailsListener() {
+ @Override
+ public void reapplyDetailsVisibility(int rowIndex, JsonObject row) {
+ if (row.hasKey(GridState.JSONKEY_DETAILS_VISIBLE)
+ && row.getBoolean(GridState.JSONKEY_DETAILS_VISIBLE)) {
+ getWidget().setDetailsVisible(rowIndex, true);
+ } else {
+ getWidget().setDetailsVisible(rowIndex, false);
+ }
+
+ detailsConnectorFetcher.schedule();
+ }
+
+ @Override
+ public void closeDetails(int rowIndex) {
+ getWidget().setDetailsVisible(rowIndex, false);
+ }
+ };
+
@Override
@SuppressWarnings("unchecked")
public Grid<JsonObject> getWidget() {
@@ -490,6 +640,36 @@ public class GridConnector extends AbstractHasComponentsConnector implements
public void recalculateColumnWidths() {
getWidget().recalculateColumnWidths();
}
+
+ @Override
+ @SuppressWarnings("boxing")
+ public void setDetailsConnectorChanges(
+ Set<DetailsConnectorChange> connectorChanges, int fetchId) {
+ customDetailsGenerator
+ .setDetailsConnectorChanges(connectorChanges);
+
+ // refresh moved/added details rows
+ for (DetailsConnectorChange change : connectorChanges) {
+ Integer oldIndex = change.getOldIndex();
+ Integer newIndex = change.getNewIndex();
+
+ assert oldIndex == null || oldIndex >= 0 : "Got an "
+ + "invalid old index: " + oldIndex
+ + " (connector: " + change.getConnector() + ")";
+ assert newIndex == null || newIndex >= 0 : "Got an "
+ + "invalid new index: " + newIndex
+ + " (connector: " + change.getConnector() + ")";
+
+ Integer index = newIndex;
+ if (index == null) {
+ index = oldIndex;
+ }
+
+ getWidget().setDetailsVisible(index, false);
+ getWidget().setDetailsVisible(index, true);
+ }
+ detailsConnectorFetcher.responseReceived(fetchId);
+ }
});
getWidget().addSelectionHandler(internalSelectionChangeHandler);
@@ -532,10 +712,10 @@ public class GridConnector extends AbstractHasComponentsConnector implements
});
getWidget().setEditorHandler(new CustomEditorHandler());
-
getWidget().addColumnReorderHandler(columnReorderHandler);
-
+ getWidget().setDetailsGenerator(customDetailsGenerator);
getLayoutManager().registerDependency(this, getWidget().getElement());
+
layout();
}
@@ -826,7 +1006,7 @@ public class GridConnector extends AbstractHasComponentsConnector implements
column.setSortable(state.sortable);
column.setHidden(state.hidden);
- column.setHideable(state.hidable);
+ column.setHidable(state.hidable);
column.setEditable(state.editable);
column.setEditorConnector((AbstractFieldConnector) state.editorConnector);
@@ -1034,4 +1214,13 @@ public class GridConnector extends AbstractHasComponentsConnector implements
public void layout() {
getWidget().onResize();
}
+
+ @Override
+ public boolean isWorkPending() {
+ return detailsConnectorFetcher.isWorkPending();
+ }
+
+ public DetailsListener getDetailsListener() {
+ return detailsListener;
+ }
}
diff --git a/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java b/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java
index f8d6ebcb62..e8c7ee5286 100644
--- a/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java
+++ b/client/src/com/vaadin/client/connectors/RpcDataSourceConnector.java
@@ -17,6 +17,7 @@
package com.vaadin.client.connectors;
import java.util.ArrayList;
+import java.util.List;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.data.AbstractRemoteDataSource;
@@ -43,6 +44,36 @@ import elemental.json.JsonObject;
@Connect(com.vaadin.data.RpcDataProviderExtension.class)
public class RpcDataSourceConnector extends AbstractExtensionConnector {
+ /**
+ * A callback interface to let {@link GridConnector} know that detail
+ * visibilities might have changed.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+ interface DetailsListener {
+
+ /**
+ * A request to verify (and correct) the visibility for a row, given
+ * updated metadata.
+ *
+ * @param rowIndex
+ * the index of the row that should be checked
+ * @param row
+ * the row object to check visibility for
+ * @see GridState#JSONKEY_DETAILS_VISIBLE
+ */
+ void reapplyDetailsVisibility(int rowIndex, JsonObject row);
+
+ /**
+ * Closes details for a row.
+ *
+ * @param rowIndex
+ * the index of the row for which to close details
+ */
+ void closeDetails(int rowIndex);
+ }
+
public class RpcDataSource extends AbstractRemoteDataSource<JsonObject> {
protected RpcDataSource() {
@@ -56,27 +87,28 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector {
rows.add(rowObject);
}
- dataSource.setRowData(firstRow, rows);
+ RpcDataSource.this.setRowData(firstRow, rows);
}
@Override
public void removeRowData(int firstRow, int count) {
- dataSource.removeRowData(firstRow, count);
+ RpcDataSource.this.removeRowData(firstRow, count);
}
@Override
public void insertRowData(int firstRow, int count) {
- dataSource.insertRowData(firstRow, count);
+ RpcDataSource.this.insertRowData(firstRow, count);
}
@Override
public void resetDataAndSize(int size) {
- dataSource.resetDataAndSize(size);
+ RpcDataSource.this.resetDataAndSize(size);
}
});
}
private DataRequestRpc rpcProxy = getRpcProxy(DataRequestRpc.class);
+ private DetailsListener detailsListener;
@Override
protected void requestRows(int firstRowIndex, int numberOfRows,
@@ -170,7 +202,29 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector {
if (!handle.isPinned()) {
rpcProxy.setPinned(key, false);
}
+ }
+
+ void setDetailsListener(DetailsListener detailsListener) {
+ this.detailsListener = detailsListener;
+ }
+
+ @Override
+ protected void setRowData(int firstRowIndex, List<JsonObject> rowData) {
+ super.setRowData(firstRowIndex, rowData);
+ /*
+ * Intercepting details information from the data source, rerouting
+ * them back to the GridConnector (as a details listener)
+ */
+ for (int i = 0; i < rowData.size(); i++) {
+ detailsListener.reapplyDetailsVisibility(firstRowIndex + i,
+ rowData.get(i));
+ }
+ }
+
+ @Override
+ protected void onDropFromCache(int rowIndex) {
+ detailsListener.closeDetails(rowIndex);
}
}
@@ -178,6 +232,8 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector {
@Override
protected void extend(ServerConnector target) {
- ((GridConnector) target).setDataSource(dataSource);
+ GridConnector gridConnector = (GridConnector) target;
+ dataSource.setDetailsListener(gridConnector.getDetailsListener());
+ gridConnector.setDataSource(dataSource);
}
}
diff --git a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java
index 1de271c646..152b66f2ca 100644
--- a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java
+++ b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java
@@ -332,9 +332,23 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> {
for (int i = range.getStart(); i < range.getEnd(); i++) {
T removed = indexToRowMap.remove(Integer.valueOf(i));
keyToIndexMap.remove(getRowKey(removed));
+
+ onDropFromCache(i);
}
}
+ /**
+ * A hook that can be overridden to do something whenever a row is dropped
+ * from the cache.
+ *
+ * @since
+ * @param rowIndex
+ * the index of the dropped row
+ */
+ protected void onDropFromCache(int rowIndex) {
+ // noop
+ }
+
private void handleMissingRows(Range range) {
if (range.isEmpty()) {
return;
@@ -570,6 +584,7 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> {
Profiler.leave("AbstractRemoteDataSource.insertRowData");
}
+ @SuppressWarnings("boxing")
private void moveRowFromIndexToIndex(int oldIndex, int newIndex) {
T row = indexToRowMap.remove(oldIndex);
if (indexToRowMap.containsKey(newIndex)) {
diff --git a/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java b/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java
index 264aa4e614..309e3f1ea3 100644
--- a/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java
+++ b/client/src/com/vaadin/client/widget/grid/DetailsGenerator.java
@@ -25,6 +25,7 @@ import com.google.gwt.user.client.ui.Widget;
*/
public interface DetailsGenerator {
+ /** A details generator that provides no details */
public static final DetailsGenerator NULL = new DetailsGenerator() {
@Override
public Widget getDetails(int rowIndex) {
diff --git a/client/src/com/vaadin/client/widgets/Grid.java b/client/src/com/vaadin/client/widgets/Grid.java
index c6ac35bba0..174f2dde38 100644
--- a/client/src/com/vaadin/client/widgets/Grid.java
+++ b/client/src/com/vaadin/client/widgets/Grid.java
@@ -7263,10 +7263,17 @@ public class Grid<T> extends ResizeComposite implements
* @since
* @param detailsGenerator
* the details generator to set
+ * @throws IllegalArgumentException
+ * if detailsGenerator is <code>null</code>;
*/
public void setDetailsGenerator(DetailsGenerator detailsGenerator)
throws IllegalArgumentException {
+ if (detailsGenerator == null) {
+ throw new IllegalArgumentException(
+ "Details generator may not be null");
+ }
+
this.detailsGenerator = detailsGenerator;
// this will refresh all visible spacers
@@ -7317,12 +7324,13 @@ public class Grid<T> extends ResizeComposite implements
* see GridSpacerUpdater.init for implementation details.
*/
- if (visible && !isDetailsVisible(rowIndex)) {
+ boolean isVisible = isDetailsVisible(rowIndex);
+ if (visible && !isVisible) {
escalator.getBody().setSpacer(rowIndex, DETAILS_ROW_INITIAL_HEIGHT);
visibleDetails.add(rowIndexInteger);
}
- else if (!visible && isDetailsVisible(rowIndex)) {
+ else if (!visible && isVisible) {
escalator.getBody().setSpacer(rowIndex, -1);
visibleDetails.remove(rowIndexInteger);
}
diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java
index 5fb0742164..8d7b654468 100644
--- a/server/src/com/vaadin/data/RpcDataProviderExtension.java
+++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java
@@ -30,6 +30,7 @@ import java.util.logging.Logger;
import com.google.gwt.thirdparty.guava.common.collect.BiMap;
import com.google.gwt.thirdparty.guava.common.collect.HashBiMap;
+import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet;
import com.vaadin.data.Container.Indexed;
import com.vaadin.data.Container.Indexed.ItemAddEvent;
import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
@@ -51,6 +52,7 @@ import com.vaadin.ui.Grid;
import com.vaadin.ui.Grid.CellReference;
import com.vaadin.ui.Grid.CellStyleGenerator;
import com.vaadin.ui.Grid.Column;
+import com.vaadin.ui.Grid.DetailComponentManager;
import com.vaadin.ui.Grid.RowReference;
import com.vaadin.ui.Grid.RowStyleGenerator;
import com.vaadin.ui.renderers.Renderer;
@@ -110,11 +112,16 @@ public class RpcDataProviderExtension extends AbstractExtension {
}
for (Object itemId : itemsRemoved) {
+ detailComponentManager.destroyDetails(itemId);
itemIdToKey.remove(itemId);
}
for (Object itemId : itemSet) {
itemIdToKey.put(itemId, getKey(itemId));
+ if (visibleDetails.contains(itemId)) {
+ detailComponentManager.createDetails(itemId,
+ indexOf(itemId));
+ }
}
}
@@ -122,7 +129,7 @@ public class RpcDataProviderExtension extends AbstractExtension {
return String.valueOf(rollingIndex++);
}
- String getKey(Object itemId) {
+ public String getKey(Object itemId) {
String key = itemIdToKey.get(itemId);
if (key == null) {
key = nextKey();
@@ -673,13 +680,23 @@ public class RpcDataProviderExtension extends AbstractExtension {
private boolean bareItemSetTriggeredSizeChange = false;
/**
+ * This map represents all the details that are user-defined as visible.
+ * This does not reflect the status in the DOM.
+ */
+ private Set<Object> visibleDetails = new HashSet<Object>();
+
+ private DetailComponentManager detailComponentManager;
+
+ /**
* Creates a new data provider using the given container.
*
* @param container
* the container to make available
*/
- public RpcDataProviderExtension(Indexed container) {
+ public RpcDataProviderExtension(Indexed container,
+ DetailComponentManager detailComponentManager) {
this.container = container;
+ this.detailComponentManager = detailComponentManager;
rpc = getRpcProxy(DataProviderRpc.class);
registerRpc(new DataRequestRpc() {
@@ -814,6 +831,10 @@ public class RpcDataProviderExtension extends AbstractExtension {
rowObject.put(GridState.JSONKEY_DATA, rowData);
rowObject.put(GridState.JSONKEY_ROWKEY, keyMapper.getKey(itemId));
+ if (visibleDetails.contains(itemId)) {
+ rowObject.put(GridState.JSONKEY_DETAILS_VISIBLE, true);
+ }
+
rowReference.set(itemId);
CellStyleGenerator cellStyleGenerator = grid.getCellStyleGenerator();
@@ -949,6 +970,10 @@ public class RpcDataProviderExtension extends AbstractExtension {
JsonArray rowArray = Json.createArray();
rowArray.set(0, row);
rpc.setRowData(index, rowArray);
+
+ if (isDetailsVisible(itemId)) {
+ detailComponentManager.createDetails(itemId, index);
+ }
}
}
@@ -1071,4 +1096,79 @@ public class RpcDataProviderExtension extends AbstractExtension {
return Logger.getLogger(RpcDataProviderExtension.class.getName());
}
+ /**
+ * Marks a row's details to be visible or hidden.
+ * <p>
+ * If that row is currently in the client side's cache, this information
+ * will be sent over to the client.
+ *
+ * @since
+ * @param itemId
+ * the id of the item of which to change the details visibility
+ * @param visible
+ * <code>true</code> to show the details, <code>false</code> to
+ * hide
+ */
+ public void setDetailsVisible(Object itemId, boolean visible) {
+ final boolean modified;
+
+ if (visible) {
+ modified = visibleDetails.add(itemId);
+
+ /*
+ * We don't want to create the component here, since the component
+ * might be out of view, and thus we don't know where the details
+ * should end up on the client side. This is also a great thing to
+ * optimize away, so that in case a lot of things would be opened at
+ * once, a huge chunk of data doesn't get sent over immediately.
+ */
+
+ } else {
+ modified = visibleDetails.remove(itemId);
+
+ /*
+ * Here we can try to destroy the component no matter what. The
+ * component has been removed and should be detached from the
+ * component hierarchy. The details row will be closed on the client
+ * side automatically.
+ */
+ detailComponentManager.destroyDetails(itemId);
+ }
+
+ int rowIndex = indexOf(itemId);
+ boolean modifiedRowIsActive = activeRowHandler.activeRange
+ .contains(rowIndex);
+ if (modified && modifiedRowIsActive) {
+ updateRowData(itemId);
+ }
+ }
+
+ /**
+ * Checks whether the details for a row is marked as visible.
+ *
+ * @since
+ * @param itemId
+ * the id of the item of which to check the visibility
+ * @return <code>true</code> iff the detials are visible for the item. This
+ * might return <code>true</code> even if the row is not currently
+ * visible in the DOM
+ */
+ public boolean isDetailsVisible(Object itemId) {
+ return visibleDetails.contains(itemId);
+ }
+
+ public void refreshDetails() {
+ for (Object itemId : ImmutableSet.copyOf(visibleDetails)) {
+ detailComponentManager.refresh(itemId);
+ }
+ }
+
+ private int indexOf(Object itemId) {
+ /*
+ * It would be great if we could optimize this method away, since the
+ * normal usage of Grid doesn't need any indices to be known. It was
+ * already optimized away once, maybe we can do away with these as well.
+ */
+ return container.indexOfId(itemId);
+ }
}
diff --git a/server/src/com/vaadin/ui/Grid.java b/server/src/com/vaadin/ui/Grid.java
index 31a25d8f8f..396e3c5a77 100644
--- a/server/src/com/vaadin/ui/Grid.java
+++ b/server/src/com/vaadin/ui/Grid.java
@@ -37,6 +37,9 @@ import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
+import com.google.gwt.thirdparty.guava.common.collect.BiMap;
+import com.google.gwt.thirdparty.guava.common.collect.HashBiMap;
+import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import com.google.gwt.thirdparty.guava.common.collect.Sets.SetView;
import com.vaadin.data.Container;
@@ -77,6 +80,7 @@ import com.vaadin.server.KeyMapper;
import com.vaadin.server.VaadinSession;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.shared.ui.grid.DetailsConnectorChange;
import com.vaadin.shared.ui.grid.EditorClientRpc;
import com.vaadin.shared.ui.grid.EditorServerRpc;
import com.vaadin.shared.ui.grid.GridClientRpc;
@@ -228,6 +232,39 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
}
/**
+ * A callback interface for generating details for a particular row in Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+ public interface DetailsGenerator extends Serializable {
+
+ /** A details generator that provides no details */
+ public DetailsGenerator NULL = new DetailsGenerator() {
+ @Override
+ public Component getDetails(RowReference rowReference) {
+ return null;
+ }
+ };
+
+ /**
+ * This method is called for whenever a new details row needs to be
+ * generated.
+ * <p>
+ * <em>Note:</em> If a component gets generated, it may not be manually
+ * attached anywhere, nor may it be a reused instance &ndash; each
+ * invocation of this method should produce a unique and isolated
+ * component instance.
+ *
+ * @param rowReference
+ * the reference for the row for which to generate details
+ * @return the details for the given row, or <code>null</code> to leave
+ * the details empty.
+ */
+ Component getDetails(RowReference rowReference);
+ }
+
+ /**
* Custom field group that allows finding property types before an item has
* been bound.
*/
@@ -2957,6 +2994,246 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
}
/**
+ * A class that makes detail component related internal communication
+ * possible between {@link RpcDataProviderExtension} and grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+ public final class DetailComponentManager implements Serializable {
+ /**
+ * This map represents all the components that have been requested for
+ * each item id.
+ * <p>
+ * Normally this map is consistent with what is displayed in the
+ * component hierarchy (and thus the DOM). The only time this map is out
+ * of sync with the DOM is between the any calls to
+ * {@link #createDetails(Object, int)} or
+ * {@link #destroyDetails(Object)}, and
+ * {@link GridClientRpc#setDetailsConnectorChanges(Set)}.
+ * <p>
+ * This is easily checked: if {@link #unattachedComponents} is
+ * {@link Collection#isEmpty() empty}, then this field is consistent
+ * with the connector hierarchy.
+ */
+ private final Map<Object, Component> visibleDetailsComponents = Maps
+ .newHashMap();
+
+ /** A lookup map for which row contains which details component. */
+ private BiMap<Integer, Component> rowIndexToDetails = HashBiMap
+ .create();
+
+ /**
+ * A copy of {@link #rowIndexToDetails} from its last stable state. Used
+ * for creating a diff against {@link #rowIndexToDetails}.
+ *
+ * @see #getAndResetConnectorChanges()
+ */
+ private BiMap<Integer, Component> prevRowIndexToDetails = HashBiMap
+ .create();
+
+ /**
+ * A set keeping track on components that have been created, but not
+ * attached. They should be attached at some later point in time.
+ * <p>
+ * This isn't strictly requried, but it's a handy explicit log. You
+ * could find out the same thing by taking out all the other components
+ * and checking whether Grid is their parent or not.
+ */
+ private final Set<Component> unattachedComponents = Sets.newHashSet();
+
+ /**
+ * Keeps tabs on all the details that did not get a component during
+ * {@link #createDetails(Object, int)}.
+ */
+ private final Map<Object, Integer> emptyDetails = Maps.newHashMap();
+
+ /**
+ * Creates a details component by the request of the client side, with
+ * the help of the user-defined {@link DetailsGenerator}.
+ * <p>
+ * Also keeps internal bookkeeping up to date.
+ *
+ * @param itemId
+ * the item id for which to create the details component.
+ * Assumed not <code>null</code> and that a component is not
+ * currently present for this item previously
+ * @param rowIndex
+ * the row index for {@code itemId}
+ * @throws IllegalStateException
+ * if the current details generator provides a component
+ * that was manually attached, or if the same instance has
+ * already been provided
+ */
+ public void createDetails(Object itemId, int rowIndex)
+ throws IllegalStateException {
+ assert itemId != null : "itemId was null";
+ Integer newRowIndex = Integer.valueOf(rowIndex);
+
+ assert !visibleDetailsComponents.containsKey(itemId) : "itemId "
+ + "already has a component. Should be destroyed first.";
+
+ RowReference rowReference = new RowReference(Grid.this);
+ rowReference.set(itemId);
+
+ Component details = getDetailsGenerator().getDetails(rowReference);
+ if (details != null) {
+ if (details.getParent() != null) {
+ String generatorName = getDetailsGenerator().getClass()
+ .getName();
+ throw new IllegalStateException(generatorName
+ + " generated a details component that already "
+ + "was attached. (itemId: " + itemId + ", row: "
+ + rowIndex + ", component: " + details);
+ }
+
+ if (rowIndexToDetails.containsValue(details)) {
+ String generatorName = getDetailsGenerator().getClass()
+ .getName();
+ throw new IllegalStateException(generatorName
+ + " provided a details component that already "
+ + "exists in Grid. (itemId: " + itemId + ", row: "
+ + rowIndex + ", component: " + details);
+ }
+
+ visibleDetailsComponents.put(itemId, details);
+ rowIndexToDetails.put(newRowIndex, details);
+ unattachedComponents.add(details);
+
+ assert !emptyDetails.containsKey(itemId) : "Bookeeping thinks "
+ + "itemId is empty even though we just created a "
+ + "component for it (" + itemId + ")";
+ } else {
+ assert !emptyDetails.containsKey(itemId) : "Bookkeeping has "
+ + "already itemId marked as empty (itemId: " + itemId
+ + ", old index: " + emptyDetails.get(itemId)
+ + ", new index: " + newRowIndex + ")";
+ assert !emptyDetails.containsValue(newRowIndex) : "Bookkeeping"
+ + " already had another itemId for this empty index "
+ + "(index: " + newRowIndex + ", new itemId: " + itemId
+ + ")";
+ emptyDetails.put(itemId, newRowIndex);
+ }
+
+ /*
+ * Don't attach the components here. It's done by
+ * GridServerRpc.sendDetailsComponents in a separate roundtrip.
+ */
+ }
+
+ /**
+ * Destroys correctly a details component, by the request of the client
+ * side.
+ * <p>
+ * Also keeps internal bookkeeping up to date.
+ *
+ * @param itemId
+ * the item id for which to destroy the details component
+ */
+ public void destroyDetails(Object itemId) {
+ emptyDetails.remove(itemId);
+
+ Component removedComponent = visibleDetailsComponents
+ .remove(itemId);
+ if (removedComponent == null) {
+ return;
+ }
+
+ rowIndexToDetails.inverse().remove(removedComponent);
+
+ removedComponent.setParent(null);
+ markAsDirty();
+ }
+
+ /**
+ * Gets all details components that are currently attached to the grid.
+ * <p>
+ * Used internally by the Grid object.
+ *
+ * @return all details components that are currently attached to the
+ * grid
+ */
+ Collection<Component> getComponents() {
+ Set<Component> components = new HashSet<Component>(
+ visibleDetailsComponents.values());
+ components.removeAll(unattachedComponents);
+ return components;
+ }
+
+ /**
+ * Gets information on how the connectors have changed.
+ * <p>
+ * This method only returns the changes that have been made between two
+ * calls of this method. I.e. Calling this method once will reset the
+ * state for the next state.
+ * <p>
+ * Used internally by the Grid object.
+ *
+ * @return information on how the connectors have changed
+ */
+ Set<DetailsConnectorChange> getAndResetConnectorChanges() {
+ Set<DetailsConnectorChange> changes = new HashSet<DetailsConnectorChange>();
+
+ // populate diff with added/changed
+ for (Entry<Integer, Component> entry : rowIndexToDetails.entrySet()) {
+ Component component = entry.getValue();
+ assert component != null : "rowIndexToDetails contains a null component";
+
+ Integer newIndex = entry.getKey();
+ Integer oldIndex = prevRowIndexToDetails.inverse().get(
+ component);
+
+ /*
+ * only attach components. Detaching already happened in
+ * destroyDetails.
+ */
+ if (newIndex != null && oldIndex == null) {
+ assert unattachedComponents.contains(component) : "unattachedComponents does not contain component for index "
+ + newIndex + " (" + component + ")";
+ component.setParent(Grid.this);
+ unattachedComponents.remove(component);
+ }
+
+ if (!SharedUtil.equals(oldIndex, newIndex)) {
+ changes.add(new DetailsConnectorChange(component, oldIndex,
+ newIndex));
+ }
+ }
+
+ // populate diff with removed
+ for (Entry<Integer, Component> entry : prevRowIndexToDetails
+ .entrySet()) {
+ Integer oldIndex = entry.getKey();
+ Component component = entry.getValue();
+ Integer newIndex = rowIndexToDetails.inverse().get(component);
+ if (newIndex == null) {
+ changes.add(new DetailsConnectorChange(null, oldIndex, null));
+ }
+ }
+
+ // reset diff map
+ prevRowIndexToDetails = HashBiMap.create(rowIndexToDetails);
+
+ return changes;
+ }
+
+ public void refresh(Object itemId) {
+ Component component = visibleDetailsComponents.get(itemId);
+ Integer rowIndex = null;
+ if (component != null) {
+ rowIndex = rowIndexToDetails.inverse().get(component);
+ destroyDetails(itemId);
+ } else {
+ rowIndex = emptyDetails.remove(itemId);
+ }
+
+ assert rowIndex != null : "Given itemId does not map to an existing detail row ("
+ + itemId + ")";
+ createDetails(itemId, rowIndex.intValue());
+ }
+ }
+
+ /**
* The data source attached to the grid
*/
private Container.Indexed datasource;
@@ -3063,6 +3340,15 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
private EditorErrorHandler editorErrorHandler = new DefaultEditorErrorHandler();
+ /**
+ * The user-defined details generator.
+ *
+ * @see #setDetailsGenerator(DetailsGenerator)
+ */
+ private DetailsGenerator detailsGenerator = DetailsGenerator.NULL;
+
+ private final DetailComponentManager detailComponentManager = new DetailComponentManager();
+
private static final Method SELECTION_CHANGE_METHOD = ReflectTools
.findMethod(SelectionListener.class, "select", SelectionEvent.class);
@@ -3310,6 +3596,13 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
markAsDirty();
}
}
+
+ @Override
+ public void sendDetailsComponents(int fetchId) {
+ getRpcProxy(GridClientRpc.class).setDetailsConnectorChanges(
+ detailComponentManager.getAndResetConnectorChanges(),
+ fetchId);
+ }
});
registerRpc(new EditorServerRpc() {
@@ -3470,7 +3763,8 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
sortOrder.clear();
}
- datasourceExtension = new RpcDataProviderExtension(container);
+ datasourceExtension = new RpcDataProviderExtension(container,
+ detailComponentManager);
datasourceExtension.extend(this, columnKeys);
/*
@@ -4852,6 +5146,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
}
componentList.addAll(getEditorFields());
+
+ componentList.addAll(detailComponentManager.getComponents());
+
return componentList.iterator();
}
@@ -5401,4 +5698,66 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
isUserOriginated));
}
+ /**
+ * Sets a new details generator for row details.
+ * <p>
+ * The currently opened row details will be re-rendered.
+ *
+ * @since
+ * @param detailsGenerator
+ * the details generator to set
+ * @throws IllegalArgumentException
+ * if detailsGenerator is <code>null</code>;
+ */
+ public void setDetailsGenerator(DetailsGenerator detailsGenerator)
+ throws IllegalArgumentException {
+ if (detailsGenerator == null) {
+ throw new IllegalArgumentException(
+ "Details generator may not be null");
+ } else if (detailsGenerator == this.detailsGenerator) {
+ return;
+ }
+
+ this.detailsGenerator = detailsGenerator;
+
+ datasourceExtension.refreshDetails();
+ getRpcProxy(GridClientRpc.class).setDetailsConnectorChanges(
+ detailComponentManager.getAndResetConnectorChanges(), -1);
+ }
+
+ /**
+ * Gets the current details generator for row details.
+ *
+ * @since
+ * @return the detailsGenerator the current details generator
+ */
+ public DetailsGenerator getDetailsGenerator() {
+ return detailsGenerator;
+ }
+
+ /**
+ * Shows or hides the details for a specific item.
+ *
+ * @since
+ * @param itemId
+ * the id of the item for which to set details visibility
+ * @param visible
+ * <code>true</code> to show the details, or <code>false</code>
+ * to hide them
+ */
+ public void setDetailsVisible(Object itemId, boolean visible) {
+ datasourceExtension.setDetailsVisible(itemId, visible);
+ }
+
+ /**
+ * Checks whether details are visible for the given item.
+ *
+ * @since
+ * @param itemId
+ * the id of the item for which to check details visibility
+ * @return <code>true</code> iff the details are visible
+ */
+ public boolean isDetailsVisible(Object itemId) {
+ return datasourceExtension.isDetailsVisible(itemId);
+ }
}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java b/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java
index 9ecf131c5b..54f5dcdbc7 100644
--- a/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java
@@ -47,7 +47,7 @@ public class DataProviderExtension {
container = new IndexedContainer();
populate(container);
- dataProvider = new RpcDataProviderExtension(container);
+ dataProvider = new RpcDataProviderExtension(container, null);
keyMapper = dataProvider.getKeyMapper();
}
diff --git a/shared/src/com/vaadin/shared/ui/grid/DetailsConnectorChange.java b/shared/src/com/vaadin/shared/ui/grid/DetailsConnectorChange.java
new file mode 100644
index 0000000000..40f4541fb1
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/DetailsConnectorChange.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2000-2014 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 com.vaadin.shared.Connector;
+
+/**
+ * A description of an indexing modification for a connector. This is used by
+ * Grid by internal bookkeeping updates.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class DetailsConnectorChange implements Serializable {
+
+ private Connector connector;
+ private Integer oldIndex;
+ private Integer newIndex;
+
+ /** Create a new connector index change */
+ public DetailsConnectorChange() {
+ }
+
+ /**
+ * Convenience constructor for setting all the fields in one line.
+ * <p>
+ * Calling this constructor will also assert that the state of the pojo is
+ * consistent by internal assumptions.
+ *
+ * @param connector
+ * the changed connector
+ * @param oldIndex
+ * the old index
+ * @param newIndex
+ * the new index
+ */
+ public DetailsConnectorChange(Connector connector, Integer oldIndex,
+ Integer newIndex) {
+ this.connector = connector;
+ this.oldIndex = oldIndex;
+ this.newIndex = newIndex;
+
+ assert assertStateIsOk();
+ }
+
+ private boolean assertStateIsOk() {
+ boolean connectorAndNewIndexIsNotNull = connector != null
+ && newIndex != null;
+ boolean connectorAndNewIndexIsNullThenOldIndexIsSet = connector == null
+ && newIndex == null && oldIndex != null;
+
+ assert (connectorAndNewIndexIsNotNull || connectorAndNewIndexIsNullThenOldIndexIsSet) : "connector: "
+ + nullityString(connector)
+ + ", oldIndex: "
+ + nullityString(oldIndex)
+ + ", newIndex: "
+ + nullityString(newIndex);
+ return true;
+ }
+
+ private static String nullityString(Object object) {
+ return object == null ? "null" : "non-null";
+ }
+
+ /**
+ * Gets the old index for the connector.
+ * <p>
+ * If <code>null</code>, the connector is recently added. This means that
+ * {@link #getConnector()} is expected not to return <code>null</code>.
+ *
+ * @return the old index for the connector
+ */
+ public Integer getOldIndex() {
+ assert assertStateIsOk();
+ return oldIndex;
+ }
+
+ /**
+ * Gets the new index for the connector.
+ * <p>
+ * If <code>null</code>, the connector should be removed. This means that
+ * {@link #getConnector()} is expected to return <code>null</code> as well.
+ *
+ * @return the new index for the connector
+ */
+ public Integer getNewIndex() {
+ assert assertStateIsOk();
+ return newIndex;
+ }
+
+ /**
+ * Gets the changed connector.
+ *
+ * @return the changed connector. Might be <code>null</code>
+ */
+ public Connector getConnector() {
+ assert assertStateIsOk();
+ return connector;
+ }
+
+ /**
+ * Sets the changed connector.
+ *
+ * @param connector
+ * the changed connector. May be <code>null</code>
+ */
+ public void setConnector(Connector connector) {
+ this.connector = connector;
+ }
+
+ /**
+ * Sets the old index
+ *
+ * @param oldIndex
+ * the old index. May be <code>null</code> if a new connector is
+ * being inserted
+ */
+ public void setOldIndex(Integer oldIndex) {
+ this.oldIndex = oldIndex;
+ }
+
+ /**
+ * Sets the new index
+ *
+ * @param newIndex
+ * the new index. May be <code>null</code> if a connector is
+ * being removed
+ */
+ public void setNewIndex(Integer newIndex) {
+ this.newIndex = newIndex;
+ }
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java
index 4ba081b5df..98e7fac567 100644
--- a/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java
+++ b/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java
@@ -15,6 +15,8 @@
*/
package com.vaadin.shared.ui.grid;
+import java.util.Set;
+
import com.vaadin.shared.communication.ClientRpc;
/**
@@ -55,4 +57,18 @@ public interface GridClientRpc extends ClientRpc {
*/
public void recalculateColumnWidths();
+ /**
+ * Informs the GridConnector on how the indexing of details connectors has
+ * changed.
+ *
+ * @since
+ * @param connectorChanges
+ * the indexing changes of details connectors
+ * @param fetchId
+ * the id of the request for fetching the changes. A negative
+ * number indicates a push (not requested by the client side)
+ */
+ public void setDetailsConnectorChanges(
+ Set<DetailsConnectorChange> connectorChanges, int fetchId);
+
}
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java
index 4dec5530aa..28f59ea93a 100644
--- a/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java
+++ b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java
@@ -59,4 +59,21 @@ public interface GridServerRpc extends ServerRpc {
*/
void columnsReordered(List<String> newColumnOrder,
List<String> oldColumnOrder);
+
+ /**
+ * This is a trigger for Grid to send whatever has changed regarding the
+ * details components.
+ * <p>
+ * The components can't be sent eagerly, since they are generated as a side
+ * effect in
+ * {@link com.vaadin.data.RpcDataProviderExtension#beforeClientResponse(boolean)}
+ * , and that is too late to change the hierarchy. So we need this
+ * round-trip to work around that limitation.
+ *
+ * @since
+ * @param fetchId
+ * an unique identifier for the request
+ * @see com.vaadin.ui.Grid#setDetailsVisible(Object, boolean)
+ */
+ void sendDetailsComponents(int fetchId);
}
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridState.java b/shared/src/com/vaadin/shared/ui/grid/GridState.java
index ab42a52424..e039f70988 100644
--- a/shared/src/com/vaadin/shared/ui/grid/GridState.java
+++ b/shared/src/com/vaadin/shared/ui/grid/GridState.java
@@ -103,6 +103,16 @@ public class GridState extends AbstractComponentState {
public static final String JSONKEY_CELLSTYLES = "cs";
/**
+ * The key that tells whether details are visible for the row
+ *
+ * @see com.vaadin.ui.Grid#setDetailsGenerator(com.vaadin.ui.Grid.DetailsGenerator)
+ * @see com.vaadin.ui.Grid#setDetailsVisible(Object, boolean)
+ * @see com.vaadin.shared.data.DataProviderRpc#setRowData(int,
+ * elemental.json.JsonArray)
+ * */
+ public static final String JSONKEY_DETAILS_VISIBLE = "dv";
+
+ /**
* Columns in grid.
*/
public List<GridColumnState> columns = new ArrayList<GridColumnState>();
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java
index aeeaa25ac3..c8c0e54123 100644
--- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java
@@ -46,6 +46,8 @@ import com.vaadin.tests.components.AbstractComponentTest;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Button.ClickListener;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.CssLayout;
import com.vaadin.ui.Grid;
import com.vaadin.ui.Grid.CellReference;
import com.vaadin.ui.Grid.CellStyleGenerator;
@@ -54,6 +56,7 @@ import com.vaadin.ui.Grid.ColumnReorderEvent;
import com.vaadin.ui.Grid.ColumnReorderListener;
import com.vaadin.ui.Grid.ColumnVisibilityChangeEvent;
import com.vaadin.ui.Grid.ColumnVisibilityChangeListener;
+import com.vaadin.ui.Grid.DetailsGenerator;
import com.vaadin.ui.Grid.FooterCell;
import com.vaadin.ui.Grid.HeaderCell;
import com.vaadin.ui.Grid.HeaderRow;
@@ -62,6 +65,9 @@ import com.vaadin.ui.Grid.RowReference;
import com.vaadin.ui.Grid.RowStyleGenerator;
import com.vaadin.ui.Grid.SelectionMode;
import com.vaadin.ui.Grid.SelectionModel;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.Notification;
+import com.vaadin.ui.Panel;
import com.vaadin.ui.renderers.DateRenderer;
import com.vaadin.ui.renderers.HtmlRenderer;
import com.vaadin.ui.renderers.NumberRenderer;
@@ -132,6 +138,55 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
}
};
+ private Panel detailsPanel;
+
+ private final DetailsGenerator detailedDetailsGenerator = new DetailsGenerator() {
+ @Override
+ public Component getDetails(final RowReference rowReference) {
+ CssLayout cssLayout = new CssLayout();
+ cssLayout.setHeight("200px");
+ cssLayout.setWidth("100%");
+
+ Item item = rowReference.getItem();
+ for (Object propertyId : item.getItemPropertyIds()) {
+ Property<?> prop = item.getItemProperty(propertyId);
+ String string = prop.getValue().toString();
+ cssLayout.addComponent(new Label(string));
+ }
+
+ final int rowIndex = grid.getContainerDataSource().indexOfId(
+ rowReference.getItemId());
+ ClickListener clickListener = new ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ Notification.show("You clicked on the "
+ + "button in the details for " + "row " + rowIndex);
+ }
+ };
+ cssLayout.addComponent(new Button("Press me", clickListener));
+ return cssLayout;
+ }
+ };
+
+ private final DetailsGenerator watchingDetailsGenerator = new DetailsGenerator() {
+ private int id = 0;
+
+ @Override
+ public Component getDetails(RowReference rowReference) {
+ return new Label("You are watching item id "
+ + rowReference.getItemId() + " (" + (id++) + ")");
+ }
+ };
+
+ private final DetailsGenerator hierarchicalDetailsGenerator = new DetailsGenerator() {
+ @Override
+ public Component getDetails(RowReference rowReference) {
+ detailsPanel = new Panel();
+ detailsPanel.setContent(new Label("One"));
+ return detailsPanel;
+ }
+ };
+
@Override
@SuppressWarnings("unchecked")
protected Grid constructComponent() {
@@ -273,6 +328,8 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
addInternalActions();
+ createDetailsActions();
+
this.grid = grid;
return grid;
}
@@ -1165,6 +1222,64 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
}, null);
}
+ private void createDetailsActions() {
+ Command<Grid, DetailsGenerator> swapDetailsGenerator = new Command<Grid, DetailsGenerator>() {
+ @Override
+ public void execute(Grid c, DetailsGenerator generator, Object data) {
+ grid.setDetailsGenerator(generator);
+ }
+ };
+
+ Command<Grid, Boolean> openOrCloseItemId = new Command<Grid, Boolean>() {
+ @Override
+ @SuppressWarnings("boxing")
+ public void execute(Grid g, Boolean visible, Object itemId) {
+ g.setDetailsVisible(itemId, visible);
+ }
+ };
+
+ createCategory("Generators", "Details");
+ createClickAction("NULL", "Generators", swapDetailsGenerator,
+ DetailsGenerator.NULL);
+ createClickAction("\"Watching\"", "Generators", swapDetailsGenerator,
+ watchingDetailsGenerator);
+ createClickAction("Detailed", "Generators", swapDetailsGenerator,
+ detailedDetailsGenerator);
+ createClickAction("Hierarchical", "Generators", swapDetailsGenerator,
+ hierarchicalDetailsGenerator);
+
+ createClickAction("- Change Component", "Generators",
+ new Command<Grid, Void>() {
+ @Override
+ public void execute(Grid c, Void value, Object data) {
+ Label label = (Label) detailsPanel.getContent();
+ if (label.getValue().equals("One")) {
+ detailsPanel.setContent(new Label("Two"));
+ } else {
+ detailsPanel.setContent(new Label("One"));
+ }
+ }
+ }, null);
+
+ createClickAction("Toggle firstItemId", "Details",
+ new Command<Grid, Void>() {
+ @Override
+ public void execute(Grid g, Void value, Object data) {
+ Object firstItemId = g.getContainerDataSource()
+ .firstItemId();
+ boolean toggle = g.isDetailsVisible(firstItemId);
+ g.setDetailsVisible(firstItemId, !toggle);
+ g.setDetailsVisible(firstItemId, toggle);
+ }
+ }, null);
+
+ createBooleanAction("Open firstItemId", "Details", false,
+ openOrCloseItemId, ds.firstItemId());
+
+ createBooleanAction("Open 995", "Details", false, openOrCloseItemId,
+ ds.getIdByIndex(995));
+ }
+
@Override
protected Integer getTicketNumber() {
return 12829;
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridDetailsServerTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridDetailsServerTest.java
new file mode 100644
index 0000000000..7ddd903161
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridDetailsServerTest.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2000-2014 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.basicfeatures.server;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.NoSuchElementException;
+
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.testbench.elements.NotificationElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class GridDetailsServerTest extends GridBasicFeaturesTest {
+ /**
+ * The reason to why last item details wasn't selected is that since it will
+ * exist only after the viewport has been scrolled into view, we wouldn't be
+ * able to scroll that particular details row into view, making tests
+ * awkward with two scroll commands back to back.
+ */
+ private static final int ALMOST_LAST_INDEX = 995;
+ private static final String[] OPEN_ALMOST_LAST_ITEM_DETAILS = new String[] {
+ "Component", "Details", "Open " + ALMOST_LAST_INDEX };
+ private static final String[] OPEN_FIRST_ITEM_DETAILS = new String[] {
+ "Component", "Details", "Open firstItemId" };
+ private static final String[] TOGGLE_FIRST_ITEM_DETAILS = new String[] {
+ "Component", "Details", "Toggle firstItemId" };
+ private static final String[] DETAILS_GENERATOR_NULL = new String[] {
+ "Component", "Details", "Generators", "NULL" };
+ private static final String[] DETAILS_GENERATOR_WATCHING = new String[] {
+ "Component", "Details", "Generators", "\"Watching\"" };
+ private static final String[] DETAILS_GENERATOR_HIERARCHICAL = new String[] {
+ "Component", "Details", "Generators", "Hierarchical" };
+ private static final String[] CHANGE_HIERARCHY = new String[] {
+ "Component", "Details", "Generators", "- Change Component" };
+
+ @Before
+ public void setUp() {
+ openTestURL();
+ }
+
+ @Test
+ public void openVisibleDetails() {
+ try {
+ getGridElement().getDetails(0);
+ fail("Expected NoSuchElementException");
+ } catch (NoSuchElementException ignore) {
+ // expected
+ }
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+ assertNotNull("details should've opened", getGridElement()
+ .getDetails(0));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void closeVisibleDetails() {
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+
+ getGridElement().getDetails(0);
+ }
+
+ @Test
+ public void openDetailsOutsideOfActiveRange() throws InterruptedException {
+ getGridElement().scroll(10000);
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+ getGridElement().scroll(0);
+ Thread.sleep(50);
+ assertNotNull("details should've been opened", getGridElement()
+ .getDetails(0));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void closeDetailsOutsideOfActiveRange() {
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+ getGridElement().scroll(10000);
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+ getGridElement().scroll(0);
+ getGridElement().getDetails(0);
+ }
+
+ @Test
+ public void componentIsVisibleClientSide() {
+ selectMenuPath(DETAILS_GENERATOR_WATCHING);
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+
+ TestBenchElement details = getGridElement().getDetails(0);
+ assertNotNull("No widget detected inside details",
+ details.findElement(By.className("v-widget")));
+ }
+
+ @Test
+ public void openingDetailsTwice() {
+ selectMenuPath(DETAILS_GENERATOR_WATCHING);
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS); // open
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS); // close
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS); // open
+
+ TestBenchElement details = getGridElement().getDetails(0);
+ assertNotNull("No widget detected inside details",
+ details.findElement(By.className("v-widget")));
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void scrollingDoesNotCreateAFloodOfDetailsRows() {
+ selectMenuPath(DETAILS_GENERATOR_WATCHING);
+
+ // scroll somewhere to hit uncached rows
+ getGridElement().scrollToRow(101);
+
+ // this should throw
+ getGridElement().getDetails(100);
+ }
+
+ @Test
+ public void openingDetailsOutOfView() {
+ getGridElement().scrollToRow(500);
+
+ selectMenuPath(DETAILS_GENERATOR_WATCHING);
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+
+ getGridElement().scrollToRow(0);
+
+ // if this fails, it'll fail before the assertNotNull
+ assertNotNull("unexpected null details row", getGridElement()
+ .getDetails(0));
+ }
+
+ @Test
+ public void togglingAVisibleDetailsRowWithOneRoundtrip() {
+ selectMenuPath(DETAILS_GENERATOR_WATCHING);
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS); // open
+
+ assertTrue("Unexpected generator content",
+ getGridElement().getDetails(0).getText().endsWith("(0)"));
+ selectMenuPath(TOGGLE_FIRST_ITEM_DETAILS);
+ assertTrue("New component was not displayed in the client",
+ getGridElement().getDetails(0).getText().endsWith("(1)"));
+ }
+
+ @Test
+ public void almostLastItemIdIsRendered() {
+ selectMenuPath(DETAILS_GENERATOR_WATCHING);
+ selectMenuPath(OPEN_ALMOST_LAST_ITEM_DETAILS);
+ scrollGridVerticallyTo(100000);
+
+ TestBenchElement details = getGridElement().getDetails(
+ ALMOST_LAST_INDEX);
+ assertNotNull(details);
+ assertTrue("Unexpected details content",
+ details.getText().endsWith(ALMOST_LAST_INDEX + " (0)"));
+ }
+
+ @Test
+ public void hierarchyChangesWorkInDetails() {
+ selectMenuPath(DETAILS_GENERATOR_HIERARCHICAL);
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+ assertEquals("One", getGridElement().getDetails(0).getText());
+ selectMenuPath(CHANGE_HIERARCHY);
+ assertEquals("Two", getGridElement().getDetails(0).getText());
+ }
+
+ @Ignore("This use case is not currently supported by Grid. If the detail "
+ + "is out of view, the component is detached from the UI and a "
+ + "new instance is generated when scrolled back. Support will "
+ + "maybe be incorporated at a later time")
+ @Test
+ public void hierarchyChangesWorkInDetailsWhileOutOfView() {
+ selectMenuPath(DETAILS_GENERATOR_HIERARCHICAL);
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+ scrollGridVerticallyTo(10000);
+ selectMenuPath(CHANGE_HIERARCHY);
+ scrollGridVerticallyTo(0);
+ assertEquals("Two", getGridElement().getDetails(0).getText());
+ }
+
+ @Test
+ public void swappingDetailsGenerators_noDetailsShown() {
+ selectMenuPath(DETAILS_GENERATOR_WATCHING);
+ selectMenuPath(DETAILS_GENERATOR_NULL);
+ assertFalse("Got some errors", $(NotificationElement.class).exists());
+ }
+
+ @Test
+ public void swappingDetailsGenerators_shownDetails() {
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+ assertTrue("Details should be empty at the start", getGridElement()
+ .getDetails(0).getText().isEmpty());
+
+ selectMenuPath(DETAILS_GENERATOR_WATCHING);
+ assertFalse("Details should not be empty after swapping generator",
+ getGridElement().getDetails(0).getText().isEmpty());
+ }
+
+ @Test
+ public void swappingDetailsGenerators_whileDetailsScrolledOut_showNever() {
+ scrollGridVerticallyTo(1000);
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+ selectMenuPath(DETAILS_GENERATOR_WATCHING);
+ assertFalse("Got some errors", $(NotificationElement.class).exists());
+ }
+
+ @Test
+ public void swappingDetailsGenerators_whileDetailsScrolledOut_showAfter() {
+ scrollGridVerticallyTo(1000);
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+ selectMenuPath(DETAILS_GENERATOR_WATCHING);
+ scrollGridVerticallyTo(0);
+
+ assertFalse("Got some errors", $(NotificationElement.class).exists());
+ assertNotNull("Could not find a details", getGridElement()
+ .getDetails(0));
+ }
+
+ @Test
+ public void swappingDetailsGenerators_whileDetailsScrolledOut_showBefore() {
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+ selectMenuPath(DETAILS_GENERATOR_WATCHING);
+ scrollGridVerticallyTo(1000);
+
+ assertFalse("Got some errors", $(NotificationElement.class).exists());
+ assertNotNull("Could not find a details", getGridElement()
+ .getDetails(0));
+ }
+
+ @Test
+ public void swappingDetailsGenerators_whileDetailsScrolledOut_showBeforeAndAfter() {
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+ selectMenuPath(DETAILS_GENERATOR_WATCHING);
+ scrollGridVerticallyTo(1000);
+ scrollGridVerticallyTo(0);
+
+ assertFalse("Got some errors", $(NotificationElement.class).exists());
+ assertNotNull("Could not find a details", getGridElement()
+ .getDetails(0));
+ }
+
+ @Test
+ public void nullDetailComponentToggling() {
+ selectMenuPath(OPEN_FIRST_ITEM_DETAILS);
+ selectMenuPath(DETAILS_GENERATOR_WATCHING);
+ selectMenuPath(DETAILS_GENERATOR_NULL);
+ assertTrue("Details should be empty with null component",
+ getGridElement().getDetails(0).getText().isEmpty());
+ selectMenuPath(DETAILS_GENERATOR_WATCHING);
+ assertFalse("Details should be not empty with details component",
+ getGridElement().getDetails(0).getText().isEmpty());
+ }
+}