aboutsummaryrefslogtreecommitdiffstats
path: root/server/src/com
diff options
context:
space:
mode:
authorTeemu Suo-Anttila <teemusa@vaadin.com>2015-05-02 10:53:26 +0000
committerVaadin Code Review <review@vaadin.com>2015-05-02 10:53:26 +0000
commit261ccc182903b963439216cc5bf8445913aa124b (patch)
treea1eea181ad23bfb9f5ec060937351779ada82707 /server/src/com
parent507c9736525ff793e5c98e2ae7686bbf9347aed6 (diff)
parentef9fb276a93d1a7967ef0e6405dee56c8aa2a148 (diff)
downloadvaadin-framework-261ccc182903b963439216cc5bf8445913aa124b.tar.gz
vaadin-framework-261ccc182903b963439216cc5bf8445913aa124b.zip
Merge "Merge branch 'grid-7.5'"
Diffstat (limited to 'server/src/com')
-rw-r--r--server/src/com/vaadin/data/RpcDataProviderExtension.java378
-rw-r--r--server/src/com/vaadin/ui/Grid.java576
2 files changed, 944 insertions, 10 deletions
diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java
index 5fb0742164..e645ec60f7 100644
--- a/server/src/com/vaadin/data/RpcDataProviderExtension.java
+++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java
@@ -25,11 +25,15 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Set;
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.google.gwt.thirdparty.guava.common.collect.Maps;
+import com.google.gwt.thirdparty.guava.common.collect.Sets;
import com.vaadin.data.Container.Indexed;
import com.vaadin.data.Container.Indexed.ItemAddEvent;
import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
@@ -45,12 +49,16 @@ import com.vaadin.server.ClientConnector;
import com.vaadin.server.KeyMapper;
import com.vaadin.shared.data.DataProviderRpc;
import com.vaadin.shared.data.DataRequestRpc;
+import com.vaadin.shared.ui.grid.DetailsConnectorChange;
import com.vaadin.shared.ui.grid.GridState;
import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.ui.Component;
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.DetailsGenerator;
import com.vaadin.ui.Grid.RowReference;
import com.vaadin.ui.Grid.RowStyleGenerator;
import com.vaadin.ui.renderers.Renderer;
@@ -110,11 +118,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 +135,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();
@@ -571,6 +584,270 @@ public class RpcDataProviderExtension extends AbstractExtension {
}
}
+ /**
+ * A class that makes detail component related internal communication
+ * possible between {@link RpcDataProviderExtension} and grid.
+ *
+ * @since 7.5.0
+ * @author Vaadin Ltd
+ */
+ public static 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();
+
+ private Grid grid;
+
+ /**
+ * 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);
+
+ if (visibleDetailsComponents.containsKey(itemId)) {
+ // Don't overwrite existing components
+ return;
+ }
+
+ RowReference rowReference = new RowReference(grid);
+ rowReference.set(itemId);
+
+ DetailsGenerator detailsGenerator = grid.getDetailsGenerator();
+ Component details = detailsGenerator.getDetails(rowReference);
+ if (details != null) {
+ String generatorName = detailsGenerator.getClass().getName();
+ if (details.getParent() != null) {
+ throw new IllegalStateException(generatorName
+ + " generated a details component that already "
+ + "was attached. (itemId: " + itemId + ", row: "
+ + rowIndex + ", component: " + details);
+ }
+
+ if (rowIndexToDetails.containsValue(details)) {
+ 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 assertItemIdHasNotMovedAndNothingIsOverwritten(itemId,
+ newRowIndex);
+ emptyDetails.put(itemId, newRowIndex);
+ }
+
+ /*
+ * Don't attach the components here. It's done by
+ * GridServerRpc.sendDetailsComponents in a separate roundtrip.
+ */
+ }
+
+ private boolean assertItemIdHasNotMovedAndNothingIsOverwritten(
+ Object itemId, Integer newRowIndex) {
+
+ Integer oldRowIndex = emptyDetails.get(itemId);
+ if (!SharedUtil.equals(oldRowIndex, newRowIndex)) {
+
+ assert !emptyDetails.containsKey(itemId) : "Unexpected "
+ + "change of empty details row index for itemId "
+ + itemId + " from " + oldRowIndex + " to "
+ + newRowIndex;
+
+ assert !emptyDetails.containsValue(newRowIndex) : "Bookkeeping"
+ + " already had another itemId for this empty index "
+ + "(index: " + newRowIndex + ", new itemId: " + itemId
+ + ")";
+ }
+
+ return true;
+ }
+
+ /**
+ * 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);
+ grid.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
+ */
+ public 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
+ */
+ public 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);
+ unattachedComponents.remove(component);
+ }
+
+ if (!SharedUtil.equals(oldIndex, newIndex)) {
+ changes.add(new DetailsConnectorChange(component, oldIndex,
+ newIndex, emptyDetails.containsKey(component)));
+ }
+ }
+
+ // 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, emptyDetails.containsValue(oldIndex)));
+ }
+ }
+
+ // 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());
+ }
+
+ void setGrid(Grid grid) {
+ if (this.grid != null) {
+ throw new IllegalStateException("Grid may injected only once.");
+ }
+ this.grid = grid;
+ }
+ }
+
private final Indexed container;
private final ActiveRowHandler activeRowHandler = new ActiveRowHandler();
@@ -673,6 +950,14 @@ 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 final DetailComponentManager detailComponentManager = new DetailComponentManager();
+
+ /**
* Creates a new data provider using the given container.
*
* @param container
@@ -814,6 +1099,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();
@@ -863,9 +1152,12 @@ public class RpcDataProviderExtension extends AbstractExtension {
*
* @param component
* the remote data grid component to extend
+ * @param columnKeys
+ * the key mapper for columns
*/
public void extend(Grid component, KeyMapper<Object> columnKeys) {
this.columnKeys = columnKeys;
+ detailComponentManager.setGrid(component);
super.extend(component);
}
@@ -949,6 +1241,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 +1367,84 @@ 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 7.5.0
+ * @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 7.5.0
+ * @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);
+ }
+
+ /** Gets the detail component manager for this data provider */
+ public DetailComponentManager getDetailComponentManager() {
+ return detailComponentManager;
+ }
}
diff --git a/server/src/com/vaadin/ui/Grid.java b/server/src/com/vaadin/ui/Grid.java
index 22d6f009e4..f1f17405e8 100644
--- a/server/src/com/vaadin/ui/Grid.java
+++ b/server/src/com/vaadin/ui/Grid.java
@@ -18,6 +18,7 @@ package com.vaadin.ui;
import java.io.Serializable;
import java.lang.reflect.Method;
+import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -52,10 +53,10 @@ import com.vaadin.data.Item;
import com.vaadin.data.Property;
import com.vaadin.data.RpcDataProviderExtension;
import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper;
+import com.vaadin.data.RpcDataProviderExtension.DetailComponentManager;
import com.vaadin.data.Validator.InvalidValueException;
import com.vaadin.data.fieldgroup.DefaultFieldGroupFieldFactory;
import com.vaadin.data.fieldgroup.FieldGroup;
-import com.vaadin.data.fieldgroup.FieldGroup.BindException;
import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
import com.vaadin.data.fieldgroup.FieldGroupFieldFactory;
import com.vaadin.data.sort.Sort;
@@ -74,6 +75,7 @@ import com.vaadin.event.SortEvent.SortListener;
import com.vaadin.event.SortEvent.SortNotifier;
import com.vaadin.server.AbstractClientConnector;
import com.vaadin.server.AbstractExtension;
+import com.vaadin.server.EncodeResult;
import com.vaadin.server.ErrorMessage;
import com.vaadin.server.JsonCodec;
import com.vaadin.server.KeyMapper;
@@ -174,6 +176,120 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
SortNotifier, SelectiveRenderer, ItemClickNotifier {
/**
+ * An event listener for column visibility change events in the Grid.
+ *
+ * @since 7.5.0
+ */
+ public interface ColumnVisibilityChangeListener extends Serializable {
+ /**
+ * Called when a column has become hidden or unhidden.
+ *
+ * @param event
+ */
+ void columnVisibilityChanged(ColumnVisibilityChangeEvent event);
+ }
+
+ /**
+ * An event that is fired when a column's visibility changes.
+ *
+ * @since 7.5.0
+ */
+ public static class ColumnVisibilityChangeEvent extends Component.Event {
+
+ private final Column column;
+ private final boolean userOriginated;
+ private final boolean hidden;
+
+ /**
+ * Constructor for a column visibility change event.
+ *
+ * @param source
+ * the grid from which this event originates
+ * @param column
+ * the column that changed its visibility
+ * @param hidden
+ * <code>true</code> if the column was hidden,
+ * <code>false</code> if it became visible
+ * @param isUserOriginated
+ * <code>true</code> iff the event was triggered by an UI
+ * interaction
+ */
+ public ColumnVisibilityChangeEvent(Grid source, Column column,
+ boolean hidden, boolean isUserOriginated) {
+ super(source);
+ this.column = column;
+ this.hidden = hidden;
+ userOriginated = isUserOriginated;
+ }
+
+ /**
+ * Gets the column that became hidden or visible.
+ *
+ * @return the column that became hidden or visible.
+ * @see Column#isHidden()
+ */
+ public Column getColumn() {
+ return column;
+ }
+
+ /**
+ * Was the column set hidden or visible.
+ *
+ * @return <code>true</code> if the column was hidden <code>false</code>
+ * if it was set visible
+ */
+ public boolean isHidden() {
+ return hidden;
+ }
+
+ /**
+ * Returns <code>true</code> if the column reorder was done by the user,
+ * <code>false</code> if not and it was triggered by server side code.
+ *
+ * @return <code>true</code> if event is a result of user interaction
+ */
+ public boolean isUserOriginated() {
+ return userOriginated;
+ }
+ }
+
+ /**
+ * A callback interface for generating details for a particular row in Grid.
+ *
+ * @since 7.5.0
+ * @author Vaadin Ltd
+ * @see DetailsGenerator#NULL
+ */
+ 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. Essentially, this should mostly be a
+ * self-contained fire-and-forget method, as external references to the
+ * generated component might cause unexpected behavior.
+ *
+ * @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.
*/
@@ -343,6 +459,58 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
}
/**
+ * An event listener for column reorder events in the Grid.
+ *
+ * @since 7.5.0
+ */
+ public interface ColumnReorderListener extends Serializable {
+ /**
+ * Called when the columns of the grid have been reordered.
+ *
+ * @param event
+ * An event providing more information
+ */
+ void columnReorder(ColumnReorderEvent event);
+ }
+
+ /**
+ * An event that is fired when the columns are reordered.
+ *
+ * @since 7.5.0
+ */
+ public static class ColumnReorderEvent extends Component.Event {
+
+ /**
+ * Is the column reorder related to this event initiated by the user
+ */
+ private final boolean userOriginated;
+
+ /**
+ *
+ * @param source
+ * the grid where the event originated from
+ * @param userOriginated
+ * <code>true</code> if event is a result of user
+ * interaction, <code>false</code> if from API call
+ */
+ public ColumnReorderEvent(Grid source, boolean userOriginated) {
+ super(source);
+ this.userOriginated = userOriginated;
+ }
+
+ /**
+ * Returns <code>true</code> if the column reorder was done by the user,
+ * <code>false</code> if not and it was triggered by server side code.
+ *
+ * @return <code>true</code> if event is a result of user interaction
+ */
+ public boolean isUserOriginated() {
+ return userOriginated;
+ }
+
+ }
+
+ /**
* Default error handler for the editor
*
*/
@@ -2356,6 +2524,46 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
}
/**
+ * Gets the caption of the hiding toggle for this column.
+ *
+ * @since
+ * @see #setHidingToggleCaption(String)
+ * @return the caption for the hiding toggle for this column
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public String getHidingToggleCaption() throws IllegalStateException {
+ checkColumnIsAttached();
+ return state.hidingToggleCaption;
+ }
+
+ /**
+ * Sets the caption of the hiding toggle for this column. Shown in the
+ * toggle for this column in the grid's sidebar when the column is
+ * {@link #isHidable() hidable}.
+ * <p>
+ * By default, before triggering this setter, a user friendly version of
+ * the column's {@link #getPropertyId() property id} is used.
+ * <p>
+ * <em>NOTE:</em> setting this to <code>null</code> or empty string
+ * might cause the hiding toggle to not render correctly.
+ *
+ * @since
+ * @param hidingToggleCaption
+ * the text to show in the column hiding toggle
+ * @return the column itself
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public Column setHidingToggleCaption(String hidingToggleCaption)
+ throws IllegalStateException {
+ checkColumnIsAttached();
+ state.hidingToggleCaption = hidingToggleCaption;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
* Returns the width (in pixels). By default a column is 100px wide.
*
* @return the width in pixels of the column
@@ -2885,7 +3093,8 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
* Getting a field before the editor has been opened depends on special
* support from the {@link FieldGroup} in use. Using this method with a
* user-provided <code>FieldGroup</code> might cause
- * {@link BindException} to be thrown.
+ * {@link com.vaadin.data.fieldgroup.FieldGroup.BindException
+ * BindException} to be thrown.
*
* @return the bound field; or <code>null</code> if the respective
* column is not editable
@@ -2901,13 +3110,79 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
}
/**
+ * Hides or shows the column. By default columns are visible before
+ * explicitly hiding them.
+ *
+ * @since 7.5.0
+ * @param hidden
+ * <code>true</code> to hide the column, <code>false</code>
+ * to show
+ * @return this column
+ */
+ public Column setHidden(boolean hidden) {
+ if (hidden != getState().hidden) {
+ getState().hidden = hidden;
+ grid.markAsDirty();
+ grid.fireColumnVisibilityChangeEvent(this, hidden, false);
+ }
+ return this;
+ }
+
+ /**
+ * Is this column hidden. Default is {@code false}.
+ *
+ * @since 7.5.0
+ * @return <code>true</code> if the column is currently hidden,
+ * <code>false</code> otherwise
+ */
+ public boolean isHidden() {
+ return getState().hidden;
+ }
+
+ /**
+ * Set whether it is possible for the user to hide this column or not.
+ * Default is {@code false}.
+ * <p>
+ * <em>Note:</em> it is still possible to hide the column
+ * programmatically using {@link #setHidden(boolean)}
+ *
+ * @since 7.5.0
+ * @param hidable
+ * <code>true</code> iff the column may be hidable by the
+ * user via UI interaction
+ * @return this column
+ */
+ public Column setHidable(boolean hidable) {
+ if (hidable != getState().hidable) {
+ getState().hidable = hidable;
+ grid.markAsDirty();
+ }
+ return this;
+ }
+
+ /**
+ * Is it possible for the the user to hide this column. Default is
+ * {@code false}.
+ * <p>
+ * <em>Note:</em> the column can be programmatically hidden using
+ * {@link #setHidden(boolean)} regardless of the returned value.
+ *
+ * @since 7.5.0
+ * @return <code>true</code> if the user can hide the column,
+ * <code>false</code> if not
+ */
+ public boolean isHidable() {
+ return getState().hidable;
+ }
+
+ /*
* Writes the design attributes for this column into given element.
*
* @since
- * @param design
- * Element to write attributes into
- * @param designContext
- * the design context
+ *
+ * @param design Element to write attributes into
+ *
+ * @param designContext the design context
*/
protected void writeDesign(Element design, DesignContext designContext) {
Attributes attributes = design.attributes();
@@ -2925,6 +3200,14 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
getMaximumWidth(), def.maxWidth, Double.class);
DesignAttributeHandler.writeAttribute("expand", attributes,
getExpandRatio(), def.expandRatio, Integer.class);
+ DesignAttributeHandler.writeAttribute("hidable", attributes,
+ isHidable(), def.hidable, boolean.class);
+ DesignAttributeHandler.writeAttribute("hidden", attributes,
+ isHidden(), def.hidden, boolean.class);
+ DesignAttributeHandler.writeAttribute("hiding-toggle-caption",
+ attributes, getHidingToggleCaption(),
+ SharedUtil.propertyIdToHumanFriendly(getPropertyId()),
+ String.class);
DesignAttributeHandler.writeAttribute("property-id", attributes,
getPropertyId(), null, Object.class);
}
@@ -2950,7 +3233,18 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
setEditable(DesignAttributeHandler.readAttribute("editable",
attributes, boolean.class));
}
-
+ if (design.hasAttr("hidable")) {
+ setHidable(DesignAttributeHandler.readAttribute("hidable",
+ attributes, boolean.class));
+ }
+ if (design.hasAttr("hidden")) {
+ setHidden(DesignAttributeHandler.readAttribute("hidden",
+ attributes, boolean.class));
+ }
+ if (design.hasAttr("hiding-toggle-caption")) {
+ setHidingToggleCaption(DesignAttributeHandler.readAttribute(
+ "hiding-toggle-caption", attributes, String.class));
+ }
// Read size info where necessary.
if (design.hasAttr("width")) {
setWidth(DesignAttributeHandler.readAttribute("width",
@@ -3202,12 +3496,30 @@ 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 DetailComponentManager detailComponentManager = null;
+
private static final Method SELECTION_CHANGE_METHOD = ReflectTools
.findMethod(SelectionListener.class, "select", SelectionEvent.class);
private static final Method SORT_ORDER_CHANGE_METHOD = ReflectTools
.findMethod(SortListener.class, "sort", SortEvent.class);
+ private static final Method COLUMN_REORDER_METHOD = ReflectTools
+ .findMethod(ColumnReorderListener.class, "columnReorder",
+ ColumnReorderEvent.class);
+
+ private static final Method COLUMN_VISIBILITY_METHOD = ReflectTools
+ .findMethod(ColumnVisibilityChangeListener.class,
+ "columnVisibilityChanged",
+ ColumnVisibilityChangeEvent.class);
+
/**
* Creates a new Grid with a new {@link IndexedContainer} as the data
* source.
@@ -3402,6 +3714,87 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
fireEvent(new ItemClickEvent(Grid.this, item, itemId,
propertyId, details));
}
+
+ @Override
+ public void columnsReordered(List<String> newColumnOrder,
+ List<String> oldColumnOrder) {
+ final String diffStateKey = "columnOrder";
+ ConnectorTracker connectorTracker = getUI()
+ .getConnectorTracker();
+ JsonObject diffState = connectorTracker.getDiffState(Grid.this);
+ // discard the change if the columns have been reordered from
+ // the server side, as the server side is always right
+ if (getState(false).columnOrder.equals(oldColumnOrder)) {
+ // Don't mark as dirty since client has the state already
+ getState(false).columnOrder = newColumnOrder;
+ // write changes to diffState so that possible reverting the
+ // column order is sent to client
+ assert diffState.hasKey(diffStateKey) : "Field name has changed";
+ Type type = null;
+ try {
+ type = (getState(false).getClass().getDeclaredField(
+ diffStateKey).getGenericType());
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ } catch (SecurityException e) {
+ e.printStackTrace();
+ }
+ EncodeResult encodeResult = JsonCodec.encode(
+ getState(false).columnOrder, diffState, type,
+ connectorTracker);
+
+ diffState.put(diffStateKey, encodeResult.getEncodedValue());
+ fireColumnReorderEvent(true);
+ } else {
+ // make sure the client is reverted to the order that the
+ // server thinks it is
+ diffState.remove(diffStateKey);
+ markAsDirty();
+ }
+ }
+
+ @Override
+ public void columnVisibilityChanged(String id, boolean hidden,
+ boolean userOriginated) {
+ final Column column = getColumnByColumnId(id);
+ final GridColumnState columnState = column.getState();
+
+ if (columnState.hidden != hidden) {
+ columnState.hidden = hidden;
+
+ final String diffStateKey = "columns";
+ ConnectorTracker connectorTracker = getUI()
+ .getConnectorTracker();
+ JsonObject diffState = connectorTracker
+ .getDiffState(Grid.this);
+
+ assert diffState.hasKey(diffStateKey) : "Field name has changed";
+ Type type = null;
+ try {
+ type = (getState(false).getClass().getDeclaredField(
+ diffStateKey).getGenericType());
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ } catch (SecurityException e) {
+ e.printStackTrace();
+ }
+ EncodeResult encodeResult = JsonCodec.encode(
+ getState(false).columns, diffState, type,
+ connectorTracker);
+
+ diffState.put(diffStateKey, encodeResult.getEncodedValue());
+
+ fireColumnVisibilityChangeEvent(column, hidden,
+ userOriginated);
+ }
+ }
+
+ @Override
+ public void sendDetailsComponents(int fetchId) {
+ getRpcProxy(GridClientRpc.class).setDetailsConnectorChanges(
+ detailComponentManager.getAndResetConnectorChanges(),
+ fetchId);
+ }
});
registerRpc(new EditorServerRpc() {
@@ -3565,6 +3958,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
datasourceExtension = new RpcDataProviderExtension(container);
datasourceExtension.extend(this, columnKeys);
+ detailComponentManager = datasourceExtension
+ .getDetailComponentManager();
+
/*
* selectionModel == null when the invocation comes from the
* constructor.
@@ -3783,6 +4179,31 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
return columnKeys.get(columnId);
}
+ /**
+ * Returns whether column reordering is allowed. Default value is
+ * <code>false</code>.
+ *
+ * @since 7.5.0
+ * @return true if reordering is allowed
+ */
+ public boolean isColumnReorderingAllowed() {
+ return getState(false).columnReorderingAllowed;
+ }
+
+ /**
+ * Sets whether or not column reordering is allowed. Default value is
+ * <code>false</code>.
+ *
+ * @since 7.5.0
+ * @param columnReorderingAllowed
+ * specifies whether column reordering is allowed
+ */
+ public void setColumnReorderingAllowed(boolean columnReorderingAllowed) {
+ if (isColumnReorderingAllowed() != columnReorderingAllowed) {
+ getState().columnReorderingAllowed = columnReorderingAllowed;
+ }
+ }
+
@Override
protected GridState getState() {
return (GridState) super.getState();
@@ -3818,8 +4239,10 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
header.addColumn(datasourcePropertyId);
footer.addColumn(datasourcePropertyId);
- column.setHeaderCaption(SharedUtil.propertyIdToHumanFriendly(String
- .valueOf(datasourcePropertyId)));
+ String humanFriendlyPropertyId = SharedUtil
+ .propertyIdToHumanFriendly(String.valueOf(datasourcePropertyId));
+ column.setHeaderCaption(humanFriendlyPropertyId);
+ column.setHidingToggleCaption(humanFriendlyPropertyId);
if (datasource instanceof Sortable
&& ((Sortable) datasource).getSortableContainerPropertyIds()
@@ -3910,6 +4333,7 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
columnOrder.addAll(stateColumnOrder);
}
getState().columnOrder = columnOrder;
+ fireColumnReorderEvent(false);
}
/**
@@ -3941,6 +4365,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
* columns will be frozen, but the built-in selection checkbox column will
* still be frozen if it's in use. -1 means that not even the selection
* column is frozen.
+ * <p>
+ * <em>NOTE:</em> this count includes {@link Column#isHidden() hidden
+ * columns} in the count.
*
* @see #setFrozenColumnCount(int)
*
@@ -3952,6 +4379,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
/**
* Scrolls to a certain item, using {@link ScrollDestination#ANY}.
+ * <p>
+ * If the item has visible details, its size will also be taken into
+ * account.
*
* @param itemId
* id of item to scroll to.
@@ -3964,6 +4394,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
/**
* Scrolls to a certain item, using user-specified scroll destination.
+ * <p>
+ * If the item has visible details, its size will also be taken into
+ * account.
*
* @param itemId
* id of item to scroll to.
@@ -4376,6 +4809,33 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
removeListener(SelectionEvent.class, listener, SELECTION_CHANGE_METHOD);
}
+ private void fireColumnReorderEvent(boolean userOriginated) {
+ fireEvent(new ColumnReorderEvent(this, userOriginated));
+ }
+
+ /**
+ * Registers a new column reorder listener.
+ *
+ * @since 7.5.0
+ * @param listener
+ * the listener to register
+ */
+ public void addColumnReorderListener(ColumnReorderListener listener) {
+ addListener(ColumnReorderEvent.class, listener, COLUMN_REORDER_METHOD);
+ }
+
+ /**
+ * Removes a previously registered column reorder listener.
+ *
+ * @since 7.5.0
+ * @param listener
+ * the listener to remove
+ */
+ public void removeColumnReorderListener(ColumnReorderListener listener) {
+ removeListener(ColumnReorderEvent.class, listener,
+ COLUMN_REORDER_METHOD);
+ }
+
/**
* Gets the
* {@link com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper
@@ -4920,6 +5380,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
}
componentList.addAll(getEditorFields());
+
+ componentList.addAll(detailComponentManager.getComponents());
+
return componentList.iterator();
}
@@ -5437,6 +5900,101 @@ public class Grid extends AbstractComponent implements SelectionNotifier,
getRpcProxy(GridClientRpc.class).recalculateColumnWidths();
}
+ /**
+ * Registers a new column visibility change listener
+ *
+ * @since 7.5.0
+ * @param listener
+ * the listener to register
+ */
+ public void addColumnVisibilityChangeListener(
+ ColumnVisibilityChangeListener listener) {
+ addListener(ColumnVisibilityChangeEvent.class, listener,
+ COLUMN_VISIBILITY_METHOD);
+ }
+
+ /**
+ * Removes a previously registered column visibility change listener
+ *
+ * @since 7.5.0
+ * @param listener
+ * the listener to remove
+ */
+ public void removeColumnVisibilityChangeListener(
+ ColumnVisibilityChangeListener listener) {
+ removeListener(ColumnVisibilityChangeEvent.class, listener,
+ COLUMN_VISIBILITY_METHOD);
+ }
+
+ private void fireColumnVisibilityChangeEvent(Column column, boolean hidden,
+ boolean isUserOriginated) {
+ fireEvent(new ColumnVisibilityChangeEvent(this, column, hidden,
+ isUserOriginated));
+ }
+
+ /**
+ * Sets a new details generator for row details.
+ * <p>
+ * The currently opened row details will be re-rendered.
+ *
+ * @since 7.5.0
+ * @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 7.5.0
+ * @return the detailsGenerator the current details generator
+ */
+ public DetailsGenerator getDetailsGenerator() {
+ return detailsGenerator;
+ }
+
+ /**
+ * Shows or hides the details for a specific item.
+ *
+ * @since 7.5.0
+ * @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 7.5.0
+ * @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);
+ }
+
protected SelectionMode getDefaultSelectionMode() {
return SelectionMode.SINGLE;
}