diff options
author | Henrik Paul <henrik@vaadin.com> | 2015-03-10 17:02:02 +0200 |
---|---|---|
committer | Pekka Hyvönen <pekka@vaadin.com> | 2015-03-17 21:53:20 +0000 |
commit | a1619ee73dc18eecda22056541826a3c8bb65a5c (patch) | |
tree | 398476a7cd0192faed7393cbc1a9e65ac4df87c9 /server | |
parent | 84c143dd76ed1d27d03c0d695e7218b477d008fe (diff) | |
download | vaadin-framework-a1619ee73dc18eecda22056541826a3c8bb65a5c.tar.gz vaadin-framework-a1619ee73dc18eecda22056541826a3c8bb65a5c.zip |
Grid's Details can now be Components (#16644)
Change-Id: If67dd2e86cf41c57f208a3691e2cb7a5a29c133c
Diffstat (limited to 'server')
3 files changed, 260 insertions, 3 deletions
diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java index cf2284a62e..62b8214cbd 100644 --- a/server/src/com/vaadin/data/RpcDataProviderExtension.java +++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java @@ -51,6 +51,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; @@ -113,6 +114,7 @@ public class RpcDataProviderExtension extends AbstractExtension { final Object itemId = indexToItemId.get(ii); if (!isPinned(itemId)) { + detailComponentManager.destroyDetails(itemId); itemIdToKey.remove(itemId); indexToItemId.remove(ii); } @@ -154,6 +156,7 @@ public class RpcDataProviderExtension extends AbstractExtension { } indexToItemId.forcePut(index, itemId); + detailComponentManager.createDetails(itemId, index); } index++; } @@ -747,14 +750,18 @@ public class RpcDataProviderExtension extends AbstractExtension { */ 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() { @@ -1018,6 +1025,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); + } } } @@ -1155,10 +1166,28 @@ public class RpcDataProviderExtension extends AbstractExtension { */ 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 = keyMapper.getIndex(itemId); diff --git a/server/src/com/vaadin/ui/Grid.java b/server/src/com/vaadin/ui/Grid.java index b56bb0d036..da5cedd999 100644 --- a/server/src/com/vaadin/ui/Grid.java +++ b/server/src/com/vaadin/ui/Grid.java @@ -36,6 +36,8 @@ 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.Sets; import com.google.gwt.thirdparty.guava.common.collect.Sets.SetView; import com.vaadin.data.Container; @@ -75,6 +77,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.ConnectorIndexChange; import com.vaadin.shared.ui.grid.EditorClientRpc; import com.vaadin.shared.ui.grid.EditorServerRpc; import com.vaadin.shared.ui.grid.GridClientRpc; @@ -183,6 +186,11 @@ public class Grid extends AbstractComponent implements SelectionNotifier, /** * 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 – 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 @@ -2810,6 +2818,208 @@ 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 = new HashMap<Object, Component>(); + + /** 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 = new HashSet<Component>(); + + /** + * 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); + } + + /* + * 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) { + 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<ConnectorIndexChange> getAndResetConnectorChanges() { + Set<ConnectorIndexChange> changes = new HashSet<ConnectorIndexChange>(); + + // 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 ConnectorIndexChange(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 ConnectorIndexChange(null, oldIndex, null)); + } + } + + // reset diff map + prevRowIndexToDetails = HashBiMap.create(rowIndexToDetails); + + return changes; + } + } + + /** * The data source attached to the grid */ private Container.Indexed datasource; @@ -2916,8 +3126,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); @@ -3118,6 +3335,13 @@ public class Grid extends AbstractComponent implements SelectionNotifier, fireEvent(new ItemClickEvent(Grid.this, item, itemId, propertyId, details)); } + + @Override + public void sendDetailsComponents(int fetchId) { + getRpcProxy(GridClientRpc.class).setDetailsConnectorChanges( + detailComponentManager.getAndResetConnectorChanges(), + fetchId); + } }); registerRpc(new EditorServerRpc() { @@ -3278,7 +3502,8 @@ public class Grid extends AbstractComponent implements SelectionNotifier, sortOrder.clear(); } - datasourceExtension = new RpcDataProviderExtension(container); + datasourceExtension = new RpcDataProviderExtension(container, + detailComponentManager); datasourceExtension.extend(this, columnKeys); /* @@ -4607,6 +4832,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier, } componentList.addAll(getEditorFields()); + + componentList.addAll(detailComponentManager.getComponents()); + return componentList.iterator(); } 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(); } |