diff options
Diffstat (limited to 'server')
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 "Column1" and "Column2" together to form a header in the row + * ColumnGroup column12 = row.addGroup("Column1", "Column2"); + * + * // Set a common header for "Column1" and "Column2" + * column12.setHeader("Column 1&2"); + * </pre> + * + * </p> + * + * @return a column group instance you can use to add column groups + */ + public ColumnGroupRow addColumnGroupRow() { + ColumnGroupRowState state = new ColumnGroupRowState(); + ColumnGroupRow row = new ColumnGroupRow(this, state, columnKeys); + columnGroupRows.add(row); + getState().columnGroupRows.add(state); + return row; + } + + /** + * Adds a new column group to the grid at a specific index + * + * @param rowIndex + * the index of the row + * @return a column group instance you can use to add column groups + */ + public ColumnGroupRow addColumnGroupRow(int rowIndex) { + ColumnGroupRowState state = new ColumnGroupRowState(); + ColumnGroupRow row = new ColumnGroupRow(this, state, columnKeys); + columnGroupRows.add(rowIndex, row); + getState().columnGroupRows.add(rowIndex, state); + return row; + } + + /** + * Removes a column group. + * + * @param row + * the row to remove + */ + public void removeColumnGroupRow(ColumnGroupRow row) { + columnGroupRows.remove(row); + getState().columnGroupRows.remove(row.getState()); + } + + /** + * Gets the column group rows. + * + * @return an unmodifiable list of column group rows + */ + public List<ColumnGroupRow> getColumnGroupRows() { + return Collections.unmodifiableList(new ArrayList<ColumnGroupRow>( + columnGroupRows)); + } + + /** + * Used internally by the {@link Grid} to get a {@link GridColumn} by + * referencing its generated state id. Also used by {@link GridColumn} to + * verify if it has been detached from the {@link Grid}. + * + * @param columnId + * the client id generated for the column when the column is + * added to the grid + * @return the column with the id or <code>null</code> if not found + */ + GridColumn getColumnByColumnId(String columnId) { + Object propertyId = getPropertyIdByColumnId(columnId); + return getColumn(propertyId); + } + + /** + * Used internally by the {@link Grid} to get a property id by referencing + * the columns generated state id. + * + * @param columnId + * The state id of the column + * @return The column instance or null if not found + */ + Object getPropertyIdByColumnId(String columnId) { + return columnKeys.get(columnId); + } + + @Override + protected GridState getState() { + return (GridState) super.getState(); + } + + @Override + protected GridState getState(boolean markAsDirty) { + return (GridState) super.getState(markAsDirty); + } + + /** + * Creates a new column based on a property id and appends it as the last + * column. + * + * @param datasourcePropertyId + * The property id of a property in the datasource + */ + private GridColumn appendColumn(Object datasourcePropertyId) { + if (datasourcePropertyId == null) { + throw new IllegalArgumentException("Property id cannot be null"); + } + assert datasource.getContainerPropertyIds().contains( + datasourcePropertyId) : "Datasource should contain the property id"; + + GridColumnState columnState = new GridColumnState(); + columnState.id = columnKeys.key(datasourcePropertyId); + getState().columns.add(columnState); + + GridColumn column = new GridColumn(this, columnState); + columns.put(datasourcePropertyId, column); + + return column; + } + + /** + * Sets (or unsets) the rightmost frozen column in the grid. + * <p> + * All columns up to and including the given column will be frozen in place + * when the grid is scrolled sideways. + * + * @param lastFrozenColumn + * the rightmost column to freeze, or <code>null</code> to not + * have any columns frozen + * @throws IllegalArgumentException + * if {@code lastFrozenColumn} is not a column from this grid + */ + void setLastFrozenColumn(GridColumn lastFrozenColumn) { + /* + * TODO: If and when Grid supports column reordering or insertion of + * columns before other columns, make sure to mention that adding + * columns before lastFrozenColumn will change the frozen column count + */ + + if (lastFrozenColumn == null) { + getState().lastFrozenColumnId = null; + } else if (columns.containsValue(lastFrozenColumn)) { + getState().lastFrozenColumnId = lastFrozenColumn.getState().id; + } else { + throw new IllegalArgumentException( + "The given column isn't attached to this grid"); + } + } + + /** + * Sets (or unsets) the rightmost frozen column in the grid. + * <p> + * All columns up to and including the indicated property will be frozen in + * place when the grid is scrolled sideways. + * <p> + * <em>Note:</em> If the container used by this grid supports a propertyId + * <code>null</code>, it can never be defined as the last frozen column, as + * a <code>null</code> parameter will always reset the frozen columns in + * Grid. + * + * @param propertyId + * the property id corresponding to the column that should be the + * last frozen column, or <code>null</code> to not have any + * columns frozen. + * @throws IllegalArgumentException + * if {@code lastFrozenColumn} is not a column from this grid + */ + public void setLastFrozenPropertyId(Object propertyId) { + final GridColumn column; + if (propertyId == null) { + column = null; + } else { + column = getColumn(propertyId); + if (column == null) { + throw new IllegalArgumentException( + "property id does not exist."); + } + } + setLastFrozenColumn(column); + } + + /** + * Gets the rightmost frozen column in the grid. + * <p> + * <em>Note:</em> Most often, this method returns the very value set with + * {@link #setLastFrozenPropertyId(Object)}. This value, however, can be + * reset to <code>null</code> if the column is detached from this grid. + * + * @return the rightmost frozen column in the grid, or <code>null</code> if + * no columns are frozen. + */ + public Object getLastFrozenPropertyId() { + return columnKeys.get(getState().lastFrozenColumnId); + } + + /** + * Scrolls to a certain item, using {@link ScrollDestination#ANY}. + * + * @param itemId + * id of item to scroll to. + * @throws IllegalArgumentException + * if the provided id is not recognized by the data source. + */ + public void scrollToItem(Object itemId) throws IllegalArgumentException { + scrollToItem(itemId, ScrollDestination.ANY); + } + + /** + * Scrolls to a certain item, using user-specified scroll destination. + * + * @param itemId + * id of item to scroll to. + * @param destination + * value specifying desired position of scrolled-to row. + * @throws IllegalArgumentException + * if the provided id is not recognized by the data source. + */ + public void scrollToItem(Object itemId, ScrollDestination destination) + throws IllegalArgumentException { + + int row = datasource.indexOfId(itemId); + + if (row == -1) { + throw new IllegalArgumentException( + "Item with specified ID does not exist in data source"); + } + + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToRow(row, destination); + } + + /** + * Scrolls to the beginning of the first data row. + */ + public void scrollToStart() { + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToStart(); + } + + /** + * Scrolls to the end of the last data row. + */ + public void scrollToEnd() { + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToEnd(); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/GridColumn.java b/server/src/com/vaadin/ui/components/grid/GridColumn.java new file mode 100644 index 0000000000..852db21275 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/GridColumn.java @@ -0,0 +1,216 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.ui.components.grid; + +import java.io.Serializable; + +import com.vaadin.shared.ui.grid.GridColumnState; + +/** + * A column in the grid. Can be obtained by calling + * {@link Grid#getColumn(Object propertyId)}. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public class GridColumn implements Serializable { + + /** + * The state of the column shared to the client + */ + private final GridColumnState state; + + /** + * The grid this column is associated with + */ + private final Grid grid; + + /** + * Internally used constructor. + * + * @param grid + * The grid this column belongs to. Should not be null. + * @param state + * the shared state of this column + */ + GridColumn(Grid grid, GridColumnState state) { + this.grid = grid; + this.state = state; + } + + /** + * Returns the serializable state of this column that is sent to the client + * side connector. + * + * @return the internal state of the column + */ + GridColumnState getState() { + return state; + } + + /** + * Returns the caption of the header. By default the header caption is the + * property id of the column. + * + * @return the text in the header + * + * @throws IllegalStateException + * if the column no longer is attached to the grid + */ + public String getHeaderCaption() throws IllegalStateException { + checkColumnIsAttached(); + return state.header; + } + + /** + * Sets the caption of the header. + * + * @param caption + * the text to show in the caption + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public void setHeaderCaption(String caption) throws IllegalStateException { + checkColumnIsAttached(); + state.header = caption; + grid.markAsDirty(); + } + + /** + * Returns the caption of the footer. By default the captions are + * <code>null</code>. + * + * @return the text in the footer + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public String getFooterCaption() throws IllegalStateException { + checkColumnIsAttached(); + return state.footer; + } + + /** + * Sets the caption of the footer. + * + * @param caption + * the text to show in the caption + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public void setFooterCaption(String caption) throws IllegalStateException { + checkColumnIsAttached(); + state.footer = caption; + grid.markAsDirty(); + } + + /** + * Returns the width (in pixels). By default a column is 100px wide. + * + * @return the width in pixels of the column + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public int getWidth() throws IllegalStateException { + checkColumnIsAttached(); + return state.width; + } + + /** + * Sets the width (in pixels). + * + * @param pixelWidth + * the new pixel width of the column + * @throws IllegalStateException + * if the column is no longer attached to any grid + * @throws IllegalArgumentException + * thrown if pixel width is less than zero + */ + public void setWidth(int pixelWidth) throws IllegalStateException, + IllegalArgumentException { + checkColumnIsAttached(); + if (pixelWidth < 0) { + throw new IllegalArgumentException( + "Pixel width should be greated than 0"); + } + state.width = pixelWidth; + grid.markAsDirty(); + } + + /** + * Marks the column width as undefined meaning that the grid is free to + * resize the column based on the cell contents and available space in the + * grid. + */ + public void setWidthUndefined() { + checkColumnIsAttached(); + state.width = -1; + grid.markAsDirty(); + } + + /** + * Is this column visible in the grid. By default all columns are visible. + * + * @return <code>true</code> if the column is visible + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public boolean isVisible() throws IllegalStateException { + checkColumnIsAttached(); + return state.visible; + } + + /** + * Set the visibility of this column + * + * @param visible + * is the column visible + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public void setVisible(boolean visible) throws IllegalStateException { + checkColumnIsAttached(); + state.visible = visible; + grid.markAsDirty(); + } + + /** + * Checks if column is attached and throws an {@link IllegalStateException} + * if it is not + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + protected void checkColumnIsAttached() throws IllegalStateException { + if (grid.getColumnByColumnId(state.id) == null) { + throw new IllegalStateException("Column no longer exists."); + } + } + + /** + * Sets this column as the last frozen column in its grid. + * + * @throws IllegalArgumentException + * if the column is no longer attached to any grid + * @see Grid#setLastFrozenColumn(GridColumn) + */ + public void setLastFrozenColumn() { + checkColumnIsAttached(); + grid.setLastFrozenColumn(this); + } +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridColumnGroups.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumnGroups.java new file mode 100644 index 0000000000..4350bf1a7b --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumnGroups.java @@ -0,0 +1,265 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.KeyMapper; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.ui.components.grid.ColumnGroup; +import com.vaadin.ui.components.grid.ColumnGroupRow; +import com.vaadin.ui.components.grid.Grid; + +/** + * + * @since + * @author Vaadin Ltd + */ +public class GridColumnGroups { + + private Grid grid; + + private GridState state; + + private Method getStateMethod; + + private Field columnIdGeneratorField; + + private KeyMapper<Object> columnIdMapper; + + @Before + public void setup() throws Exception { + IndexedContainer ds = new IndexedContainer(); + for (int c = 0; c < 10; c++) { + ds.addContainerProperty("column" + c, String.class, ""); + } + grid = new Grid(ds); + + getStateMethod = Grid.class.getDeclaredMethod("getState"); + getStateMethod.setAccessible(true); + + state = (GridState) getStateMethod.invoke(grid); + + columnIdGeneratorField = Grid.class.getDeclaredField("columnKeys"); + columnIdGeneratorField.setAccessible(true); + + columnIdMapper = (KeyMapper<Object>) columnIdGeneratorField.get(grid); + } + + @Test + public void testColumnGroupRows() throws Exception { + + // No column group rows by default + List<ColumnGroupRow> rows = grid.getColumnGroupRows(); + assertEquals(0, rows.size()); + + // Add some rows + ColumnGroupRow row1 = grid.addColumnGroupRow(); + ColumnGroupRow row3 = grid.addColumnGroupRow(); + ColumnGroupRow row2 = grid.addColumnGroupRow(1); + + rows = grid.getColumnGroupRows(); + assertEquals(3, rows.size()); + assertEquals(row1, rows.get(0)); + assertEquals(row2, rows.get(1)); + assertEquals(row3, rows.get(2)); + + // Header should be visible by default, footer should not + assertTrue(row1.isHeaderVisible()); + assertFalse(row1.isFooterVisible()); + + row1.setHeaderVisible(false); + assertFalse(row1.isHeaderVisible()); + row1.setHeaderVisible(true); + assertTrue(row1.isHeaderVisible()); + + row1.setFooterVisible(true); + assertTrue(row1.isFooterVisible()); + row1.setFooterVisible(false); + assertFalse(row1.isFooterVisible()); + + row1.setHeaderVisible(true); + row1.setFooterVisible(true); + assertTrue(row1.isHeaderVisible()); + assertTrue(row1.isFooterVisible()); + + row1.setHeaderVisible(false); + row1.setFooterVisible(false); + assertFalse(row1.isHeaderVisible()); + assertFalse(row1.isFooterVisible()); + } + + @Test + public void testColumnGroupsInState() throws Exception { + + // Add a new row + ColumnGroupRow row = grid.addColumnGroupRow(); + assertTrue(state.columnGroupRows.size() == 1); + + // Add a group by property id + ColumnGroup columns12 = row.addGroup("column1", "column2"); + assertTrue(state.columnGroupRows.get(0).groups.size() == 1); + + // Set header of column + columns12.setHeaderCaption("Column12"); + assertEquals("Column12", + state.columnGroupRows.get(0).groups.get(0).header); + + // Set footer of column + columns12.setFooterCaption("Footer12"); + assertEquals("Footer12", + state.columnGroupRows.get(0).groups.get(0).footer); + + // Add another group by column instance + ColumnGroup columns34 = row.addGroup(grid.getColumn("column3"), + grid.getColumn("column4")); + assertTrue(state.columnGroupRows.get(0).groups.size() == 2); + + // add another group row + ColumnGroupRow row2 = grid.addColumnGroupRow(); + assertTrue(state.columnGroupRows.size() == 2); + + // add a group by combining the two previous groups + ColumnGroup columns1234 = row2.addGroup(columns12, columns34); + assertTrue(columns1234.getColumns().size() == 4); + + // Insert a group as the second group + ColumnGroupRow newRow2 = grid.addColumnGroupRow(1); + assertTrue(state.columnGroupRows.size() == 3); + } + + @Test + public void testAddingColumnGroups() throws Exception { + + ColumnGroupRow row = grid.addColumnGroupRow(); + + // By property id + ColumnGroup columns01 = row.addGroup("column0", "column1"); + assertEquals(2, columns01.getColumns().size()); + assertEquals("column0", columns01.getColumns().get(0)); + assertTrue(columns01.isColumnInGroup("column0")); + assertEquals("column1", columns01.getColumns().get(1)); + assertTrue(columns01.isColumnInGroup("column1")); + + // By grid column + ColumnGroup columns23 = row.addGroup(grid.getColumn("column2"), + grid.getColumn("column3")); + assertEquals(2, columns23.getColumns().size()); + assertEquals("column2", columns23.getColumns().get(0)); + assertTrue(columns23.isColumnInGroup("column2")); + assertEquals("column3", columns23.getColumns().get(1)); + assertTrue(columns23.isColumnInGroup("column3")); + + // Combine groups + ColumnGroupRow row2 = grid.addColumnGroupRow(); + ColumnGroup columns0123 = row2.addGroup(columns01, columns23); + assertEquals(4, columns0123.getColumns().size()); + assertEquals("column0", columns0123.getColumns().get(0)); + assertTrue(columns0123.isColumnInGroup("column0")); + assertEquals("column1", columns0123.getColumns().get(1)); + assertTrue(columns0123.isColumnInGroup("column1")); + assertEquals("column2", columns0123.getColumns().get(2)); + assertTrue(columns0123.isColumnInGroup("column2")); + assertEquals("column3", columns0123.getColumns().get(3)); + assertTrue(columns0123.isColumnInGroup("column3")); + } + + @Test + public void testColumnGroupHeadersAndFooters() throws Exception { + + ColumnGroupRow row = grid.addColumnGroupRow(); + ColumnGroup group = row.addGroup("column1", "column2"); + + // Header + assertNull(group.getHeaderCaption()); + group.setHeaderCaption("My header"); + assertEquals("My header", group.getHeaderCaption()); + group.setHeaderCaption(null); + assertNull(group.getHeaderCaption()); + + // Footer + assertNull(group.getFooterCaption()); + group.setFooterCaption("My footer"); + assertEquals("My footer", group.getFooterCaption()); + group.setFooterCaption(null); + assertNull(group.getFooterCaption()); + } + + @Test + public void testColumnGroupDetachment() throws Exception { + + ColumnGroupRow row = grid.addColumnGroupRow(); + ColumnGroup group = row.addGroup("column1", "column2"); + + // Remove group + row.removeGroup(group); + + try { + group.setHeaderCaption("Header"); + fail("Should throw exception for setting header caption on detached group"); + } catch (IllegalStateException ise) { + + } + + try { + group.setFooterCaption("Footer"); + fail("Should throw exception for setting footer caption on detached group"); + } catch (IllegalStateException ise) { + + } + } + + @Test + public void testColumnGroupLimits() throws Exception { + + ColumnGroupRow row = grid.addColumnGroupRow(); + row.addGroup("column1", "column2"); + row.addGroup("column3", "column4"); + + try { + row.addGroup("column2", "column3"); + fail("Adding a group with already grouped properties should throw exception"); + } catch (IllegalArgumentException iae) { + + } + + ColumnGroupRow row2 = grid.addColumnGroupRow(); + + try { + row2.addGroup("column2", "column3"); + fail("Adding a group that breaks previous grouping boundaries should throw exception"); + } catch (IllegalArgumentException iae) { + + } + + // This however should not throw an exception as it spans completely + // over the parent rows groups + row2.addGroup("column1", "column2", "column3", "column4"); + + } +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java new file mode 100644 index 0000000000..da07611b48 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java @@ -0,0 +1,228 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.KeyMapper; +import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.GridColumn; + +public class GridColumns { + + private Grid grid; + + private GridState state; + + private Method getStateMethod; + + private Field columnIdGeneratorField; + + private KeyMapper<Object> columnIdMapper; + + @Before + public void setup() throws Exception { + IndexedContainer ds = new IndexedContainer(); + for (int c = 0; c < 10; c++) { + ds.addContainerProperty("column" + c, String.class, ""); + } + grid = new Grid(ds); + + getStateMethod = Grid.class.getDeclaredMethod("getState"); + getStateMethod.setAccessible(true); + + state = (GridState) getStateMethod.invoke(grid); + + columnIdGeneratorField = Grid.class.getDeclaredField("columnKeys"); + columnIdGeneratorField.setAccessible(true); + + columnIdMapper = (KeyMapper<Object>) columnIdGeneratorField.get(grid); + } + + @Test + public void testColumnGeneration() throws Exception { + + for (Object propertyId : grid.getContainerDatasource() + .getContainerPropertyIds()) { + + // All property ids should get a column + GridColumn column = grid.getColumn(propertyId); + assertNotNull(column); + + // Property id should be the column header by default + assertEquals(propertyId.toString(), column.getHeaderCaption()); + } + } + + @Test + public void testModifyingColumnProperties() throws Exception { + + // Modify first column + GridColumn column = grid.getColumn("column1"); + assertNotNull(column); + + column.setFooterCaption("CustomFooter"); + assertEquals("CustomFooter", column.getFooterCaption()); + assertEquals(column.getFooterCaption(), + getColumnState("column1").footer); + + column.setHeaderCaption("CustomHeader"); + assertEquals("CustomHeader", column.getHeaderCaption()); + assertEquals(column.getHeaderCaption(), + getColumnState("column1").header); + + column.setVisible(false); + assertFalse(column.isVisible()); + assertFalse(getColumnState("column1").visible); + + column.setVisible(true); + assertTrue(column.isVisible()); + assertTrue(getColumnState("column1").visible); + + column.setWidth(100); + assertEquals(100, column.getWidth()); + assertEquals(column.getWidth(), getColumnState("column1").width); + + try { + column.setWidth(-1); + fail("Setting width to -1 should throw exception"); + } catch (IllegalArgumentException iae) { + + } + + assertEquals(100, column.getWidth()); + assertEquals(100, getColumnState("column1").width); + } + + @Test + public void testRemovingColumn() throws Exception { + + GridColumn column = grid.getColumn("column1"); + assertNotNull(column); + + // Remove column + grid.getContainerDatasource().removeContainerProperty("column1"); + + try { + column.setHeaderCaption("asd"); + + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + try { + column.setFooterCaption("asd"); + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + try { + column.setVisible(false); + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + try { + column.setWidth(123); + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + assertNull(grid.getColumn("column1")); + assertNull(getColumnState("column1")); + } + + @Test + public void testAddingColumn() throws Exception { + grid.getContainerDatasource().addContainerProperty("columnX", + String.class, ""); + GridColumn column = grid.getColumn("columnX"); + assertNotNull(column); + } + + @Test + public void testHeaderVisiblility() throws Exception { + + assertTrue(grid.isColumnHeadersVisible()); + assertTrue(state.columnHeadersVisible); + + grid.setColumnHeadersVisible(false); + assertFalse(grid.isColumnHeadersVisible()); + assertFalse(state.columnHeadersVisible); + + grid.setColumnHeadersVisible(true); + assertTrue(grid.isColumnHeadersVisible()); + assertTrue(state.columnHeadersVisible); + } + + @Test + public void testFooterVisibility() throws Exception { + + assertFalse(grid.isColumnFootersVisible()); + assertFalse(state.columnFootersVisible); + + grid.setColumnFootersVisible(false); + assertFalse(grid.isColumnFootersVisible()); + assertFalse(state.columnFootersVisible); + + grid.setColumnFootersVisible(true); + assertTrue(grid.isColumnFootersVisible()); + assertTrue(state.columnFootersVisible); + } + + @Test + public void testFrozenColumnByPropertyId() { + assertNull("Grid should not start with a frozen column", + grid.getLastFrozenPropertyId()); + + Object propertyId = grid.getContainerDatasource() + .getContainerPropertyIds().iterator().next(); + grid.setLastFrozenPropertyId(propertyId); + assertEquals(propertyId, grid.getLastFrozenPropertyId()); + + grid.getContainerDatasource().removeContainerProperty(propertyId); + assertNull(grid.getLastFrozenPropertyId()); + } + + private GridColumnState getColumnState(Object propertyId) { + String columnId = columnIdMapper.key(propertyId); + for (GridColumnState columnState : state.columns) { + if (columnState.id.equals(columnId)) { + return columnState; + } + } + return null; + } + +} |