summaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/src/com/vaadin/data/RpcDataProviderExtension.java148
-rw-r--r--server/src/com/vaadin/ui/components/grid/ColumnGroup.java165
-rw-r--r--server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java303
-rw-r--r--server/src/com/vaadin/ui/components/grid/Grid.java861
-rw-r--r--server/src/com/vaadin/ui/components/grid/GridColumn.java216
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/GridColumnGroups.java265
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java228
7 files changed, 2186 insertions, 0 deletions
diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java
new file mode 100644
index 0000000000..b22e6a209b
--- /dev/null
+++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.data;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.server.AbstractExtension;
+import com.vaadin.shared.data.DataProviderRpc;
+import com.vaadin.shared.data.DataProviderState;
+import com.vaadin.shared.data.DataRequestRpc;
+import com.vaadin.ui.components.grid.Grid;
+
+/**
+ * Provides Vaadin server-side container data source to a
+ * {@link com.vaadin.client.ui.grid.GridConnector}. This is currently
+ * implemented as an Extension hardcoded to support a specific connector type.
+ * This will be changed once framework support for something more flexible has
+ * been implemented.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class RpcDataProviderExtension extends AbstractExtension {
+
+ private final Indexed container;
+
+ /**
+ * Creates a new data provider using the given container.
+ *
+ * @param container
+ * the container to make available
+ */
+ public RpcDataProviderExtension(Indexed container) {
+ this.container = container;
+
+ // TODO support for reacting to events from the container added later
+
+ registerRpc(new DataRequestRpc() {
+ @Override
+ public void requestRows(int firstRow, int numberOfRows) {
+ pushRows(firstRow, numberOfRows);
+ }
+ });
+
+ getState().containerSize = container.size();
+ }
+
+ private void pushRows(int firstRow, int numberOfRows) {
+ List<?> itemIds = container.getItemIds(firstRow, numberOfRows);
+ Collection<?> propertyIds = container.getContainerPropertyIds();
+ List<String[]> rows = new ArrayList<String[]>(itemIds.size());
+ for (Object itemId : itemIds) {
+ rows.add(getRowData(propertyIds, itemId));
+ }
+ getRpcProxy(DataProviderRpc.class).setRowData(firstRow, rows);
+ }
+
+ private String[] getRowData(Collection<?> propertyIds, Object itemId) {
+ Item item = container.getItem(itemId);
+ String[] row = new String[propertyIds.size()];
+
+ int i = 0;
+ for (Object propertyId : propertyIds) {
+ Object value = item.getItemProperty(propertyId).getValue();
+ String stringValue = String.valueOf(value);
+ row[i++] = stringValue;
+ }
+ return row;
+ }
+
+ @Override
+ protected DataProviderState getState() {
+ return (DataProviderState) super.getState();
+ }
+
+ /**
+ * Makes the data source available to the given {@link Grid} component.
+ *
+ * @param component
+ * the remote data grid component to extend
+ */
+ public void extend(Grid component) {
+ super.extend(component);
+ }
+
+ /**
+ * Informs the client side that new rows have been inserted into the data
+ * source.
+ *
+ * @param index
+ * the index at which new rows have been inserted
+ * @param count
+ * the number of rows inserted at <code>index</code>
+ */
+ public void insertRowData(int index, int count) {
+ getState().containerSize += count;
+ getRpcProxy(DataProviderRpc.class).insertRowData(index, count);
+ }
+
+ /**
+ * Informs the client side that rows have been removed from the data source.
+ *
+ * @param firstIndex
+ * the index of the first row removed
+ * @param count
+ * the number of rows removed
+ */
+ public void removeRowData(int firstIndex, int count) {
+ getState().containerSize -= count;
+ getRpcProxy(DataProviderRpc.class).removeRowData(firstIndex, count);
+ }
+
+ /**
+ * Informs the client side that data of a row has been modified in the data
+ * source.
+ *
+ * @param index
+ * the index of the row that was updated
+ */
+ public void updateRowData(int index) {
+ /*
+ * TODO: ignore duplicate requests for the same index during the same
+ * roundtrip.
+ */
+ Object itemId = container.getIdByIndex(index);
+ String[] row = getRowData(container.getContainerPropertyIds(), itemId);
+ getRpcProxy(DataProviderRpc.class).setRowData(index,
+ Collections.singletonList(row));
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/ColumnGroup.java b/server/src/com/vaadin/ui/components/grid/ColumnGroup.java
new file mode 100644
index 0000000000..6b14ef81d4
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/ColumnGroup.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.ui.components.grid;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import com.vaadin.shared.ui.grid.ColumnGroupState;
+
+/**
+ * Column groups are used to group columns together for adding common auxiliary
+ * headers and footers. Columns groups are added to {@link ColumnGroupRow}'s.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class ColumnGroup implements Serializable {
+
+ /**
+ * List of property ids belonging to this group
+ */
+ private List<Object> columns;
+
+ /**
+ * The grid the column group is associated with
+ */
+ private final Grid grid;
+
+ /**
+ * The column group row the column group is attached to
+ */
+ private final ColumnGroupRow row;
+
+ /**
+ * The common state between the server and the client
+ */
+ private final ColumnGroupState state;
+
+ /**
+ * Constructs a new column group
+ *
+ * @param grid
+ * the grid the column group is associated with
+ * @param state
+ * the state representing the data of the grid. Sent to the
+ * client
+ * @param propertyIds
+ * the property ids of the columns that belongs to the group
+ * @param groups
+ * the sub groups who should be included in this group
+ *
+ */
+ ColumnGroup(Grid grid, ColumnGroupRow row, ColumnGroupState state,
+ List<Object> propertyIds) {
+ if (propertyIds == null) {
+ throw new IllegalArgumentException(
+ "propertyIds cannot be null. Use empty list instead.");
+ }
+
+ this.state = state;
+ this.row = row;
+ columns = Collections.unmodifiableList(new ArrayList<Object>(
+ propertyIds));
+ this.grid = grid;
+ }
+
+ /**
+ * Sets the text displayed in the header of the column group.
+ *
+ * @param header
+ * the text displayed in the header of the column
+ */
+ public void setHeaderCaption(String header) {
+ checkGroupIsAttached();
+ state.header = header;
+ grid.markAsDirty();
+ }
+
+ /**
+ * Sets the text displayed in the header of the column group.
+ *
+ * @return the text displayed in the header of the column
+ */
+ public String getHeaderCaption() {
+ checkGroupIsAttached();
+ return state.header;
+ }
+
+ /**
+ * Sets the text displayed in the footer of the column group.
+ *
+ * @param footer
+ * the text displayed in the footer of the column
+ */
+ public void setFooterCaption(String footer) {
+ checkGroupIsAttached();
+ state.footer = footer;
+ grid.markAsDirty();
+ }
+
+ /**
+ * The text displayed in the footer of the column group.
+ *
+ * @return the text displayed in the footer of the column
+ */
+ public String getFooterCaption() {
+ checkGroupIsAttached();
+ return state.footer;
+ }
+
+ /**
+ * Is a property id in this group or in some sub group of this group.
+ *
+ * @param propertyId
+ * the property id to check for
+ * @return <code>true</code> if the property id is included in this group.
+ */
+ public boolean isColumnInGroup(Object propertyId) {
+ if (columns.contains(propertyId)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns a list of property ids where all also the child groups property
+ * ids are included.
+ *
+ * @return a unmodifiable list with all the columns in the group. Includes
+ * any subgroup columns as well.
+ */
+ public List<Object> getColumns() {
+ return columns;
+ }
+
+ /**
+ * Checks if column group is attached to a row and throws an
+ * {@link IllegalStateException} if it is not.
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ protected void checkGroupIsAttached() throws IllegalStateException {
+ if (!row.getState().groups.contains(state)) {
+ throw new IllegalStateException(
+ "Column Group has been removed from the row.");
+ }
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java b/server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java
new file mode 100644
index 0000000000..b90b4df2c5
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.ui.components.grid;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import com.vaadin.server.KeyMapper;
+import com.vaadin.shared.ui.grid.ColumnGroupRowState;
+import com.vaadin.shared.ui.grid.ColumnGroupState;
+
+/**
+ * A column group row represents an auxiliary header or footer row added to the
+ * grid. A column group row includes column groups that group columns together.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class ColumnGroupRow implements Serializable {
+
+ /**
+ * The common state shared between the client and server
+ */
+ private final ColumnGroupRowState state;
+
+ /**
+ * The column groups in this row
+ */
+ private List<ColumnGroup> groups = new ArrayList<ColumnGroup>();
+
+ /**
+ * Grid that the group row belongs to
+ */
+ private final Grid grid;
+
+ /**
+ * The column keys used to identify the column on the client side
+ */
+ private final KeyMapper<Object> columnKeys;
+
+ /**
+ * Constructs a new column group
+ *
+ * @param grid
+ * The grid that the column group is associated to
+ * @param state
+ * The shared state which contains the data shared between server
+ * and client
+ * @param columnKeys
+ * The column key mapper for converting property ids to client
+ * side column identifiers
+ */
+ ColumnGroupRow(Grid grid, ColumnGroupRowState state,
+ KeyMapper<Object> columnKeys) {
+ this.grid = grid;
+ this.columnKeys = columnKeys;
+ this.state = state;
+ }
+
+ /**
+ * Gets the shared state for the column group row. Used internally to send
+ * the group row to the client.
+ *
+ * @return The current state of the row
+ */
+ ColumnGroupRowState getState() {
+ return state;
+ }
+
+ /**
+ * Add a new group to the row by using property ids for the columns.
+ *
+ * @param propertyIds
+ * The property ids of the columns that should be included in the
+ * group. A column can only belong in group on a row at a time.
+ * @return a column group representing the collection of columns added to
+ * the group
+ */
+ public ColumnGroup addGroup(Object... propertyIds)
+ throws IllegalArgumentException {
+ assert propertyIds != null : "propertyIds cannot be null.";
+
+ for (Object propertyId : propertyIds) {
+ if (hasColumnBeenGrouped(propertyId)) {
+ throw new IllegalArgumentException("Column "
+ + String.valueOf(propertyId)
+ + " already belongs to another group.");
+ }
+ }
+
+ validateNewGroupProperties(Arrays.asList(propertyIds));
+
+ ColumnGroupState state = new ColumnGroupState();
+ for (Object propertyId : propertyIds) {
+ assert propertyId != null : "null items in columns array not supported.";
+ state.columns.add(columnKeys.key(propertyId));
+ }
+ this.state.groups.add(state);
+
+ ColumnGroup group = new ColumnGroup(grid, this, state,
+ Arrays.asList(propertyIds));
+ groups.add(group);
+
+ grid.markAsDirty();
+ return group;
+ }
+
+ private void validateNewGroupProperties(List<Object> propertyIds)
+ throws IllegalArgumentException {
+
+ /*
+ * Validate parent grouping
+ */
+ int rowIndex = grid.getColumnGroupRows().indexOf(this);
+ int parentRowIndex = rowIndex - 1;
+
+ // Get the parent row of this row.
+ ColumnGroupRow parentRow = null;
+ if (parentRowIndex > -1) {
+ parentRow = grid.getColumnGroupRows().get(parentRowIndex);
+ }
+
+ if (parentRow == null) {
+ // A parentless row is always valid and is usually the first row
+ // added to the grid
+ return;
+ }
+
+ for (Object id : propertyIds) {
+ if (parentRow.hasColumnBeenGrouped(id)) {
+ /*
+ * If a property has been grouped in the parent row then all of
+ * the properties in the parent group also needs to be included
+ * in the child group for the groups to be valid
+ */
+ ColumnGroup parentGroup = parentRow.getGroupForProperty(id);
+ if (!propertyIds.containsAll(parentGroup.getColumns())) {
+ throw new IllegalArgumentException(
+ "Grouped properties overlaps previous grouping bounderies");
+ }
+ }
+ }
+ }
+
+ /**
+ * Add a new group to the row by using column instances.
+ *
+ * @param columns
+ * the columns that should belong to the group
+ * @return a column group representing the collection of columns added to
+ * the group
+ */
+ public ColumnGroup addGroup(GridColumn... columns)
+ throws IllegalArgumentException {
+ assert columns != null : "columns cannot be null";
+
+ List<Object> propertyIds = new ArrayList<Object>();
+ for (GridColumn column : columns) {
+ assert column != null : "null items in columns array not supported.";
+
+ String columnId = column.getState().id;
+ Object propertyId = grid.getPropertyIdByColumnId(columnId);
+ propertyIds.add(propertyId);
+ }
+ return addGroup(propertyIds.toArray());
+ }
+
+ /**
+ * Add a new group to the row by using other already greated groups
+ *
+ * @param groups
+ * the subgroups of the group
+ * @return a column group representing the collection of columns added to
+ * the group
+ *
+ */
+ public ColumnGroup addGroup(ColumnGroup... groups)
+ throws IllegalArgumentException {
+ assert groups != null : "groups cannot be null";
+
+ // Gather all groups columns into one list
+ List<Object> propertyIds = new ArrayList<Object>();
+ for (ColumnGroup group : groups) {
+ propertyIds.addAll(group.getColumns());
+ }
+
+ validateNewGroupProperties(propertyIds);
+
+ ColumnGroupState state = new ColumnGroupState();
+ ColumnGroup group = new ColumnGroup(grid, this, state, propertyIds);
+ this.groups.add(group);
+
+ // Update state
+ for (Object propertyId : group.getColumns()) {
+ state.columns.add(columnKeys.key(propertyId));
+ }
+ this.state.groups.add(state);
+
+ grid.markAsDirty();
+ return group;
+ }
+
+ /**
+ * Removes a group from the row. Does not remove the group from subgroups,
+ * to remove it from the subgroup invoke removeGroup on the subgroup.
+ *
+ * @param group
+ * the group to remove
+ */
+ public void removeGroup(ColumnGroup group) {
+ int index = groups.indexOf(group);
+ groups.remove(index);
+ state.groups.remove(index);
+ grid.markAsDirty();
+ }
+
+ /**
+ * Get the groups in the row.
+ *
+ * @return unmodifiable list of groups in this row
+ */
+ public List<ColumnGroup> getGroups() {
+ return Collections.unmodifiableList(groups);
+ }
+
+ /**
+ * Checks if a property id has been added to a group in this row.
+ *
+ * @param propertyId
+ * the property id to check for
+ * @return <code>true</code> if the column is included in a group
+ */
+ private boolean hasColumnBeenGrouped(Object propertyId) {
+ return getGroupForProperty(propertyId) != null;
+ }
+
+ private ColumnGroup getGroupForProperty(Object propertyId) {
+ for (ColumnGroup group : groups) {
+ if (group.isColumnInGroup(propertyId)) {
+ return group;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Is the header visible for the row.
+ *
+ * @return <code>true</code> if header is visible
+ */
+ public boolean isHeaderVisible() {
+ return state.headerVisible;
+ }
+
+ /**
+ * Sets the header visible for the row.
+ *
+ * @param visible
+ * should the header be shown
+ */
+ public void setHeaderVisible(boolean visible) {
+ state.headerVisible = visible;
+ grid.markAsDirty();
+ }
+
+ /**
+ * Is the footer visible for the row.
+ *
+ * @return <code>true</code> if footer is visible
+ */
+ public boolean isFooterVisible() {
+ return state.footerVisible;
+ }
+
+ /**
+ * Sets the footer visible for the row.
+ *
+ * @param visible
+ * should the footer be shown
+ */
+ public void setFooterVisible(boolean visible) {
+ state.footerVisible = visible;
+ grid.markAsDirty();
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java
new file mode 100644
index 0000000000..4126ec6d93
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/Grid.java
@@ -0,0 +1,861 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.ui.components.grid;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Indexed.ItemAddEvent;
+import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
+import com.vaadin.data.Container.ItemSetChangeEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
+import com.vaadin.data.Container.ItemSetChangeNotifier;
+import com.vaadin.data.Container.PropertySetChangeEvent;
+import com.vaadin.data.Container.PropertySetChangeListener;
+import com.vaadin.data.Container.PropertySetChangeNotifier;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.Property.ValueChangeEvent;
+import com.vaadin.data.Property.ValueChangeListener;
+import com.vaadin.data.Property.ValueChangeNotifier;
+import com.vaadin.data.RpcDataProviderExtension;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.shared.ui.grid.ColumnGroupRowState;
+import com.vaadin.shared.ui.grid.GridClientRpc;
+import com.vaadin.shared.ui.grid.GridColumnState;
+import com.vaadin.shared.ui.grid.GridServerRpc;
+import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.ui.AbstractComponent;
+import com.vaadin.ui.Component;
+
+/**
+ * Data grid component
+ *
+ * <h3>Lazy loading</h3> TODO To be revised when the data data source
+ * implementation has been don.
+ *
+ * <h3>Columns</h3> The grid columns are based on the property ids of the
+ * underlying data source. Each property id represents one column in the grid.
+ * To retrive a column in the grid you can use {@link Grid#getColumn(Object)}
+ * with the property id of the column. A grid column contains properties like
+ * the width, the footer and header captions of the column.
+ *
+ * <h3>Auxiliary headers and footers</h3> TODO To be revised when column
+ * grouping is implemented.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class Grid extends AbstractComponent {
+
+ /**
+ * A helper class that handles the client-side Escalator logic relating to
+ * making sure that whatever is currently visible to the user, is properly
+ * initialized and otherwise handled on the server side (as far as
+ * requried).
+ * <p>
+ * This bookeeping includes, but is not limited to:
+ * <ul>
+ * <li>listening to the currently visible {@link Property Properties'} value
+ * changes on the server side and sending those back to the client; and
+ * <li>attaching and detaching {@link Component Components} from the Vaadin
+ * Component hierarchy.
+ * </ul>
+ */
+ private final class ActiveRowHandler {
+ /**
+ * A map from itemId to the value change listener used for all of its
+ * properties
+ */
+ private final Map<Object, GridValueChangeListener> valueChangeListeners = new HashMap<Object, GridValueChangeListener>();
+
+ /**
+ * The currently active range. Practically, it's the range of row
+ * indices being displayed currently.
+ */
+ private Range activeRange = Range.withLength(0, 0);
+
+ /**
+ * A hook for making sure that appropriate data is "active". All other
+ * rows should be "inactive".
+ * <p>
+ * "Active" can mean different things in different contexts. For
+ * example, only the Properties in the active range need
+ * ValueChangeListeners. Also, whenever a row with a Component becomes
+ * active, it needs to be attached (and conversely, when inactive, it
+ * needs to be detached).
+ *
+ * @param firstActiveRow
+ * the first active row
+ * @param activeRowCount
+ * the number of active rows
+ */
+ public void setActiveRows(int firstActiveRow, int activeRowCount) {
+
+ final Range newActiveRange = Range.withLength(firstActiveRow,
+ activeRowCount);
+
+ // TODO [[Components]] attach and detach components
+
+ /*-
+ * Example
+ *
+ * New Range: [3, 4, 5, 6, 7]
+ * Old Range: [1, 2, 3, 4, 5]
+ * Result: [1, 2][3, 4, 5] []
+ */
+ final Range[] depractionPartition = activeRange
+ .partitionWith(newActiveRange);
+ removeValueChangeListeners(depractionPartition[0]);
+ removeValueChangeListeners(depractionPartition[2]);
+
+ /*-
+ * Example
+ *
+ * Old Range: [1, 2, 3, 4, 5]
+ * New Range: [3, 4, 5, 6, 7]
+ * Result: [] [3, 4, 5][6, 7]
+ */
+ final Range[] activationPartition = newActiveRange
+ .partitionWith(activeRange);
+ addValueChangeListeners(activationPartition[0]);
+ addValueChangeListeners(activationPartition[2]);
+
+ activeRange = newActiveRange;
+ }
+
+ private void addValueChangeListeners(Range range) {
+ for (int i = range.getStart(); i < range.getEnd(); i++) {
+
+ final Object itemId = datasource.getIdByIndex(i);
+ final Item item = datasource.getItem(itemId);
+
+ if (valueChangeListeners.containsKey(itemId)) {
+ /*
+ * This might occur when items are removed from above the
+ * viewport, the escalator scrolls up to compensate, but the
+ * same items remain in the view: It looks as if one row was
+ * scrolled, when in fact the whole viewport was shifted up.
+ */
+ continue;
+ }
+
+ GridValueChangeListener listener = new GridValueChangeListener(
+ itemId);
+ valueChangeListeners.put(itemId, listener);
+
+ for (final Object propertyId : item.getItemPropertyIds()) {
+ final Property<?> property = item
+ .getItemProperty(propertyId);
+ if (property instanceof ValueChangeNotifier) {
+ ((ValueChangeNotifier) property)
+ .addValueChangeListener(listener);
+ }
+ }
+ }
+ }
+
+ private void removeValueChangeListeners(Range range) {
+ for (int i = range.getStart(); i < range.getEnd(); i++) {
+ final Object itemId = datasource.getIdByIndex(i);
+ final Item item = datasource.getItem(itemId);
+ final GridValueChangeListener listener = valueChangeListeners
+ .remove(itemId);
+
+ if (listener != null) {
+ for (final Object propertyId : item.getItemPropertyIds()) {
+ final Property<?> property = item
+ .getItemProperty(propertyId);
+
+ /*
+ * Because listener != null, we can be certain that this
+ * property is a ValueChangeNotifier: It wouldn't be
+ * inserted in addValueChangeListeners if the property
+ * wasn't a suitable type. I.e. No need for "instanceof"
+ * check.
+ */
+ ((ValueChangeNotifier) property)
+ .removeValueChangeListener(listener);
+ }
+ }
+ }
+ }
+
+ public void clear() {
+ removeValueChangeListeners(activeRange);
+ /*
+ * we're doing an assert for emptiness there (instead of a
+ * carte-blanche ".clear()"), to be absolutely sure that everything
+ * is cleaned up properly, and that we have no dangling listeners.
+ */
+ assert valueChangeListeners.isEmpty() : "GridValueChangeListeners are leaking";
+
+ activeRange = Range.withLength(0, 0);
+ }
+
+ /**
+ * Manages removed properties in active rows.
+ *
+ * @param removedPropertyIds
+ * the property ids that have been removed from the container
+ */
+ public void propertiesRemoved(Collection<Object> removedPropertyIds) {
+ /*
+ * no-op, for now.
+ *
+ * The Container should be responsible for cleaning out any
+ * ValueChangeListeners from removed Properties. Components will
+ * benefit from this, however.
+ */
+ }
+
+ /**
+ * Manages added properties in active rows.
+ *
+ * @param addedPropertyIds
+ * the property ids that have been added to the container
+ */
+ public void propertiesAdded(Collection<Object> addedPropertyIds) {
+ for (int i = activeRange.getStart(); i < activeRange.getEnd(); i++) {
+ final Object itemId = datasource.getIdByIndex(i);
+ final Item item = datasource.getItem(itemId);
+ final GridValueChangeListener listener = valueChangeListeners
+ .get(itemId);
+ assert (listener != null) : "a listener should've been pre-made by addValueChangeListeners";
+
+ for (final Object propertyId : addedPropertyIds) {
+ final Property<?> property = item
+ .getItemProperty(propertyId);
+ if (property instanceof ValueChangeNotifier) {
+ ((ValueChangeNotifier) property)
+ .addValueChangeListener(listener);
+ }
+ }
+ }
+ }
+
+ /**
+ * Handles the insertion of rows.
+ * <p>
+ * This method's responsibilities are to:
+ * <ul>
+ * <li>shift the internal bookkeeping by <code>count</code> if the
+ * insertion happens above currently active range
+ * <li>ignore rows inserted below the currently active range
+ * <li>shift (and deactivate) rows pushed out of view
+ * <li>activate rows that are inserted in the current viewport
+ * </ul>
+ *
+ * @param firstIndex
+ * the index of the first inserted rows
+ * @param count
+ * the number of rows inserted at <code>firstIndex</code>
+ */
+ public void insertRows(int firstIndex, int count) {
+ if (firstIndex < activeRange.getStart()) {
+ activeRange = activeRange.offsetBy(count);
+ } else if (firstIndex < activeRange.getEnd()) {
+ final Range deprecatedRange = Range.withLength(
+ activeRange.getEnd(), count);
+ removeValueChangeListeners(deprecatedRange);
+
+ final Range freshRange = Range.between(firstIndex, count);
+ addValueChangeListeners(freshRange);
+ } else {
+ // out of view, noop
+ }
+ }
+
+ /**
+ * Removes a single item by its id.
+ *
+ * @param itemId
+ * the id of the removed id. <em>Note:</em> this item does
+ * not exist anymore in the datasource
+ */
+ public void removeItemId(Object itemId) {
+ final GridValueChangeListener removedListener = valueChangeListeners
+ .remove(itemId);
+ if (removedListener != null) {
+ /*
+ * We removed an item from somewhere in the visible range, so we
+ * make the active range shorter. The empty hole will be filled
+ * by the client-side code when it asks for more information.
+ */
+ activeRange = Range.withLength(activeRange.getStart(),
+ activeRange.length() - 1);
+ }
+ }
+ }
+
+ /**
+ * A class to listen to changes in property values in the Container added
+ * with {@link Grid#setContainerDatasource(Container.Indexed)}, and notifies
+ * the data source to update the client-side representation of the modified
+ * item.
+ * <p>
+ * One instance of this class can (and should) be reused for all the
+ * properties in an item, since this class will inform that the entire row
+ * needs to be re-evaluated (in contrast to a property-based change
+ * management)
+ * <p>
+ * Since there's no Container-wide possibility to listen to any kind of
+ * value changes, an instance of this class needs to be attached to each and
+ * every Item's Property in the container.
+ *
+ * @see Grid#addValueChangeListener(Container, Object, Object)
+ * @see Grid#valueChangeListeners
+ */
+ private class GridValueChangeListener implements ValueChangeListener {
+ private final Object itemId;
+
+ public GridValueChangeListener(Object itemId) {
+ /*
+ * Using an assert instead of an exception throw, just to optimize
+ * prematurely
+ */
+ assert itemId != null : "null itemId not accepted";
+ this.itemId = itemId;
+ }
+
+ @Override
+ public void valueChange(ValueChangeEvent event) {
+ datasourceExtension.updateRowData(datasource.indexOfId(itemId));
+ }
+ }
+
+ /**
+ * The data source attached to the grid
+ */
+ private Container.Indexed datasource;
+
+ /**
+ * Property id to column instance mapping
+ */
+ private final Map<Object, GridColumn> columns = new HashMap<Object, GridColumn>();
+
+ /**
+ * Key generator for column server-to-client communication
+ */
+ private final KeyMapper<Object> columnKeys = new KeyMapper<Object>();
+
+ /**
+ * The column groups added to the grid
+ */
+ private final List<ColumnGroupRow> columnGroupRows = new ArrayList<ColumnGroupRow>();
+
+ /**
+ * Property listener for listening to changes in data source properties.
+ */
+ private final PropertySetChangeListener propertyListener = new PropertySetChangeListener() {
+
+ @Override
+ public void containerPropertySetChange(PropertySetChangeEvent event) {
+ Collection<?> properties = new HashSet<Object>(event.getContainer()
+ .getContainerPropertyIds());
+
+ // Cleanup columns that are no longer in grid
+ List<Object> removedColumns = new LinkedList<Object>();
+ for (Object columnId : columns.keySet()) {
+ if (!properties.contains(columnId)) {
+ removedColumns.add(columnId);
+ }
+ }
+ for (Object columnId : removedColumns) {
+ GridColumn column = columns.remove(columnId);
+ columnKeys.remove(columnId);
+ getState().columns.remove(column.getState());
+ }
+ activeRowHandler.propertiesRemoved(removedColumns);
+
+ // Add new columns
+ HashSet<Object> addedPropertyIds = new HashSet<Object>();
+ for (Object propertyId : properties) {
+ if (!columns.containsKey(propertyId)) {
+ appendColumn(propertyId);
+ addedPropertyIds.add(propertyId);
+ }
+ }
+ activeRowHandler.propertiesAdded(addedPropertyIds);
+
+ Object frozenPropertyId = columnKeys
+ .get(getState(false).lastFrozenColumnId);
+ if (!columns.containsKey(frozenPropertyId)) {
+ setLastFrozenPropertyId(null);
+ }
+ }
+ };
+
+ private ItemSetChangeListener itemListener = new ItemSetChangeListener() {
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+
+ if (event instanceof ItemAddEvent) {
+ ItemAddEvent addEvent = (ItemAddEvent) event;
+ int firstIndex = addEvent.getFirstIndex();
+ int count = addEvent.getAddedItemsCount();
+ datasourceExtension.insertRowData(firstIndex, count);
+ activeRowHandler.insertRows(firstIndex, count);
+ }
+
+ else if (event instanceof ItemRemoveEvent) {
+ ItemRemoveEvent removeEvent = (ItemRemoveEvent) event;
+ int firstIndex = removeEvent.getFirstIndex();
+ int count = removeEvent.getRemovedItemsCount();
+ datasourceExtension.removeRowData(firstIndex, count);
+
+ /*
+ * Unfortunately, there's no sane way of getting the rest of the
+ * removed itemIds.
+ *
+ * Fortunately, the only time _currently_ an event with more
+ * than one removed item seems to be when calling
+ * AbstractInMemoryContainer.removeAllElements(). Otherwise,
+ * it's only removing one item at a time.
+ *
+ * We _could_ have a backup of all the itemIds, and compare to
+ * that one, but we really really don't want to go there.
+ */
+ activeRowHandler.removeItemId(removeEvent.getFirstItemId());
+ }
+
+ else {
+ // TODO no diff info available, redraw everything
+ throw new UnsupportedOperationException("bare "
+ + "ItemSetChangeEvents are currently "
+ + "not supported, use a container that "
+ + "uses AddItemEvents and RemoveItemEvents.");
+ }
+ }
+ };
+
+ private RpcDataProviderExtension datasourceExtension;
+
+ private final ActiveRowHandler activeRowHandler = new ActiveRowHandler();
+
+ /**
+ * Creates a new Grid using the given datasource.
+ *
+ * @param datasource
+ * the data source for the grid
+ */
+ public Grid(Container.Indexed datasource) {
+ setContainerDatasource(datasource);
+
+ registerRpc(new GridServerRpc() {
+ @Override
+ public void setVisibleRows(int firstVisibleRow, int visibleRowCount) {
+ activeRowHandler
+ .setActiveRows(firstVisibleRow, visibleRowCount);
+ }
+ });
+ }
+
+ /**
+ * Sets the grid data source.
+ *
+ * @param container
+ * The container data source. Cannot be null.
+ * @throws IllegalArgumentException
+ * if the data source is null
+ */
+ public void setContainerDatasource(Container.Indexed container) {
+ if (container == null) {
+ throw new IllegalArgumentException(
+ "Cannot set the datasource to null");
+ }
+ if (datasource == container) {
+ return;
+ }
+
+ // Remove old listeners
+ if (datasource instanceof PropertySetChangeNotifier) {
+ ((PropertySetChangeNotifier) datasource)
+ .removePropertySetChangeListener(propertyListener);
+ }
+ if (datasource instanceof ItemSetChangeNotifier) {
+ ((ItemSetChangeNotifier) datasource)
+ .removeItemSetChangeListener(itemListener);
+ }
+ activeRowHandler.clear();
+
+ if (datasourceExtension != null) {
+ removeExtension(datasourceExtension);
+ }
+
+ datasource = container;
+ datasourceExtension = new RpcDataProviderExtension(container);
+ datasourceExtension.extend(this);
+
+ // Listen to changes in properties and remove columns if needed
+ if (datasource instanceof PropertySetChangeNotifier) {
+ ((PropertySetChangeNotifier) datasource)
+ .addPropertySetChangeListener(propertyListener);
+ }
+ if (datasource instanceof ItemSetChangeNotifier) {
+ ((ItemSetChangeNotifier) datasource)
+ .addItemSetChangeListener(itemListener);
+ }
+ /*
+ * activeRowHandler will be updated by the client-side request that
+ * occurs on container change - no need to actively re-insert any
+ * ValueChangeListeners at this point.
+ */
+
+ getState().columns.clear();
+ setLastFrozenPropertyId(null);
+
+ // Add columns
+ for (Object propertyId : datasource.getContainerPropertyIds()) {
+ if (!columns.containsKey(propertyId)) {
+ GridColumn column = appendColumn(propertyId);
+
+ // Add by default property id as column header
+ column.setHeaderCaption(String.valueOf(propertyId));
+ }
+ }
+
+ }
+
+ /**
+ * Returns the grid data source.
+ *
+ * @return the container data source of the grid
+ */
+ public Container.Indexed getContainerDatasource() {
+ return datasource;
+ }
+
+ /**
+ * Returns a column based on the property id
+ *
+ * @param propertyId
+ * the property id of the column
+ * @return the column or <code>null</code> if not found
+ */
+ public GridColumn getColumn(Object propertyId) {
+ return columns.get(propertyId);
+ }
+
+ /**
+ * Sets the header rows visible.
+ *
+ * @param visible
+ * <code>true</code> if the header rows should be visible
+ */
+ public void setColumnHeadersVisible(boolean visible) {
+ getState().columnHeadersVisible = visible;
+ }
+
+ /**
+ * Are the header rows visible?
+ *
+ * @return <code>true</code> if the headers of the columns are visible
+ */
+ public boolean isColumnHeadersVisible() {
+ return getState(false).columnHeadersVisible;
+ }
+
+ /**
+ * Sets the footer rows visible.
+ *
+ * @param visible
+ * <code>true</code> if the footer rows should be visible
+ */
+ public void setColumnFootersVisible(boolean visible) {
+ getState().columnFootersVisible = visible;
+ }
+
+ /**
+ * Are the footer rows visible.
+ *
+ * @return <code>true</code> if the footer rows should be visible
+ */
+ public boolean isColumnFootersVisible() {
+ return getState(false).columnFootersVisible;
+ }
+
+ /**
+ * <p>
+ * Adds a new column group to the grid.
+ *
+ * <p>
+ * Column group rows are rendered in the header and footer of the grid.
+ * Column group rows are made up of column groups which groups together
+ * columns for adding a common auxiliary header or footer for the columns.
+ * </p>
+ * </p>
+ *
+ * <p>
+ * Example usage:
+ *
+ * <pre>
+ * // Add a new column group row to the grid
+ * ColumnGroupRow row = grid.addColumnGroupRow();
+ *
+ * // Group &quot;Column1&quot; and &quot;Column2&quot; together to form a header in the row
+ * ColumnGroup column12 = row.addGroup(&quot;Column1&quot;, &quot;Column2&quot;);
+ *
+ * // Set a common header for &quot;Column1&quot; and &quot;Column2&quot;
+ * column12.setHeader(&quot;Column 1&amp;2&quot;);
+ * </pre>
+ *
+ * </p>
+ *
+ * @return a column group instance you can use to add column groups
+ */
+ public ColumnGroupRow addColumnGroupRow() {
+ ColumnGroupRowState state = new ColumnGroupRowState();
+ ColumnGroupRow row = new ColumnGroupRow(this, state, columnKeys);
+ columnGroupRows.add(row);
+ getState().columnGroupRows.add(state);
+ return row;
+ }
+
+ /**
+ * Adds a new column group to the grid at a specific index
+ *
+ * @param rowIndex
+ * the index of the row
+ * @return a column group instance you can use to add column groups
+ */
+ public ColumnGroupRow addColumnGroupRow(int rowIndex) {
+ ColumnGroupRowState state = new ColumnGroupRowState();
+ ColumnGroupRow row = new ColumnGroupRow(this, state, columnKeys);
+ columnGroupRows.add(rowIndex, row);
+ getState().columnGroupRows.add(rowIndex, state);
+ return row;
+ }
+
+ /**
+ * Removes a column group.
+ *
+ * @param row
+ * the row to remove
+ */
+ public void removeColumnGroupRow(ColumnGroupRow row) {
+ columnGroupRows.remove(row);
+ getState().columnGroupRows.remove(row.getState());
+ }
+
+ /**
+ * Gets the column group rows.
+ *
+ * @return an unmodifiable list of column group rows
+ */
+ public List<ColumnGroupRow> getColumnGroupRows() {
+ return Collections.unmodifiableList(new ArrayList<ColumnGroupRow>(
+ columnGroupRows));
+ }
+
+ /**
+ * Used internally by the {@link Grid} to get a {@link GridColumn} by
+ * referencing its generated state id. Also used by {@link GridColumn} to
+ * verify if it has been detached from the {@link Grid}.
+ *
+ * @param columnId
+ * the client id generated for the column when the column is
+ * added to the grid
+ * @return the column with the id or <code>null</code> if not found
+ */
+ GridColumn getColumnByColumnId(String columnId) {
+ Object propertyId = getPropertyIdByColumnId(columnId);
+ return getColumn(propertyId);
+ }
+
+ /**
+ * Used internally by the {@link Grid} to get a property id by referencing
+ * the columns generated state id.
+ *
+ * @param columnId
+ * The state id of the column
+ * @return The column instance or null if not found
+ */
+ Object getPropertyIdByColumnId(String columnId) {
+ return columnKeys.get(columnId);
+ }
+
+ @Override
+ protected GridState getState() {
+ return (GridState) super.getState();
+ }
+
+ @Override
+ protected GridState getState(boolean markAsDirty) {
+ return (GridState) super.getState(markAsDirty);
+ }
+
+ /**
+ * Creates a new column based on a property id and appends it as the last
+ * column.
+ *
+ * @param datasourcePropertyId
+ * The property id of a property in the datasource
+ */
+ private GridColumn appendColumn(Object datasourcePropertyId) {
+ if (datasourcePropertyId == null) {
+ throw new IllegalArgumentException("Property id cannot be null");
+ }
+ assert datasource.getContainerPropertyIds().contains(
+ datasourcePropertyId) : "Datasource should contain the property id";
+
+ GridColumnState columnState = new GridColumnState();
+ columnState.id = columnKeys.key(datasourcePropertyId);
+ getState().columns.add(columnState);
+
+ GridColumn column = new GridColumn(this, columnState);
+ columns.put(datasourcePropertyId, column);
+
+ return column;
+ }
+
+ /**
+ * Sets (or unsets) the rightmost frozen column in the grid.
+ * <p>
+ * All columns up to and including the given column will be frozen in place
+ * when the grid is scrolled sideways.
+ *
+ * @param lastFrozenColumn
+ * the rightmost column to freeze, or <code>null</code> to not
+ * have any columns frozen
+ * @throws IllegalArgumentException
+ * if {@code lastFrozenColumn} is not a column from this grid
+ */
+ void setLastFrozenColumn(GridColumn lastFrozenColumn) {
+ /*
+ * TODO: If and when Grid supports column reordering or insertion of
+ * columns before other columns, make sure to mention that adding
+ * columns before lastFrozenColumn will change the frozen column count
+ */
+
+ if (lastFrozenColumn == null) {
+ getState().lastFrozenColumnId = null;
+ } else if (columns.containsValue(lastFrozenColumn)) {
+ getState().lastFrozenColumnId = lastFrozenColumn.getState().id;
+ } else {
+ throw new IllegalArgumentException(
+ "The given column isn't attached to this grid");
+ }
+ }
+
+ /**
+ * Sets (or unsets) the rightmost frozen column in the grid.
+ * <p>
+ * All columns up to and including the indicated property will be frozen in
+ * place when the grid is scrolled sideways.
+ * <p>
+ * <em>Note:</em> If the container used by this grid supports a propertyId
+ * <code>null</code>, it can never be defined as the last frozen column, as
+ * a <code>null</code> parameter will always reset the frozen columns in
+ * Grid.
+ *
+ * @param propertyId
+ * the property id corresponding to the column that should be the
+ * last frozen column, or <code>null</code> to not have any
+ * columns frozen.
+ * @throws IllegalArgumentException
+ * if {@code lastFrozenColumn} is not a column from this grid
+ */
+ public void setLastFrozenPropertyId(Object propertyId) {
+ final GridColumn column;
+ if (propertyId == null) {
+ column = null;
+ } else {
+ column = getColumn(propertyId);
+ if (column == null) {
+ throw new IllegalArgumentException(
+ "property id does not exist.");
+ }
+ }
+ setLastFrozenColumn(column);
+ }
+
+ /**
+ * Gets the rightmost frozen column in the grid.
+ * <p>
+ * <em>Note:</em> Most often, this method returns the very value set with
+ * {@link #setLastFrozenPropertyId(Object)}. This value, however, can be
+ * reset to <code>null</code> if the column is detached from this grid.
+ *
+ * @return the rightmost frozen column in the grid, or <code>null</code> if
+ * no columns are frozen.
+ */
+ public Object getLastFrozenPropertyId() {
+ return columnKeys.get(getState().lastFrozenColumnId);
+ }
+
+ /**
+ * Scrolls to a certain item, using {@link ScrollDestination#ANY}.
+ *
+ * @param itemId
+ * id of item to scroll to.
+ * @throws IllegalArgumentException
+ * if the provided id is not recognized by the data source.
+ */
+ public void scrollToItem(Object itemId) throws IllegalArgumentException {
+ scrollToItem(itemId, ScrollDestination.ANY);
+ }
+
+ /**
+ * Scrolls to a certain item, using user-specified scroll destination.
+ *
+ * @param itemId
+ * id of item to scroll to.
+ * @param destination
+ * value specifying desired position of scrolled-to row.
+ * @throws IllegalArgumentException
+ * if the provided id is not recognized by the data source.
+ */
+ public void scrollToItem(Object itemId, ScrollDestination destination)
+ throws IllegalArgumentException {
+
+ int row = datasource.indexOfId(itemId);
+
+ if (row == -1) {
+ throw new IllegalArgumentException(
+ "Item with specified ID does not exist in data source");
+ }
+
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToRow(row, destination);
+ }
+
+ /**
+ * Scrolls to the beginning of the first data row.
+ */
+ public void scrollToStart() {
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToStart();
+ }
+
+ /**
+ * Scrolls to the end of the last data row.
+ */
+ public void scrollToEnd() {
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToEnd();
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/GridColumn.java b/server/src/com/vaadin/ui/components/grid/GridColumn.java
new file mode 100644
index 0000000000..852db21275
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/GridColumn.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.ui.components.grid;
+
+import java.io.Serializable;
+
+import com.vaadin.shared.ui.grid.GridColumnState;
+
+/**
+ * A column in the grid. Can be obtained by calling
+ * {@link Grid#getColumn(Object propertyId)}.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class GridColumn implements Serializable {
+
+ /**
+ * The state of the column shared to the client
+ */
+ private final GridColumnState state;
+
+ /**
+ * The grid this column is associated with
+ */
+ private final Grid grid;
+
+ /**
+ * Internally used constructor.
+ *
+ * @param grid
+ * The grid this column belongs to. Should not be null.
+ * @param state
+ * the shared state of this column
+ */
+ GridColumn(Grid grid, GridColumnState state) {
+ this.grid = grid;
+ this.state = state;
+ }
+
+ /**
+ * Returns the serializable state of this column that is sent to the client
+ * side connector.
+ *
+ * @return the internal state of the column
+ */
+ GridColumnState getState() {
+ return state;
+ }
+
+ /**
+ * Returns the caption of the header. By default the header caption is the
+ * property id of the column.
+ *
+ * @return the text in the header
+ *
+ * @throws IllegalStateException
+ * if the column no longer is attached to the grid
+ */
+ public String getHeaderCaption() throws IllegalStateException {
+ checkColumnIsAttached();
+ return state.header;
+ }
+
+ /**
+ * Sets the caption of the header.
+ *
+ * @param caption
+ * the text to show in the caption
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public void setHeaderCaption(String caption) throws IllegalStateException {
+ checkColumnIsAttached();
+ state.header = caption;
+ grid.markAsDirty();
+ }
+
+ /**
+ * Returns the caption of the footer. By default the captions are
+ * <code>null</code>.
+ *
+ * @return the text in the footer
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public String getFooterCaption() throws IllegalStateException {
+ checkColumnIsAttached();
+ return state.footer;
+ }
+
+ /**
+ * Sets the caption of the footer.
+ *
+ * @param caption
+ * the text to show in the caption
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public void setFooterCaption(String caption) throws IllegalStateException {
+ checkColumnIsAttached();
+ state.footer = caption;
+ grid.markAsDirty();
+ }
+
+ /**
+ * Returns the width (in pixels). By default a column is 100px wide.
+ *
+ * @return the width in pixels of the column
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public int getWidth() throws IllegalStateException {
+ checkColumnIsAttached();
+ return state.width;
+ }
+
+ /**
+ * Sets the width (in pixels).
+ *
+ * @param pixelWidth
+ * the new pixel width of the column
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ * @throws IllegalArgumentException
+ * thrown if pixel width is less than zero
+ */
+ public void setWidth(int pixelWidth) throws IllegalStateException,
+ IllegalArgumentException {
+ checkColumnIsAttached();
+ if (pixelWidth < 0) {
+ throw new IllegalArgumentException(
+ "Pixel width should be greated than 0");
+ }
+ state.width = pixelWidth;
+ grid.markAsDirty();
+ }
+
+ /**
+ * Marks the column width as undefined meaning that the grid is free to
+ * resize the column based on the cell contents and available space in the
+ * grid.
+ */
+ public void setWidthUndefined() {
+ checkColumnIsAttached();
+ state.width = -1;
+ grid.markAsDirty();
+ }
+
+ /**
+ * Is this column visible in the grid. By default all columns are visible.
+ *
+ * @return <code>true</code> if the column is visible
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public boolean isVisible() throws IllegalStateException {
+ checkColumnIsAttached();
+ return state.visible;
+ }
+
+ /**
+ * Set the visibility of this column
+ *
+ * @param visible
+ * is the column visible
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public void setVisible(boolean visible) throws IllegalStateException {
+ checkColumnIsAttached();
+ state.visible = visible;
+ grid.markAsDirty();
+ }
+
+ /**
+ * Checks if column is attached and throws an {@link IllegalStateException}
+ * if it is not
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ protected void checkColumnIsAttached() throws IllegalStateException {
+ if (grid.getColumnByColumnId(state.id) == null) {
+ throw new IllegalStateException("Column no longer exists.");
+ }
+ }
+
+ /**
+ * Sets this column as the last frozen column in its grid.
+ *
+ * @throws IllegalArgumentException
+ * if the column is no longer attached to any grid
+ * @see Grid#setLastFrozenColumn(GridColumn)
+ */
+ public void setLastFrozenColumn() {
+ checkColumnIsAttached();
+ grid.setLastFrozenColumn(this);
+ }
+}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridColumnGroups.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumnGroups.java
new file mode 100644
index 0000000000..4350bf1a7b
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumnGroups.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.ui.components.grid.ColumnGroup;
+import com.vaadin.ui.components.grid.ColumnGroupRow;
+import com.vaadin.ui.components.grid.Grid;
+
+/**
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridColumnGroups {
+
+ private Grid grid;
+
+ private GridState state;
+
+ private Method getStateMethod;
+
+ private Field columnIdGeneratorField;
+
+ private KeyMapper<Object> columnIdMapper;
+
+ @Before
+ public void setup() throws Exception {
+ IndexedContainer ds = new IndexedContainer();
+ for (int c = 0; c < 10; c++) {
+ ds.addContainerProperty("column" + c, String.class, "");
+ }
+ grid = new Grid(ds);
+
+ getStateMethod = Grid.class.getDeclaredMethod("getState");
+ getStateMethod.setAccessible(true);
+
+ state = (GridState) getStateMethod.invoke(grid);
+
+ columnIdGeneratorField = Grid.class.getDeclaredField("columnKeys");
+ columnIdGeneratorField.setAccessible(true);
+
+ columnIdMapper = (KeyMapper<Object>) columnIdGeneratorField.get(grid);
+ }
+
+ @Test
+ public void testColumnGroupRows() throws Exception {
+
+ // No column group rows by default
+ List<ColumnGroupRow> rows = grid.getColumnGroupRows();
+ assertEquals(0, rows.size());
+
+ // Add some rows
+ ColumnGroupRow row1 = grid.addColumnGroupRow();
+ ColumnGroupRow row3 = grid.addColumnGroupRow();
+ ColumnGroupRow row2 = grid.addColumnGroupRow(1);
+
+ rows = grid.getColumnGroupRows();
+ assertEquals(3, rows.size());
+ assertEquals(row1, rows.get(0));
+ assertEquals(row2, rows.get(1));
+ assertEquals(row3, rows.get(2));
+
+ // Header should be visible by default, footer should not
+ assertTrue(row1.isHeaderVisible());
+ assertFalse(row1.isFooterVisible());
+
+ row1.setHeaderVisible(false);
+ assertFalse(row1.isHeaderVisible());
+ row1.setHeaderVisible(true);
+ assertTrue(row1.isHeaderVisible());
+
+ row1.setFooterVisible(true);
+ assertTrue(row1.isFooterVisible());
+ row1.setFooterVisible(false);
+ assertFalse(row1.isFooterVisible());
+
+ row1.setHeaderVisible(true);
+ row1.setFooterVisible(true);
+ assertTrue(row1.isHeaderVisible());
+ assertTrue(row1.isFooterVisible());
+
+ row1.setHeaderVisible(false);
+ row1.setFooterVisible(false);
+ assertFalse(row1.isHeaderVisible());
+ assertFalse(row1.isFooterVisible());
+ }
+
+ @Test
+ public void testColumnGroupsInState() throws Exception {
+
+ // Add a new row
+ ColumnGroupRow row = grid.addColumnGroupRow();
+ assertTrue(state.columnGroupRows.size() == 1);
+
+ // Add a group by property id
+ ColumnGroup columns12 = row.addGroup("column1", "column2");
+ assertTrue(state.columnGroupRows.get(0).groups.size() == 1);
+
+ // Set header of column
+ columns12.setHeaderCaption("Column12");
+ assertEquals("Column12",
+ state.columnGroupRows.get(0).groups.get(0).header);
+
+ // Set footer of column
+ columns12.setFooterCaption("Footer12");
+ assertEquals("Footer12",
+ state.columnGroupRows.get(0).groups.get(0).footer);
+
+ // Add another group by column instance
+ ColumnGroup columns34 = row.addGroup(grid.getColumn("column3"),
+ grid.getColumn("column4"));
+ assertTrue(state.columnGroupRows.get(0).groups.size() == 2);
+
+ // add another group row
+ ColumnGroupRow row2 = grid.addColumnGroupRow();
+ assertTrue(state.columnGroupRows.size() == 2);
+
+ // add a group by combining the two previous groups
+ ColumnGroup columns1234 = row2.addGroup(columns12, columns34);
+ assertTrue(columns1234.getColumns().size() == 4);
+
+ // Insert a group as the second group
+ ColumnGroupRow newRow2 = grid.addColumnGroupRow(1);
+ assertTrue(state.columnGroupRows.size() == 3);
+ }
+
+ @Test
+ public void testAddingColumnGroups() throws Exception {
+
+ ColumnGroupRow row = grid.addColumnGroupRow();
+
+ // By property id
+ ColumnGroup columns01 = row.addGroup("column0", "column1");
+ assertEquals(2, columns01.getColumns().size());
+ assertEquals("column0", columns01.getColumns().get(0));
+ assertTrue(columns01.isColumnInGroup("column0"));
+ assertEquals("column1", columns01.getColumns().get(1));
+ assertTrue(columns01.isColumnInGroup("column1"));
+
+ // By grid column
+ ColumnGroup columns23 = row.addGroup(grid.getColumn("column2"),
+ grid.getColumn("column3"));
+ assertEquals(2, columns23.getColumns().size());
+ assertEquals("column2", columns23.getColumns().get(0));
+ assertTrue(columns23.isColumnInGroup("column2"));
+ assertEquals("column3", columns23.getColumns().get(1));
+ assertTrue(columns23.isColumnInGroup("column3"));
+
+ // Combine groups
+ ColumnGroupRow row2 = grid.addColumnGroupRow();
+ ColumnGroup columns0123 = row2.addGroup(columns01, columns23);
+ assertEquals(4, columns0123.getColumns().size());
+ assertEquals("column0", columns0123.getColumns().get(0));
+ assertTrue(columns0123.isColumnInGroup("column0"));
+ assertEquals("column1", columns0123.getColumns().get(1));
+ assertTrue(columns0123.isColumnInGroup("column1"));
+ assertEquals("column2", columns0123.getColumns().get(2));
+ assertTrue(columns0123.isColumnInGroup("column2"));
+ assertEquals("column3", columns0123.getColumns().get(3));
+ assertTrue(columns0123.isColumnInGroup("column3"));
+ }
+
+ @Test
+ public void testColumnGroupHeadersAndFooters() throws Exception {
+
+ ColumnGroupRow row = grid.addColumnGroupRow();
+ ColumnGroup group = row.addGroup("column1", "column2");
+
+ // Header
+ assertNull(group.getHeaderCaption());
+ group.setHeaderCaption("My header");
+ assertEquals("My header", group.getHeaderCaption());
+ group.setHeaderCaption(null);
+ assertNull(group.getHeaderCaption());
+
+ // Footer
+ assertNull(group.getFooterCaption());
+ group.setFooterCaption("My footer");
+ assertEquals("My footer", group.getFooterCaption());
+ group.setFooterCaption(null);
+ assertNull(group.getFooterCaption());
+ }
+
+ @Test
+ public void testColumnGroupDetachment() throws Exception {
+
+ ColumnGroupRow row = grid.addColumnGroupRow();
+ ColumnGroup group = row.addGroup("column1", "column2");
+
+ // Remove group
+ row.removeGroup(group);
+
+ try {
+ group.setHeaderCaption("Header");
+ fail("Should throw exception for setting header caption on detached group");
+ } catch (IllegalStateException ise) {
+
+ }
+
+ try {
+ group.setFooterCaption("Footer");
+ fail("Should throw exception for setting footer caption on detached group");
+ } catch (IllegalStateException ise) {
+
+ }
+ }
+
+ @Test
+ public void testColumnGroupLimits() throws Exception {
+
+ ColumnGroupRow row = grid.addColumnGroupRow();
+ row.addGroup("column1", "column2");
+ row.addGroup("column3", "column4");
+
+ try {
+ row.addGroup("column2", "column3");
+ fail("Adding a group with already grouped properties should throw exception");
+ } catch (IllegalArgumentException iae) {
+
+ }
+
+ ColumnGroupRow row2 = grid.addColumnGroupRow();
+
+ try {
+ row2.addGroup("column2", "column3");
+ fail("Adding a group that breaks previous grouping boundaries should throw exception");
+ } catch (IllegalArgumentException iae) {
+
+ }
+
+ // This however should not throw an exception as it spans completely
+ // over the parent rows groups
+ row2.addGroup("column1", "column2", "column3", "column4");
+
+ }
+}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java
new file mode 100644
index 0000000000..da07611b48
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.shared.ui.grid.GridColumnState;
+import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.ui.components.grid.Grid;
+import com.vaadin.ui.components.grid.GridColumn;
+
+public class GridColumns {
+
+ private Grid grid;
+
+ private GridState state;
+
+ private Method getStateMethod;
+
+ private Field columnIdGeneratorField;
+
+ private KeyMapper<Object> columnIdMapper;
+
+ @Before
+ public void setup() throws Exception {
+ IndexedContainer ds = new IndexedContainer();
+ for (int c = 0; c < 10; c++) {
+ ds.addContainerProperty("column" + c, String.class, "");
+ }
+ grid = new Grid(ds);
+
+ getStateMethod = Grid.class.getDeclaredMethod("getState");
+ getStateMethod.setAccessible(true);
+
+ state = (GridState) getStateMethod.invoke(grid);
+
+ columnIdGeneratorField = Grid.class.getDeclaredField("columnKeys");
+ columnIdGeneratorField.setAccessible(true);
+
+ columnIdMapper = (KeyMapper<Object>) columnIdGeneratorField.get(grid);
+ }
+
+ @Test
+ public void testColumnGeneration() throws Exception {
+
+ for (Object propertyId : grid.getContainerDatasource()
+ .getContainerPropertyIds()) {
+
+ // All property ids should get a column
+ GridColumn column = grid.getColumn(propertyId);
+ assertNotNull(column);
+
+ // Property id should be the column header by default
+ assertEquals(propertyId.toString(), column.getHeaderCaption());
+ }
+ }
+
+ @Test
+ public void testModifyingColumnProperties() throws Exception {
+
+ // Modify first column
+ GridColumn column = grid.getColumn("column1");
+ assertNotNull(column);
+
+ column.setFooterCaption("CustomFooter");
+ assertEquals("CustomFooter", column.getFooterCaption());
+ assertEquals(column.getFooterCaption(),
+ getColumnState("column1").footer);
+
+ column.setHeaderCaption("CustomHeader");
+ assertEquals("CustomHeader", column.getHeaderCaption());
+ assertEquals(column.getHeaderCaption(),
+ getColumnState("column1").header);
+
+ column.setVisible(false);
+ assertFalse(column.isVisible());
+ assertFalse(getColumnState("column1").visible);
+
+ column.setVisible(true);
+ assertTrue(column.isVisible());
+ assertTrue(getColumnState("column1").visible);
+
+ column.setWidth(100);
+ assertEquals(100, column.getWidth());
+ assertEquals(column.getWidth(), getColumnState("column1").width);
+
+ try {
+ column.setWidth(-1);
+ fail("Setting width to -1 should throw exception");
+ } catch (IllegalArgumentException iae) {
+
+ }
+
+ assertEquals(100, column.getWidth());
+ assertEquals(100, getColumnState("column1").width);
+ }
+
+ @Test
+ public void testRemovingColumn() throws Exception {
+
+ GridColumn column = grid.getColumn("column1");
+ assertNotNull(column);
+
+ // Remove column
+ grid.getContainerDatasource().removeContainerProperty("column1");
+
+ try {
+ column.setHeaderCaption("asd");
+
+ fail("Succeeded in modifying a detached column");
+ } catch (IllegalStateException ise) {
+ // Detached state should throw exception
+ }
+
+ try {
+ column.setFooterCaption("asd");
+ fail("Succeeded in modifying a detached column");
+ } catch (IllegalStateException ise) {
+ // Detached state should throw exception
+ }
+
+ try {
+ column.setVisible(false);
+ fail("Succeeded in modifying a detached column");
+ } catch (IllegalStateException ise) {
+ // Detached state should throw exception
+ }
+
+ try {
+ column.setWidth(123);
+ fail("Succeeded in modifying a detached column");
+ } catch (IllegalStateException ise) {
+ // Detached state should throw exception
+ }
+
+ assertNull(grid.getColumn("column1"));
+ assertNull(getColumnState("column1"));
+ }
+
+ @Test
+ public void testAddingColumn() throws Exception {
+ grid.getContainerDatasource().addContainerProperty("columnX",
+ String.class, "");
+ GridColumn column = grid.getColumn("columnX");
+ assertNotNull(column);
+ }
+
+ @Test
+ public void testHeaderVisiblility() throws Exception {
+
+ assertTrue(grid.isColumnHeadersVisible());
+ assertTrue(state.columnHeadersVisible);
+
+ grid.setColumnHeadersVisible(false);
+ assertFalse(grid.isColumnHeadersVisible());
+ assertFalse(state.columnHeadersVisible);
+
+ grid.setColumnHeadersVisible(true);
+ assertTrue(grid.isColumnHeadersVisible());
+ assertTrue(state.columnHeadersVisible);
+ }
+
+ @Test
+ public void testFooterVisibility() throws Exception {
+
+ assertFalse(grid.isColumnFootersVisible());
+ assertFalse(state.columnFootersVisible);
+
+ grid.setColumnFootersVisible(false);
+ assertFalse(grid.isColumnFootersVisible());
+ assertFalse(state.columnFootersVisible);
+
+ grid.setColumnFootersVisible(true);
+ assertTrue(grid.isColumnFootersVisible());
+ assertTrue(state.columnFootersVisible);
+ }
+
+ @Test
+ public void testFrozenColumnByPropertyId() {
+ assertNull("Grid should not start with a frozen column",
+ grid.getLastFrozenPropertyId());
+
+ Object propertyId = grid.getContainerDatasource()
+ .getContainerPropertyIds().iterator().next();
+ grid.setLastFrozenPropertyId(propertyId);
+ assertEquals(propertyId, grid.getLastFrozenPropertyId());
+
+ grid.getContainerDatasource().removeContainerProperty(propertyId);
+ assertNull(grid.getLastFrozenPropertyId());
+ }
+
+ private GridColumnState getColumnState(Object propertyId) {
+ String columnId = columnIdMapper.key(propertyId);
+ for (GridColumnState columnState : state.columns) {
+ if (columnState.id.equals(columnId)) {
+ return columnState;
+ }
+ }
+ return null;
+ }
+
+}